Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FRAME: Reintroduce TransactionExtension as a replacement for SignedExtension #3685

Open
wants to merge 158 commits into
base: master
Choose a base branch
from

Conversation

georgepisaltu
Copy link
Contributor

@georgepisaltu georgepisaltu commented Mar 13, 2024

Original PR #2280 reverted in #3665

This PR reintroduces the reverted functionality with additional changes, related effort here. Description is copied over from the original PR

First part of Extrinsic Horizon

Introduces a new trait TransactionExtension to replace SignedExtension. Introduce the idea of transactions which obey the runtime's extensions and have according Extension data (né Extra data) yet do not have hard-coded signatures.

Deprecate the terminology of "Unsigned" when used for transactions/extrinsics owing to there now being "proper" unsigned transactions which obey the extension framework and "old-style" unsigned which do not. Instead we have General for the former and Bare for the latter. (Ultimately, the latter will be phased out as a type of transaction, and Bare will only be used for Inherents.)

Types of extrinsic are now therefore:

  • Bare (no hardcoded signature, no Extra data; used to be known as "Unsigned")
    • Bare transactions (deprecated): Gossiped, validated with ValidateUnsigned (deprecated) and the _bare_compat bits of TransactionExtension (deprecated).
    • Inherents: Not gossiped, validated with ProvideInherent.
  • Extended (Extra data): Gossiped, validated via TransactionExtension.
    • Signed transactions (with a hardcoded signature).
    • General transactions (without a hardcoded signature).

TransactionExtension differs from SignedExtension because:

  • A signature on the underlying transaction may validly not be present.
  • It may alter the origin during validation.
  • pre_dispatch is renamed to prepare and need not contain the checks present in validate.
  • validate and prepare is passed an Origin rather than a AccountId.
  • validate may pass arbitrary information into prepare via a new user-specifiable type Val.
  • AdditionalSigned/additional_signed is renamed to Implicit/implicit. It is encoded for the entire transaction and passed in to each extension as a new argument to validate. This facilitates the ability of extensions to acts as underlying crypto.

There is a new DispatchTransaction trait which contains only default function impls and is impl'ed for any TransactionExtension impler. It provides several utility functions which reduce some of the tedium from using TransactionExtension (indeed, none of its regular functions should now need to be called directly).

Three transaction version discriminator ("versions") are now permissible (RFC here) in extrinsic version 5:

  • 0b00000101: Bare (used to be called "Unsigned"): contains Signature or Extra (extension data). After bare transactions are no longer supported, this will strictly identify an Inherents only.
  • 0b10000101: Old-school "Signed" Transaction: contains Signature, Extra (extension data) and an extension version byte, introduced as part of RFC99.
  • 0b01000101: New-school "General" Transaction: contains Extra (extension data) and an extension version byte, as per RFC99, but no Signature.

For the New-school General Transaction, it becomes trivial for authors to publish extensions to the mechanism for authorizing an Origin, e.g. through new kinds of key-signing schemes, ZK proofs, pallet state, mutations over pre-authenticated origins or any combination of the above.

UncheckedExtrinsic still maintains encode/decode backwards compatibility with extrinsic version 4, where the first byte was encoded as:

  • 0b00000100 - Unsigned transactions
  • 0b10000100 - Old-school Signed transactions, without the extension version byte

Now, UncheckedExtrinsic contains a Preamble and the actual call. The Preamble describes the type of extrinsic as follows:

/// A "header" for extrinsics leading up to the call itself. Determines the type of extrinsic and
/// holds any necessary specialized data.
#[derive(Eq, PartialEq, Clone)]
pub enum Preamble<Address, Signature, Extension> {
	/// An extrinsic without a signature or any extension. This means it's either an inherent or
	/// an old-school "Unsigned" (we don't use that terminology any more since it's confusable with
	/// the general transaction which is without a signature but does have an extension).
	///
	/// NOTE: In the future, once we remove `ValidateUnsigned`, this will only serve Inherent
	/// extrinsics and thus can be renamed to `Inherent`.
	Bare(ExtrinsicVersion),
	/// An old-school transaction extrinsic which includes a signature of some hard-coded crypto.
	Signed(Address, Signature, ExtensionVersion, Extension, ExtrinsicVersion),
	/// A new-school transaction extrinsic which does not include a signature by default. The
	/// origin authorization, through signatures or other means, is performed by the transaction
	/// extension in this extrinsic.
	General(ExtensionVersion, Extension),
}

Code Migration

NOW: Getting it to build

Wrap your SignedExtensions in AsTransactionExtension. This should be accompanied by renaming your aggregate type in line with the new terminology. E.g. Before:

/// The SignedExtension to the basic transaction logic.
pub type SignedExtra = (
	/* snip */
	MySpecialSignedExtension,
);
/// Unchecked extrinsic type as expected by this runtime.
pub type UncheckedExtrinsic =
	generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, SignedExtra>;

After:

/// The extension to the basic transaction logic.
pub type TxExtension = (
	/* snip */
	AsTransactionExtension<MySpecialSignedExtension>,
);
/// Unchecked extrinsic type as expected by this runtime.
pub type UncheckedExtrinsic =
	generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, TxExtension>;

You'll also need to alter any transaction building logic to add a .into() to make the conversion happen. E.g. Before:

fn construct_extrinsic(
		/* snip */
) -> UncheckedExtrinsic {
	let extra: SignedExtra = (
		/* snip */
		MySpecialSignedExtension::new(/* snip */),
	);
	let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap();
	let signature = payload.using_encoded(|e| sender.sign(e));
	UncheckedExtrinsic::new_signed(
		/* snip */
		Signature::Sr25519(signature),
		extra,
	)
}

After:

fn construct_extrinsic(
		/* snip */
) -> UncheckedExtrinsic {
	let tx_ext: TxExtension = (
		/* snip */
		MySpecialSignedExtension::new(/* snip */).into(),
	);
	let payload = SignedPayload::new(call.clone(), tx_ext.clone()).unwrap();
	let signature = payload.using_encoded(|e| sender.sign(e));
	UncheckedExtrinsic::new_signed(
		/* snip */
		Signature::Sr25519(signature),
		tx_ext,
	)
}

SOON: Migrating to TransactionExtension

Most SignedExtensions can be trivially converted to become a TransactionExtension. There are a few things to know.

  • Instead of a single trait like SignedExtension, you should now implement two traits individually: TransactionExtensionBase and TransactionExtension.
  • Weights are now a thing and must be provided via the new function fn weight.

TransactionExtensionBase

This trait takes care of anything which is not dependent on types specific to your runtime, most notably Call.

  • AdditionalSigned/additional_signed is renamed to Implicit/implicit.
  • Weight must be returned by implementing the weight function. If your extension is associated with a pallet, you'll probably want to do this via the pallet's existing benchmarking infrastructure.

TransactionExtension

Generally:

  • pre_dispatch is now prepare and you should not reexecute the validate functionality in there!
  • You don't get an account ID any more; you get an origin instead. If you need to presume an account ID, then you can use the trait function AsSystemOriginSigner::as_system_origin_signer.
  • You get an additional ticket, similar to Pre, called Val. This defines data which is passed from validate into prepare. This is important since you should not be duplicating logic from validate to prepare, you need a way of passing your working from the former into the latter. This is it.
  • This trait takes a Call type parameter. Call is the runtime call type which used to be an associated type; you can just move it to become a type parameter for your trait impl.
  • There's no AccountId associated type any more. Just remove it.

Regarding validate:

  • You get three new parameters in validate; all can be ignored when migrating from SignedExtension.
  • validate returns a tuple on success; the second item in the tuple is the new ticket type Self::Val which gets passed in to prepare. If you use any information extracted during validate (off-chain and on-chain, non-mutating) in prepare (on-chain, mutating) then you can pass it through with this. For the tuple's last item, just return the origin argument.

Regarding prepare:

  • This is renamed from pre_dispatch, but there is one change:
  • FUNCTIONALITY TO VALIDATE THE TRANSACTION NEED NOT BE DUPLICATED FROM validate!!
  • (This is different to SignedExtension which was required to run the same checks in pre_dispatch as in validate.)

Regarding post_dispatch:

  • Since there are no unsigned transactions handled by TransactionExtension, Pre is always defined, so the first parameter is Self::Pre rather than Option<Self::Pre>.

If you make use of SignedExtension::validate_unsigned or SignedExtension::pre_dispatch_unsigned, then:

  • Just use the regular versions of these functions instead.
  • Have your logic execute in the case that the origin is None.
  • Ensure your transaction creation logic creates a General Transaction rather than a Bare Transaction; this means having to include all TransactionExtensions' data.
  • ValidateUnsigned can still be used (for now) if you need to be able to construct transactions which contain none of the extension data, however these will be phased out in stage 2 of the Transactions Horizon, so you should consider moving to an extension-centric design.

@paritytech-review-bot paritytech-review-bot bot requested a review from a team March 13, 2024 19:39
@georgepisaltu georgepisaltu changed the title George/restore gav tx ext FRAME: Reintroduce TransactionExtension as a replacement for SignedExtension Mar 13, 2024
@georgepisaltu georgepisaltu added the T1-FRAME This PR/Issue is related to core FRAME, the framework. label Mar 13, 2024
Copy link
Contributor

@gui1117 gui1117 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

I did add some change to the PR, but it was always checked and approved by @georgepisaltu
If this is an abuse of review rules, I will remove my approval.

Signed-off-by: georgepisaltu <[email protected]>
Signed-off-by: georgepisaltu <[email protected]>
Signed-off-by: georgepisaltu <[email protected]>
Signed-off-by: georgepisaltu <[email protected]>
Signed-off-by: georgepisaltu <[email protected]>
Signed-off-by: georgepisaltu <[email protected]>
Copy link
Contributor

@bkontur bkontur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@georgepisaltu
Looks good so far for the Bridges part. I like the deduplication of the validate vs prepare stuff here. Could you please check: #5906? There are just some small docs nits.

Thanks to FakeDispatchable, I am able to compile the relayer part.

I am also running the bridges zombienet tests. For example, continuous-integration/gitlab-zombienet-bridges-0001-asset-transfer-works fails, but it seems this needs more investigation, as I might be encountering a probably PJS-API error Unsupported unsigned extrinsic version 5 - full stack:

2024-10-02 15:05:21        REGISTRY: Unknown signed extensions StorageWeightReclaim, CheckMetadataHash found, treating them as no-effect
2024-10-02 15:05:21        API/INIT: RPC methods not decorated: chainHead_v1_body, chainHead_v1_call, chainHead_v1_continue, chainHead_v1_follow, chainHead_v1_header, chainHead_v1_stopOperation, chainHead_v1_storage, chainHead_v1_unfollow, chainHead_v1_unpin, chainSpec_v1_chainName, chainSpec_v1_genesisHash, chainSpec_v1_properties, transactionWatch_v1_submitAndWatch, transactionWatch_v1_unwatch, transaction_v1_broadcast, transaction_v1_stop
2024-10-02 15:05:21        API/INIT: statemine/1015000: Not decorating runtime apis without matching versions: Core/5 (1/2/3/4 known)
2024-10-02 15:05:21        API/INIT: statemine/1015000: Not decorating unknown runtime apis: 0xd7bdd8a272ca0d65/1, 0x6ff52ee858e6c5bd/1, 0x91b1c8b16328eb92/1, 0x9ffb505aa738d69c/1, 0x695c80446b8b3d4e/1, 0xfbc577b9d747efd6/1
Error: createType(ExtrinsicUnknown):: Unsupported unsigned extrinsic version 5
    at createTypeUnsafe (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/types-create/cjs/create/type.js:54:22)
    at TypeRegistry.createTypeUnsafe (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/types/cjs/create/registry.js:230:52)
    at newFromValue (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/types/cjs/extrinsic/Extrinsic.js:25:21)
    at decodeExtrinsic (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/types/cjs/extrinsic/Extrinsic.js:33:16)
    at new GenericExtrinsic (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/types/cjs/extrinsic/Extrinsic.js:186:25)
    at new Submittable (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/api/cjs/submittable/createClass.js:55:13)
    at /home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/api/cjs/submittable/createSubmittable.js:7:27
    at Object.decorated [as forceCreate] (/home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/node_modules/@polkadot/api/cjs/base/Decorate.js:491:42)
    at /home/bparity/parity/polkadot-sdk/bridges/testing/framework/utils/generate_hex_encoded_call/index.js:99:38
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

Basically, we use PJS-API in the scripts to construct encoded call bytes for xcm::Transact and this PJS-API call exactly fails with error above:

const call = api.tx.foreignAssets.forceCreate(JSON.parse(assetId), assetOwnerAccountId, isSufficient, minBalance);
writeHexEncodedBytesToOutput(call.method, outputFile);

I am using testnet runtimes from this branch, so it looks like that actual runtimes returns something to the PJS-API, that causes the error above? I don't know. I will investigate and open an issue with PJS-API.

frame_metadata_hash_extension::CheckMetadataHash::new(false),
cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim::new(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim::new(),
cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim::new(),

@georgepisaltu
Copy link
Contributor Author

georgepisaltu commented Oct 2, 2024

I am also running the bridges zombienet tests. For example, continuous-integration/gitlab-zombienet-bridges-0001-asset-transfer-works fails, but it seems this needs more investigation, as I might be encountering a probably PAPI error Unsupported unsigned extrinsic version 5 - full stack

We are aware of this zombienet issue. The error seems to be coming from here and there is an effort in polkadot-js/api to support v5 extrinsics. I am not aware of the exact dependency tree in those scripts. We need to support the v5 extrinsic format in PAPI as well anyway.

Some of those changes could not be suggested in the main:
[PR](#3685).
Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will approve it, with some last remarks. Maybe some can be done in a follow up.

I'm still on the fence when it comes to treat RawOrigin::None as a "forbidden" origin. This will implicitly change a lot of code and I'm not a very big fan of this. IMO the not allowed origin should not be able to appear in the pallet code at all. Otherwise pallet functions that ignore the origin could run into problems (like batch for example).

Also please rename CreateInherent and do not abuse it for general transactions (aka old unsigned transactions), can probably be a follow up.

Comment on lines +36 to +41
T: Send + Sync,
<T as Config>::RuntimeCall: From<frame_system::Call<T>>,
<T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo> + GetDispatchInfo,
<<T as frame_system::Config>::RuntimeCall as Dispatchable>::PostInfo: Default,
<<T as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
AsSystemOriginSigner<T::AccountId> + AsTransactionAuthorizedOrigin + Clone,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
T: Send + Sync,
<T as Config>::RuntimeCall: From<frame_system::Call<T>>,
<T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo> + GetDispatchInfo,
<<T as frame_system::Config>::RuntimeCall as Dispatchable>::PostInfo: Default,
<<T as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
AsSystemOriginSigner<T::AccountId> + AsTransactionAuthorizedOrigin + Clone,
<T as Config>::RuntimeCall: From<frame_system::Call<T>>

Comment on lines +392 to +393
/// Weight information for the extensions of this pallet.
type ExtensionsWeightInfo = ();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not also just merge this into the default weights of the pallet?

}

/// Interface for creating an inherent.
pub trait CreateInherent<LocalCall>: CreateTransactionBase<LocalCall> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here CreateInherent which IMHO should be named CreateBare is to create a bare extrinsic from a call only, without transaction extension.

In the future we want to have these "old unsigned tx" be general transactions or not? So, this should be called CreateGeneral?

/// extension.
#[derive(RuntimeDebugNoBound)]
pub enum Pre {
/// The transaction extension weight should not be refund:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The transaction extension weight should not be refund:
/// The transaction extension weight should not be refund.

Comment on lines +204 to +206
fn weight(&self, _call: &Call) -> Weight {
Weight::zero()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this function should not provide a default implementation. Otherwise it is probably overseen to implement this, while this is quite important.

}

/// New instance of an old-school signed transaction on extrinsic format version 4.
pub fn new_signed_legacy(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Probably for tests? If yes, can we put it behind cfg(test)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Probably for tests? If yes, can we put it behind cfg(test)?

I think we will need it for the P/K bridge relayer, because Polkadot and Kusama runtime upgrades are asynchronous. At some point, KusamaBridgeHub will be upgraded to the new format (5), while PolkadotBridgeHub will still use the older format (4). Our relayer will need to support both new_signed and new_signed_legacy, for example here.

Can we deprecate it and remove it by Q1-Q2/25?

ext.encode_to(dest);
},
_ => {
// unreachable, versions are checked in the constructor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least add a debug_assert! that this is never hit.

///
/// This version needs to be bumped if there are breaking changes to the extension used in the
/// [UncheckedExtrinsic] implementation.
pub const EXTENSION_VERSION: ExtensionVersion = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub const EXTENSION_VERSION: ExtensionVersion = 0;
const EXTENSION_VERSION: ExtensionVersion = 0;

No need to ahve this public.

@@ -0,0 +1,70 @@
[package]
name = "pallet-verify-signature"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name = "pallet-verify-signature"
name = "frame-verify-signature-extension"

Not really a pallet. Also doesn't really need the Config trait etc. This can just be a standalone extension like CheckMetadataHash extension.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +854 to +878
pub enum Val<T: Config> {
Charge {
tip: BalanceOf<T>,
// who paid the fee
who: T::AccountId,
// transaction fee
fee: BalanceOf<T>,
},
NoCharge,
}

/// The info passed between the prepare and post-dispatch steps for the `ChargeAssetTxPayment`
/// extension.
pub enum Pre<T: Config> {
Charge {
tip: BalanceOf<T>,
// who paid the fee
who: T::AccountId,
// imbalance resulting from withdrawing the fee
imbalance: <<T as Config>::OnChargeTransaction as OnChargeTransaction<T>>::LiquidityInfo,
},
NoCharge {
// weight initially estimated by the extension, to be refunded
refund: Weight,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both could just be an Option that with None refunds the weight.

_len: usize,
) -> TransactionValidity {
Ok(ValidTransaction::default())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realize that this function is not implemented for tuples. How can people make use of AsTransactionExtension for signed extensions?
Isn't the convertion invalid?

Maybe we should implement some stuff for tuples, or just remove the AsTransactionExtension, it seems more error prone than helpful, do I miss something?

@bkontur
Copy link
Contributor

bkontur commented Oct 3, 2024

I am also running the bridges zombienet tests. For example, continuous-integration/gitlab-zombienet-bridges-0001-asset-transfer-works fails, but it seems this needs more investigation, as I might be encountering a probably PAPI error Unsupported unsigned extrinsic version 5 - full stack

We are aware of this zombienet issue. The error seems to be coming from here and there is an effort in polkadot-js/api to support v5 extrinsics. I am not aware of the exact dependency tree in those scripts. We need to support the v5 extrinsic format in PAPI as well anyway.

With the fix in polkadot-js/api#5976 and also in PJS/api-cli polkadot-js/api#5976 (comment), the bridges zombienet tests now work locally. I need to release a new relayer with support for ext. version 4/5, so I will prepare another PR with fixed zombienet tests.

@@ -0,0 +1,144 @@
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should be deleted.

if !auth.first.1.verify(&msg[..], &first_account) {
Err(InvalidTransaction::Custom(100))?
}
if !auth.second.1.verify(&msg[..], &second_account) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is like a fully state-less, offchain, 2/2 multisig

@@ -921,6 +942,7 @@ pub mod pallet {

/// Total length (in bytes) for all extrinsics put together, for the current block.
#[pallet::storage]
#[pallet::whitelist_storage]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

> {
// If the extension is disabled, return early.
let Self::Signed { signature, account } = &self else {
return Ok((Default::default(), (), origin))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little rust opinion: I find an explicit match more explicit here, this is equivalent to

match &self {
    Self::Signed(_) => {}
    _ => {}
}

which is not as good as

match &self {
    Self::Signed(_) => {}
    Self::Disabled => {}
}

pub use pallet::*;

#[frame_support::pallet]
pub mod pallet {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially a bit lost as to why this has to be a pallet. You need access to a few types like Signature, but you can also get those from generics on a standalone extension.

But I now see that there is a also the matter of benchmarking. We don't really have the machinery to benchmark extensions outside of a pallet.

Am I missing any other points?

Copy link
Contributor

@kianenigma kianenigma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no major issues found, but I stumbled upon a lot of open conversations, some of which are still valid. Good to do one final pass on all open conversations before merging.

PR deserves a revival of our old buy-this-man-a berr label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T1-FRAME This PR/Issue is related to core FRAME, the framework.
Projects
Status: Audited
Development

Successfully merging this pull request may close these issues.

9 participants