-
Notifications
You must be signed in to change notification settings - Fork 248
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
Allow domain owner to change Private EVM contract creation allow list #3381
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the code good enough to deploy to Devnet?
If you just want to test it before deploy to Taurus, you can set up a local dev network with --chain dev
are there any API changes needed? (ideally they should happen before Rust or Devnet tests)
Not sure what kind of API you mean
do we want the call to take an encoded blob, or a list of accounts? There is a commit for each of these alternatives.
I much prefer to take the call argument as input instead of a whole encoded call, the required fraud proof and host function change should be similar to the messenger channel allowlist update.
are there any migrations needed for Taurus?
Not storage migration needed but we need to careful with the runtime API and host function change
let domain_runtime = Self::domain_runtime_code(domain_id).ok_or( | ||
Error::<T>::DomainRegistry(DomainRegistryError::DomainNotFound), | ||
)?; | ||
ensure!( | ||
domain_runtime_call( | ||
domain_runtime, | ||
StatelessDomainRuntimeCall::IsValidEvmDomainContractCreationAllowedByCall( | ||
contract_creation_allowed_by.clone() | ||
), | ||
) | ||
.unwrap_or(false), | ||
Error::<T>::InvalidEvmDomainContractCreationAllowedByCall | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed as we take the call argument instead of the encoded call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all argument values going to be valid for all EVM domains?
For example, what if a specific Private EVM domain wanted to ban Anyone
as an argument?
Edited:
This is required so that older EVM domains can reject the inherent:
https://github.com/autonomys/subspace/pull/3381/files#r1953459735
I added comments to explain why it's needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, what if a specific Private EVM domain wanted to ban Anyone as an argument?
No sure I understand, the domain owner create the domain and update the allowlist, if they don't want to use Anyone
no one can force the allowlist update to Anyone
If there are any validation needed for the arg in the future, that validation can be added to the inherent call (e.g. fail to execute if a wrong arg is provide), which is also in the domain runtime code but not like this in a runtime API and checked in the consensus runtime.
<Self as EvmTrackerApi<Block>>::construct_evm_contract_creation_allowed_by_extrinsic( | ||
self, | ||
Default::default(), | ||
inner_call, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to check runtime api version to see if this API exist in the domain runtime code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a new API, so there's no versioning here. What kind of check do you mean? Is there an example somewhere?
Edited:
I did it with a new API, because we only want EVM domains to implement that API. It doesn't make sense for other domains.
construct_domain_inherent_extrinsic
only calls construct_evm_contract_creation_allowed_by_extrinsic
if the fraud proof contains that EVM call. That call (and fraud proof) is only valid if the domain is an upgraded EVM domain.
If it's not EVM or not upgraded, then the existing code finds an unimplemented API method, and returns an error:
subspace/domains/client/block-preprocessor/src/stateless_runtime.rs
Lines 195 to 206 in deff3e2
self.executor | |
.call( | |
&mut ext, | |
&runtime_code, | |
fn_name, | |
&input, | |
CallContext::Offchain, | |
) | |
.0 | |
.map_err(|err| { | |
ApiError::Application(format!("Failed to invoke call to {fn_name}: {err}").into()) | |
}) |
This eventually causes verify_invalid_domain_extrinsics_root_fraud_proof
to return VerificationError::FailedToDeriveDomainInherentExtrinsic
, causing validate_fraud_proof
to return FraudProofError::InvalidExtrinsicRootFraudProof
, which marks the transaction as invalid.
I think this is correct behaviour, because a fraud proof containing an EVM call for a non-EVM domain (or an EVM domain that hasn't been upgraded) is an invalid fraud proof. Is there some other way we should handle the error?
Actually, I think there's one way this could go wrong:
- pallet-domains is upgraded and has the new call
- but the EVM domain is not upgraded, so the call is invalid
This is why we need is_valid_evm_contract_creation_allowed_by_call
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a new API, so there's no versioning here. What kind of check do you mean? Is there an example somewhere?
Actually, I think there's one way this could go wrong:
pallet-domains is upgraded and has the new call
but the EVM domain is not upgraded, so the call is invalid
This usually happens because the consensus and domain runtime are not guaranteed to happen in the same block.
But I don't think we need is_valid_evm_contract_creation_allowed_by_call
to prevent this, because if pallet-domains
is upgraded but the domain runtime is not then:
- During domain block production, even inherent data for the allowlist update is provided, the domain runtime doesn't have
construct_evm_contract_creation_allowed_by_extrinsic
to construct the inherent tx so no inherent tx added to the block - During FP verification, since the domain runtime code don't has
construct_evm_contract_creation_allowed_by_extrinsic
we simple returnNone
to keep consistency with the domain block production process.
@@ -102,6 +103,8 @@ pub struct DomainInherentExtrinsicData { | |||
pub consensus_transaction_byte_fee: Balance, | |||
pub domain_chain_allowlist: DomainAllowlistUpdates, | |||
pub maybe_sudo_runtime_call: Option<Vec<u8>>, | |||
pub maybe_evm_domain_contract_creation_allowed_by_call: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes both the input and output of the host function, while both the input and output will SCALE encoded and if this value is None
it should be fine, but I think it is better to have test to ensure it won't break anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are coming in the next PR, can you point me to an example of what this test looks like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are coming in the next PR, can you point me to an example of what this test looks like?
I'm actually not comfortable pushing this without testing. Reason being any runtime upgrades before the tests and might need potential fix will break the fraud proofs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you point me to an example of what this test looks like?
It is hard to do that in code because a lot of infra will be needed, but you can set up a local dev network with an old release and then upgrade the node with binary built from this branch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the quick review, I have a few follow-up questions!
let domain_runtime = Self::domain_runtime_code(domain_id).ok_or( | ||
Error::<T>::DomainRegistry(DomainRegistryError::DomainNotFound), | ||
)?; | ||
ensure!( | ||
domain_runtime_call( | ||
domain_runtime, | ||
StatelessDomainRuntimeCall::IsValidEvmDomainContractCreationAllowedByCall( | ||
contract_creation_allowed_by.clone() | ||
), | ||
) | ||
.unwrap_or(false), | ||
Error::<T>::InvalidEvmDomainContractCreationAllowedByCall | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all argument values going to be valid for all EVM domains?
For example, what if a specific Private EVM domain wanted to ban Anyone
as an argument?
Edited:
This is required so that older EVM domains can reject the inherent:
https://github.com/autonomys/subspace/pull/3381/files#r1953459735
I added comments to explain why it's needed.
@@ -102,6 +103,8 @@ pub struct DomainInherentExtrinsicData { | |||
pub consensus_transaction_byte_fee: Balance, | |||
pub domain_chain_allowlist: DomainAllowlistUpdates, | |||
pub maybe_sudo_runtime_call: Option<Vec<u8>>, | |||
pub maybe_evm_domain_contract_creation_allowed_by_call: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are coming in the next PR, can you point me to an example of what this test looks like?
<Self as EvmTrackerApi<Block>>::construct_evm_contract_creation_allowed_by_extrinsic( | ||
self, | ||
Default::default(), | ||
inner_call, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a new API, so there's no versioning here. What kind of check do you mean? Is there an example somewhere?
Edited:
I did it with a new API, because we only want EVM domains to implement that API. It doesn't make sense for other domains.
construct_domain_inherent_extrinsic
only calls construct_evm_contract_creation_allowed_by_extrinsic
if the fraud proof contains that EVM call. That call (and fraud proof) is only valid if the domain is an upgraded EVM domain.
If it's not EVM or not upgraded, then the existing code finds an unimplemented API method, and returns an error:
subspace/domains/client/block-preprocessor/src/stateless_runtime.rs
Lines 195 to 206 in deff3e2
self.executor | |
.call( | |
&mut ext, | |
&runtime_code, | |
fn_name, | |
&input, | |
CallContext::Offchain, | |
) | |
.0 | |
.map_err(|err| { | |
ApiError::Application(format!("Failed to invoke call to {fn_name}: {err}").into()) | |
}) |
This eventually causes verify_invalid_domain_extrinsics_root_fraud_proof
to return VerificationError::FailedToDeriveDomainInherentExtrinsic
, causing validate_fraud_proof
to return FraudProofError::InvalidExtrinsicRootFraudProof
, which marks the transaction as invalid.
I think this is correct behaviour, because a fraud proof containing an EVM call for a non-EVM domain (or an EVM domain that hasn't been upgraded) is an invalid fraud proof. Is there some other way we should handle the error?
Actually, I think there's one way this could go wrong:
- pallet-domains is upgraded and has the new call
- but the EVM domain is not upgraded, so the call is invalid
This is why we need is_valid_evm_contract_creation_allowed_by_call
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added commits that inline the redundant trait, and explain why we need to check calls for validity.
are there any API changes needed? (ideally they should happen before Rust or Devnet tests)
Not sure what kind of API you mean
Any changes to functions or types that we'll use in Rust tests?
Or any significant behaviour changes that we'll be testing in Rust or Devnet tests?
EthereumAccountId, | ||
>, | ||
) -> DispatchResult { | ||
let maybe_owner = ensure_signed(origin)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we usually prefix maybe_*
if its optional type
Error::<T>::DomainRegistry(DomainRegistryError::DomainNotFound), | ||
)?; | ||
ensure!( | ||
domain_runtime_call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be simplified.
Only reason we want to check is if the specific call exists in the Domain Runtime since consensus runtime upgrade may end up before domain runtime upgrade. Once the upgrade is pushed to domains, this call is redundant and can be removed. Benefits from saving a host function call
@@ -102,6 +103,8 @@ pub struct DomainInherentExtrinsicData { | |||
pub consensus_transaction_byte_fee: Balance, | |||
pub domain_chain_allowlist: DomainAllowlistUpdates, | |||
pub maybe_sudo_runtime_call: Option<Vec<u8>>, | |||
pub maybe_evm_domain_contract_creation_allowed_by_call: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are coming in the next PR, can you point me to an example of what this test looks like?
I'm actually not comfortable pushing this without testing. Reason being any runtime upgrades before the tests and might need potential fix will break the fraud proofs
// - timestamp extrinsic | ||
// - executive set_code extrinsic | ||
// - messenger update_domain_allowlist extrinsic | ||
// - block_fees transaction_byte_fee_extrinsic | ||
// - domain_sudo extrinsic | ||
// since we use `push_front` the extrinsic should be pushed in reversed order | ||
// - evm_tracker contract_creation_allowed_by extrinsic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the order is incorrect here. EVM tracker as per the contruct_runtime
of the EVM is after messenger
https://github.com/autonomys/subspace/blob/main/domains/runtime/evm/src/lib.rs#L840
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls also add a test case in test_bad_fraud_proof_is_rejected
by adding a allowlist update extrinsic after this line:
subspace/domains/client/domain-operator/src/tests.rs
Lines 2021 to 2027 in d170f27
alice | |
.construct_and_send_extrinsic(pallet_balances::Call::transfer_allow_death { | |
dest: Bob.to_account_id(), | |
value: 12334567890987654321, | |
}) | |
.await | |
.expect("Failed to send extrinsic"); |
/// An inherent call to set ContractCreationAllowedBy. | ||
#[pallet::call_index(1)] | ||
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Mandatory))] | ||
pub fn inherent_set_contract_creation_allowed_by( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inherent_*
prefix would be redundant here
@@ -73,6 +73,7 @@ pub type EthereumSignature = EVMSignature; | |||
|
|||
/// Some way of identifying an account on the EVM chain. We intentionally make it equivalent | |||
/// to the public key of the EVM transaction signing scheme. | |||
// TODO: sometimes this type alias causes complex trait ambiguity / conflicting implementation errors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note instead of TODO is better.
TODO would be something we want to handle in future but in this case I'm not sure about the future change
@@ -911,6 +914,34 @@ fn construct_sudo_call_extrinsic(encoded_ext: Vec<u8>) -> <Block as BlockT>::Ext | |||
) | |||
} | |||
|
|||
/// Returns `true` if this is a valid pallet-evm-tracker "contract creation allowed by" inherent | |||
/// call. | |||
fn is_valid_evm_contract_creation_allowed_by_call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can always adapt the code when we have such requirements. No need to future proof this right now. This just adds more noise right now
Thinking further on Here is what I'm thinking, we can allow users to pick the type of EVM they want
This also enables us to know the EVM type of a given Domain. We currently use This is not a blocker for this PR but would need to go in later before we deploy. I have also noticed another thing regarding the genesis hash of domain when it comes to taurus specifically this piece of code https://github.com/autonomys/subspace/blob/main/crates/pallet-domains/src/runtime_registry.rs#L156 When a new operator tries to sync from initial block, due to the above piece of code, currently running genesis hash would be different as So we would need to handle the migration in way that when requesting genesis storage using For mainnet this would not be an issue since there are no domains yet. |
Adding a test case in
I have considered this when I reviewed the previous private EVM PR, but it is not an issue because when the node syncs from genesis it will use the block where the domain is instantiated to derive the genesis domain block instead of the latest block in the chain. |
Okay that is fair. But in general problem still exists where I cannot use the latest block to actually derive the correct genesis hash. Ofcourse, we can always derive it from the block at which this was instantiated but Ideally at any block, shouldn't this always gives you same genesis for a given domain ? Or maybe I'm overthinking this situation |
That is true, but we don't have use case of this currently, we can keep the genesis ER in the consensus chain never pruned if needed. Also, even without the allowlist state, the issue still exists since the domain runtime code in the consensus chain will also change after upgrade. |
That is also correct. We should store the initial genesis state used at the time of domain instantiation in the domain object itself. This ensure, the genesis storage is untouched for the lifetime of the doman and will always gives the correct storage at any block not a blocker for this PR but if agreed, should be handled in a different PR through a tracking github issue |
I don't think we should do that, which will bloat up the consensus state and may end up storing all versions of domain runtime code in the worst case, and this is just for a non-exist use case. Again, we already store the |
Why would it store all the versions? It would store the genesis storage for each domain instance once. While this maybe a non existent issue but nevertheless an issue if we ever need to reconstruct the genesis. I do not see any reason why we should not do that 🤔 |
If there are domains instantiated at each runtime version, we at least need to keep one copy of each version of domain runtime.
Well, every domain node are able to reconstruct the genesis from consensus block history during sync. What you propose just reconstruct the domain genesis from any height of the consensus chain, as the genesis state is the stalest state I don't see why would anyone want to reconstruct and query from it.
As said above, it bloats up the consensus state for no good reason. |
moving this genesis storage discussion to this issue - #3388 |
Why are we doing this?
We want to allow the domain owner to change the EVM contract creation allow list. So we need to add a call to the subspace runtime, which gets turned into an inherent for the EVM domain runtime.
What it does
This PR makes the following changes:
EthereumAccountId
type in all code that deals with contract creation filters (calledAccountId
in EVM runtime code)pallet-domains
, and turn it into an inherent inpallet-evm-tracker
pallet-domains
call to take a list of accounts, rather than an encoded callThis is similar to
pallet-domain-sudo
, except that we're sending just one kind of call (earlier in the PR), or just the call argument (at the end of the PR).Breaking Changes
This PR has some (potentially) breaking changes:
What it doesn’t do
There are no tests yet, I'll do those in another PR, while we're testing on devnet.
Focus of Initial Reviews
The next step is testing on Devnet, and writing another PR with Rust tests. I don’t think we need to merge it to do that, but we should have any major changes done.
So here’s what I’d like to focus on in the review:
Code contributor checklist: