datahaven/operator/pallets/ethereum-client/src/tests_electra.rs
undercover-cactus b8cc9eee4b
feat: upgrade to polkadot SDK 2503 (#444)
## Polkadot upgrade 2503

In this PR, we upgrade form Polkadot SDK 2412 to Polkadot SDK 2503. In
order to upgrade the SDK we need to upgrade some dependencies :
StorageHub and frontier simultaneously.


## What changes 

### Trivial changes

* https://github.com/paritytech/polkadot-sdk/pull/7634 -> The new trait
is required in all the pallets using scale encoding.

* https://github.com/paritytech/polkadot-sdk/pull/7043 -> Remove
deprecated `sp-std` and replace with `alloc` or `core`.

* https://github.com/paritytech/polkadot-sdk/pull/6140 -> Accurate
weight reclaim with frame_system::WeightReclaim


### Breaking changes

* https://github.com/paritytech/polkadot-sdk/pull/2072 -> There is a
change in `pallet-referenda`. Now, the tracks are retrieved as a list of
`Track`s. Also, the names of the tracks might have some trailing null
values (`\0`). This means display representation of the tracks' names
must be sanitized.

* https://github.com/paritytech/polkadot-sdk/pull/5723 -> adds the
ability for these pallets to specify their source of the block number.
This is useful when these pallets are migrated from the relay chain to a
parachain and vice versa. (Not entirely sure it is breaking as it is
being marked as backward compatible).

* https://github.com/paritytech/polkadot-sdk/pull/6338 -> Update
Referenda to Support Block Number Provider

### Notable changes

* https://github.com/paritytech/polkadot-sdk/pull/5703 -> Not changes
required in the codebase but impact fastSync mode. Should improve
testing.

* https://github.com/paritytech/polkadot-sdk/pull/5842 -> Removes
`libp2p` types in authority-discovery, and replace them with network
backend agnostic types from `sc-network-types`. The `sc-network`
interface is therefore updated accordingly.

## What changes in Frontier 

* https://github.com/polkadot-evm/frontier/pull/1693 -> Add support for
EIP 7702 which has been enable with Pectra. This EIP add a new field
`AuthorizationList` in Ethereum transaction.

Changes example :

```rust
#[test]
fn validate_transaction_fails_on_filtered_call() {
...
            pallet_evm::Call::<Runtime>::call {
                source: H160::default(),
                target: H160::default(),
                input: Vec::new(),
                value: sp_core::U256::zero(),
                gas_limit: 21000,
                max_fee_per_gas: sp_core::U256::zero(),
                max_priority_fee_per_gas: Some(sp_core::U256::zero()),
                nonce: None,
                access_list: Vec::new(),
                authorization_list: Vec::new(),
            }
            .into(),
```

## ⚠️ Breaking Changes ⚠️

* Upgrade to Stirage hub v0.5.1
* Support for Ethereum latest upgrade (txs now have the
`authoriation_list` for support EIP 7702)

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 10:04:57 +01:00

960 lines
38 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pub use crate::mock_electra::*;
use crate::{
config::{EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT},
functions::compute_period,
mock_electra::{
get_message_verification_payload, load_checkpoint_update_fixture,
load_finalized_header_update_fixture, load_next_finalized_header_update_fixture,
load_next_sync_committee_update_fixture, load_sync_committee_update_fixture,
},
sync_committee_sum, verify_merkle_branch, BeaconHeader, CompactBeaconState, Error,
FinalizedBeaconState, LatestFinalizedBlockRoot, NextSyncCommittee, SyncCommitteePrepared,
};
use frame_support::{assert_err, assert_noop, assert_ok, pallet_prelude::Pays};
use hex_literal::hex;
use snowbridge_beacon_primitives::{
merkle_proof::{generalized_index_length, subtree_index},
types::deneb,
Fork, ForkVersions, NextSyncCommitteeUpdate, VersionedExecutionPayloadHeader,
};
use snowbridge_inbound_queue_primitives::{VerificationError, Verifier};
use sp_core::H256;
use sp_runtime::DispatchError;
/// Arbitrary hash used for tests and invalid hashes.
const TEST_HASH: [u8; 32] =
hex!["5f6f02af29218292d21a69b64a794a7c0873b3e0f54611972863706e8cbdf371"];
/* UNIT TESTS */
#[test]
pub fn sum_sync_committee_participation() {
new_tester().execute_with(|| {
assert_eq!(sync_committee_sum(&[0, 1, 0, 1, 1, 0, 1, 0, 1]), 5);
});
}
#[test]
pub fn compute_domain() {
new_tester().execute_with(|| {
let domain = EthereumBeaconClient::compute_domain(
hex!("07000000").into(),
hex!("00000001"),
hex!("5dec7ae03261fde20d5b024dfabce8bac3276c9a4908e23d50ba8c9b50b0adff").into(),
);
assert_ok!(&domain);
assert_eq!(
domain.unwrap(),
hex!("0700000046324489ceb6ada6d118eacdbe94f49b1fcb49d5481a685979670c7c").into()
);
});
}
#[test]
pub fn compute_signing_root_bls() {
new_tester().execute_with(|| {
let signing_root = EthereumBeaconClient::compute_signing_root(
&BeaconHeader {
slot: 3529537,
proposer_index: 192549,
parent_root: hex!(
"1f8dc05ea427f78e84e2e2666e13c3befb7106fd1d40ef8a3f67cf615f3f2a4c"
)
.into(),
state_root: hex!(
"0dfb492a83da711996d2d76b64604f9bca9dc08b6c13cf63b3be91742afe724b"
)
.into(),
body_root: hex!("66fba38f7c8c2526f7ddfe09c1a54dd12ff93bdd4d0df6a0950e88e802228bfa")
.into(),
},
hex!("07000000afcaaba0efab1ca832a15152469bb09bb84641c405171dfa2d3fb45f").into(),
);
assert_ok!(&signing_root);
assert_eq!(
signing_root.unwrap(),
hex!("3ff6e9807da70b2f65cdd58ea1b25ed441a1d589025d2c4091182026d7af08fb").into()
);
});
}
#[test]
pub fn compute_signing_root() {
new_tester().execute_with(|| {
let signing_root = EthereumBeaconClient::compute_signing_root(
&BeaconHeader {
slot: 222472,
proposer_index: 10726,
parent_root: hex!(
"5d481a9721f0ecce9610eab51d400d223683d599b7fcebca7e4c4d10cdef6ebb"
)
.into(),
state_root: hex!(
"14eb4575895f996a84528b789ff2e4d5148242e2983f03068353b2c37015507a"
)
.into(),
body_root: hex!("7bb669c75b12e0781d6fa85d7fc2f32d64eafba89f39678815b084c156e46cac")
.into(),
},
hex!("07000000e7acb21061790987fa1c1e745cccfb358370b33e8af2b2c18938e6c2").into(),
);
assert_ok!(&signing_root);
assert_eq!(
signing_root.unwrap(),
hex!("da12b6a6d3516bc891e8a49f82fc1925cec40b9327e06457f695035303f55cd8").into()
);
});
}
#[test]
pub fn compute_domain_bls() {
new_tester().execute_with(|| {
let domain = EthereumBeaconClient::compute_domain(
hex!("07000000").into(),
hex!("01000000"),
hex!("4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95").into(),
);
assert_ok!(&domain);
assert_eq!(
domain.unwrap(),
hex!("07000000afcaaba0efab1ca832a15152469bb09bb84641c405171dfa2d3fb45f").into()
);
});
}
#[test]
pub fn may_refund_call_fee() {
let finalized_update = Box::new(load_next_finalized_header_update_fixture());
let sync_committee_update = Box::new(load_sync_committee_update_fixture());
new_tester().execute_with(|| {
let free_headers_interval: u64 = crate::mock::FREE_SLOTS_INTERVAL as u64;
// Not free, smaller than the allowed free header interval
assert_eq!(
EthereumBeaconClient::check_refundable(
&finalized_update.clone(),
finalized_update.finalized_header.slot + free_headers_interval
),
Pays::Yes
);
// Is free, larger than the minimum interval
assert_eq!(
EthereumBeaconClient::check_refundable(
&finalized_update,
finalized_update.finalized_header.slot - (free_headers_interval + 2)
),
Pays::No
);
// Is free, valid sync committee update
assert_eq!(
EthereumBeaconClient::check_refundable(
&sync_committee_update,
finalized_update.finalized_header.slot
),
Pays::No
);
});
}
#[test]
pub fn verify_merkle_branch_for_finalized_root() {
new_tester().execute_with(|| {
assert!(verify_merkle_branch(
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
&[
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
hex!("5f6f02af29218292d21a69b64a794a7c0873b3e0f54611972863706e8cbdf371").into(),
hex!("e7125ff9ab5a840c44bedb4731f440a405b44e15f2d1a89e27341b432fabe13d").into(),
hex!("002c1fe5bc0bd62db6f299a582f2a80a6d5748ccc82e7ed843eaf0ae0739f74a").into(),
hex!("d2dc4ba9fd4edff6716984136831e70a6b2e74fca27b8097a820cbbaa5a6e3c3").into(),
hex!("91f77a19d8afa4a08e81164bb2e570ecd10477b3b65c305566a6d2be88510584").into(),
],
subtree_index(crate::config::altair::FINALIZED_ROOT_INDEX),
generalized_index_length(crate::config::altair::FINALIZED_ROOT_INDEX),
hex!("e46559327592741956f6beaa0f52e49625eb85dce037a0bd2eff333c743b287f").into()
));
});
}
#[test]
pub fn verify_merkle_branch_fails_if_depth_and_branch_dont_match() {
new_tester().execute_with(|| {
assert!(!verify_merkle_branch(
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
&[
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
hex!("5f6f02af29218292d21a69b64a794a7c0873b3e0f54611972863706e8cbdf371").into(),
hex!("e7125ff9ab5a840c44bedb4731f440a405b44e15f2d1a89e27341b432fabe13d").into(),
],
subtree_index(crate::config::altair::FINALIZED_ROOT_INDEX),
generalized_index_length(crate::config::altair::FINALIZED_ROOT_INDEX),
hex!("e46559327592741956f6beaa0f52e49625eb85dce037a0bd2eff333c743b287f").into()
));
});
}
#[test]
pub fn sync_committee_participation_is_supermajority() {
let bits =
hex!("bffffffff7f1ffdfcfeffeffbfdffffbfffffdffffefefffdffff7f7ffff77fffdf7bff77ffdf7fffafffffff77fefffeff7effffffff5f7fedfffdfb6ddff7b"
);
let participation =
snowbridge_beacon_primitives::decompress_sync_committee_bits::<512, 64>(bits);
assert_ok!(EthereumBeaconClient::sync_committee_participation_is_supermajority(&participation));
}
#[test]
pub fn sync_committee_participation_is_supermajority_errors_when_not_supermajority() {
new_tester().execute_with(|| {
let participation = hex!("0000000000000000000000000000000000000001010100010100000000000000000000000101010101000100010101010101010101010101010101010100010101000000000001010101010100010101000000000000000000000000000101000101010101010001010101010100010101010101010101010101000101010101010100010101010100000000010101010100000000000000000001010101010101010101010101010101010100010101010101010001010101010101010101010101010101000101010101010101010101010100010101010101010101010101010101010101010101010101010101010101010001010100010101010101010101000101010101010101010001010101010101010101000101010100010101010101010101010100010000000000000000000100000000000001010100000001000100010101010100000000000000000000000000000000000000010101010101010100010101010101010101010100010101010001010101010101010101010101010100000000000000000101010101000000000001000000000000000000010000000000000000000101010101010100010001010101010101000101010101010101010101010101010101000101010101010101010101010101010001010101010101010001010001000000000000000000000000000001000000000000");
assert_err!(
EthereumBeaconClient::sync_committee_participation_is_supermajority(&participation),
Error::<Test>::SyncCommitteeParticipantsNotSupermajority
);
});
}
#[test]
fn compute_fork_version() {
let mock_fork_versions = ForkVersions {
genesis: Fork {
version: [0, 0, 0, 0],
epoch: 0,
},
altair: Fork {
version: [0, 0, 0, 1],
epoch: 10,
},
bellatrix: Fork {
version: [0, 0, 0, 2],
epoch: 20,
},
capella: Fork {
version: [0, 0, 0, 3],
epoch: 30,
},
deneb: Fork {
version: [0, 0, 0, 4],
epoch: 40,
},
electra: Fork {
version: [0, 0, 0, 5],
epoch: 50,
},
fulu: Fork {
version: [0, 0, 0, 6],
epoch: 60,
},
};
new_tester().execute_with(|| {
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 0),
[0, 0, 0, 0]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 1),
[0, 0, 0, 0]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 10),
[0, 0, 0, 1]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 21),
[0, 0, 0, 2]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 20),
[0, 0, 0, 2]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 32),
[0, 0, 0, 3]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 40),
[0, 0, 0, 4]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 50),
[0, 0, 0, 5]
);
assert_eq!(
EthereumBeaconClient::select_fork_version(&mock_fork_versions, 60),
[0, 0, 0, 6]
);
});
}
#[test]
fn find_absent_keys() {
let participation: [u8; 32] =
hex!("0001010101010100010101010101010101010101010101010101010101010101").into();
let update = load_sync_committee_update_fixture();
let sync_committee_prepared: SyncCommitteePrepared = (&update
.next_sync_committee_update
.unwrap()
.next_sync_committee)
.try_into()
.unwrap();
new_tester().execute_with(|| {
let pubkeys = EthereumBeaconClient::find_pubkeys(
&participation,
(*sync_committee_prepared.pubkeys).as_ref(),
false,
);
assert_eq!(pubkeys.len(), 2);
assert_eq!(pubkeys[0], sync_committee_prepared.pubkeys[0]);
assert_eq!(pubkeys[1], sync_committee_prepared.pubkeys[7]);
});
}
#[test]
fn find_present_keys() {
let participation: [u8; 32] =
hex!("0001000000000000010000000000000000000000000000000000010000000100").into();
let update = load_sync_committee_update_fixture();
let sync_committee_prepared: SyncCommitteePrepared = (&update
.next_sync_committee_update
.unwrap()
.next_sync_committee)
.try_into()
.unwrap();
new_tester().execute_with(|| {
let pubkeys = EthereumBeaconClient::find_pubkeys(
&participation,
(*sync_committee_prepared.pubkeys).as_ref(),
true,
);
assert_eq!(pubkeys.len(), 4);
assert_eq!(pubkeys[0], sync_committee_prepared.pubkeys[1]);
assert_eq!(pubkeys[1], sync_committee_prepared.pubkeys[8]);
assert_eq!(pubkeys[2], sync_committee_prepared.pubkeys[26]);
assert_eq!(pubkeys[3], sync_committee_prepared.pubkeys[30]);
});
}
/* SYNC PROCESS TESTS */
#[test]
fn process_initial_checkpoint() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::force_checkpoint(
RuntimeOrigin::root(),
checkpoint.clone()
));
let block_root: H256 = checkpoint.header.hash_tree_root().unwrap();
assert!(<FinalizedBeaconState<Test>>::contains_key(block_root));
});
}
#[test]
fn process_initial_checkpoint_with_invalid_sync_committee_proof() {
let mut checkpoint = Box::new(load_checkpoint_update_fixture());
checkpoint.current_sync_committee_branch[0] = TEST_HASH.into();
new_tester().execute_with(|| {
assert_err!(
EthereumBeaconClient::force_checkpoint(RuntimeOrigin::root(), checkpoint),
Error::<Test>::InvalidSyncCommitteeMerkleProof
);
});
}
#[test]
fn process_initial_checkpoint_with_invalid_blocks_root_proof() {
let mut checkpoint = Box::new(load_checkpoint_update_fixture());
checkpoint.block_roots_branch[0] = TEST_HASH.into();
new_tester().execute_with(|| {
assert_err!(
EthereumBeaconClient::force_checkpoint(RuntimeOrigin::root(), checkpoint),
Error::<Test>::InvalidBlockRootsRootMerkleProof
);
});
}
#[test]
fn submit_update_in_current_period() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_finalized_header_update_fixture());
let initial_period = compute_period(checkpoint.header.slot);
let update_period = compute_period(update.finalized_header.slot);
assert_eq!(initial_period, update_period);
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
let block_root: H256 = update.finalized_header.hash_tree_root().unwrap();
assert!(<FinalizedBeaconState<Test>>::contains_key(block_root));
});
}
#[test]
fn submit_update_with_sync_committee_in_current_period() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_sync_committee_update_fixture());
let init_period = compute_period(checkpoint.header.slot);
let update_period = compute_period(update.finalized_header.slot);
assert_eq!(init_period, update_period);
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert!(!<NextSyncCommittee<Test>>::exists());
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
assert!(<NextSyncCommittee<Test>>::exists());
});
}
#[test]
fn reject_submit_update_in_next_period() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let sync_committee_update = Box::new(load_sync_committee_update_fixture());
let finalized_update = Box::new(load_finalized_header_update_fixture());
let update = Box::new(load_next_finalized_header_update_fixture());
let sync_committee_period = compute_period(sync_committee_update.finalized_header.slot);
let next_sync_committee_period = compute_period(update.finalized_header.slot);
assert_eq!(sync_committee_period + 1, next_sync_committee_period);
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
// interim update required so the header gap is not too large
let other_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), finalized_update);
assert_ok!(other_result);
// check an update in the next period is rejected
let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(second_result, Error::<Test>::SyncCommitteeUpdateRequired);
assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_invalid_header_proof() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let mut update = Box::new(load_sync_committee_update_fixture());
let init_period = compute_period(checkpoint.header.slot);
let update_period = compute_period(update.finalized_header.slot);
assert_eq!(init_period, update_period);
update.finality_branch[0] = TEST_HASH.into();
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert!(!<NextSyncCommittee<Test>>::exists());
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(result, Error::<Test>::InvalidHeaderMerkleProof);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_invalid_block_roots_proof() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let mut update = Box::new(load_sync_committee_update_fixture());
let init_period = compute_period(checkpoint.header.slot);
let update_period = compute_period(update.finalized_header.slot);
assert_eq!(init_period, update_period);
update.block_roots_branch[0] = TEST_HASH.into();
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert!(!<NextSyncCommittee<Test>>::exists());
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(result, Error::<Test>::InvalidBlockRootsRootMerkleProof);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_invalid_next_sync_committee_proof() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let mut update = Box::new(load_sync_committee_update_fixture());
let init_period = compute_period(checkpoint.header.slot);
let update_period = compute_period(update.finalized_header.slot);
assert_eq!(init_period, update_period);
if let Some(ref mut next_sync_committee_update) = update.next_sync_committee_update {
next_sync_committee_update.next_sync_committee_branch[0] = TEST_HASH.into();
}
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert!(!<NextSyncCommittee<Test>>::exists());
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(result, Error::<Test>::InvalidSyncCommitteeMerkleProof);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_skipped_period() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let sync_committee_update = Box::new(load_sync_committee_update_fixture());
let mut update = Box::new(load_next_finalized_header_update_fixture());
update.signature_slot += (EPOCHS_PER_SYNC_COMMITTEE_PERIOD * SLOTS_PER_EPOCH) as u64;
update.attested_header.slot = update.signature_slot - 1;
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result =
EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update.clone());
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(second_result, Error::<Test>::SkippedSyncCommitteePeriod);
assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_sync_committee_in_next_period() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_sync_committee_update_fixture());
let next_update = Box::new(load_next_sync_committee_update_fixture());
let update_period = compute_period(update.finalized_header.slot);
let next_update_period = compute_period(next_update.finalized_header.slot);
assert_eq!(update_period + 1, next_update_period);
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert!(!<NextSyncCommittee<Test>>::exists());
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
assert!(<NextSyncCommittee<Test>>::exists());
let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update);
assert_ok!(second_result);
assert_eq!(second_result.unwrap().pays_fee, Pays::No);
let last_finalized_state =
FinalizedBeaconState::<Test>::get(LatestFinalizedBlockRoot::<Test>::get()).unwrap();
let last_synced_period = compute_period(last_finalized_state.slot);
assert_eq!(last_synced_period, next_update_period);
});
}
#[test]
fn submit_update_with_sync_committee_invalid_signature_slot() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let mut update = Box::new(load_sync_committee_update_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
// makes an invalid update with signature_slot should be more than attested_slot
update.signature_slot = update.attested_header.slot;
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(result, Error::<Test>::InvalidUpdateSlot);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_skipped_sync_committee_period() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let finalized_update = Box::new(load_next_finalized_header_update_fixture());
let checkpoint_period = compute_period(checkpoint.header.slot);
let next_sync_committee_period = compute_period(finalized_update.finalized_header.slot);
assert_eq!(checkpoint_period + 1, next_sync_committee_period);
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), finalized_update);
assert_err!(result, Error::<Test>::SkippedSyncCommitteePeriod);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_irrelevant_update() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let mut update = Box::new(load_next_finalized_header_update_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
// makes an invalid update where the attested_header slot value should be greater than the
// checkpoint slot value
update.finalized_header.slot = checkpoint.header.slot;
update.attested_header.slot = checkpoint.header.slot;
update.signature_slot = checkpoint.header.slot + 1;
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(result, Error::<Test>::IrrelevantUpdate);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_missing_bootstrap() {
let update = Box::new(load_next_finalized_header_update_fixture());
new_tester().execute_with(|| {
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_err!(result, Error::<Test>::NotBootstrapped);
assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn submit_update_with_invalid_sync_committee_update() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_sync_committee_update_fixture());
let mut next_update = Box::new(load_next_sync_committee_update_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
// makes update with invalid next_sync_committee
<FinalizedBeaconState<Test>>::mutate(<LatestFinalizedBlockRoot<Test>>::get(), |x| {
let prev = x.unwrap();
*x = Some(CompactBeaconState {
slot: next_update.attested_header.slot,
..prev
});
});
next_update.attested_header.slot += 1;
next_update.signature_slot = next_update.attested_header.slot + 1;
let next_sync_committee = NextSyncCommitteeUpdate::default();
next_update.next_sync_committee_update = Some(next_sync_committee);
let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update);
assert_err!(second_result, Error::<Test>::InvalidSyncCommitteeUpdate);
assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
/// Check that a gap of more than 8192 slots between finalized headers is not allowed.
#[test]
fn submit_finalized_header_update_with_too_large_gap() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_sync_committee_update_fixture());
let mut next_update = Box::new(load_next_sync_committee_update_fixture());
// Adds 8193 slots, so that the next update is still in the next sync committee, but the
// gap between the finalized headers is more than 8192 slots.
let slot_with_large_gap = checkpoint.header.slot + SLOTS_PER_HISTORICAL_ROOT as u64 + 1;
next_update.finalized_header.slot = slot_with_large_gap;
// Adding some slots to the attested header and signature slot since they need to be ahead
// of the finalized header.
next_update.attested_header.slot = slot_with_large_gap + 33;
next_update.signature_slot = slot_with_large_gap + 43;
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
assert!(<NextSyncCommittee<Test>>::exists());
let second_result =
EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone());
assert_err!(second_result, Error::<Test>::InvalidFinalizedHeaderGap);
assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
/// Check that a gap of 8192 slots between finalized headers is allowed.
#[test]
fn submit_finalized_header_update_with_gap_at_limit() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_sync_committee_update_fixture());
let mut next_update = Box::new(load_next_sync_committee_update_fixture());
next_update.finalized_header.slot = checkpoint.header.slot + SLOTS_PER_HISTORICAL_ROOT as u64;
// Adding some slots to the attested header and signature slot since they need to be ahead
// of the finalized header.
next_update.attested_header.slot =
checkpoint.header.slot + SLOTS_PER_HISTORICAL_ROOT as u64 + 33;
next_update.signature_slot = checkpoint.header.slot + SLOTS_PER_HISTORICAL_ROOT as u64 + 43;
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
assert!(<NextSyncCommittee<Test>>::exists());
let second_result =
EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone());
assert_err!(
second_result,
// The test should pass the InvalidFinalizedHeaderGap check, and will fail at the
// next check, the merkle proof, because we changed the next_update slots.
Error::<Test>::InvalidHeaderMerkleProof
);
assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
});
}
#[test]
fn duplicate_sync_committee_updates_are_not_free() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let sync_committee_update = Box::new(load_sync_committee_update_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
let result =
EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update.clone());
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, Pays::No);
// Check that if the same update is submitted, the update is not free.
let second_result =
EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update);
assert_ok!(second_result);
assert_eq!(second_result.unwrap().pays_fee, Pays::Yes);
});
}
/* IMPLS */
#[test]
fn verify_message() {
let (event_log, proof) = get_message_verification_payload();
new_tester().execute_with(|| {
assert_ok!(initialize_storage());
assert_ok!(EthereumBeaconClient::verify(&event_log, &proof));
});
}
#[test]
fn verify_message_invalid_proof() {
let (event_log, mut proof) = get_message_verification_payload();
proof.receipt_proof.1[0] = TEST_HASH.into();
new_tester().execute_with(|| {
assert_ok!(initialize_storage());
assert_err!(
EthereumBeaconClient::verify(&event_log, &proof),
VerificationError::InvalidProof
);
});
}
#[test]
fn verify_message_invalid_receipts_root() {
let (event_log, mut proof) = get_message_verification_payload();
let mut payload = deneb::ExecutionPayloadHeader::default();
payload.receipts_root = TEST_HASH.into();
proof.execution_proof.execution_header = VersionedExecutionPayloadHeader::Deneb(payload);
new_tester().execute_with(|| {
assert_ok!(initialize_storage());
assert_err!(
EthereumBeaconClient::verify(&event_log, &proof),
VerificationError::InvalidExecutionProof(
Error::<Test>::BlockBodyHashTreeRootFailed.into()
)
);
});
}
#[test]
fn verify_message_invalid_log() {
let (mut event_log, proof) = get_message_verification_payload();
event_log.topics = vec![H256::zero(); 10];
new_tester().execute_with(|| {
assert_ok!(initialize_storage());
assert_err!(
EthereumBeaconClient::verify(&event_log, &proof),
VerificationError::InvalidLog
);
});
}
#[test]
fn verify_message_receipt_does_not_contain_log() {
let (mut event_log, proof) = get_message_verification_payload();
event_log.data = hex!("f9013c94ee9170abfbf9421ad6dd07f6bdec9d89f2b581e0f863a01b11dcf133cc240f682dab2d3a8e4cd35c5da8c9cf99adac4336f8512584c5ada000000000000000000000000000000000000000000000000000000000000003e8a00000000000000000000000000000000000000000000000000000000000000002b8c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000068000f000000000000000101d184c103f7acc340847eee82a0b909e3358bc28d440edffa1352b13227e8ee646f3ea37456dec70100000101001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c0000e8890423c78a0000000000000000000000000000000000000000000000000000000000000000").to_vec();
new_tester().execute_with(|| {
assert_ok!(initialize_storage());
assert_err!(
EthereumBeaconClient::verify(&event_log, &proof),
VerificationError::LogNotFound
);
});
}
#[test]
fn set_operating_mode() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let update = Box::new(load_finalized_header_update_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert_ok!(EthereumBeaconClient::set_operating_mode(
RuntimeOrigin::root(),
snowbridge_core::BasicOperatingMode::Halted
));
assert_noop!(
EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
Error::<Test>::Halted
);
});
}
#[test]
fn set_operating_mode_root_only() {
new_tester().execute_with(|| {
assert_noop!(
EthereumBeaconClient::set_operating_mode(
RuntimeOrigin::signed(1),
snowbridge_core::BasicOperatingMode::Halted
),
DispatchError::BadOrigin
);
});
}
#[test]
fn verify_execution_proof_invalid_ancestry_proof() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let finalized_header_update = Box::new(load_finalized_header_update_fixture());
let mut execution_header_update = Box::new(load_execution_proof_fixture());
if let Some(ref mut ancestry_proof) = execution_header_update.ancestry_proof {
ancestry_proof.header_branch[0] = TEST_HASH.into()
}
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert_ok!(EthereumBeaconClient::submit(
RuntimeOrigin::signed(1),
finalized_header_update
));
assert_err!(
EthereumBeaconClient::verify_execution_proof(&execution_header_update),
Error::<Test>::InvalidAncestryMerkleProof
);
});
}
#[test]
fn verify_execution_proof_invalid_execution_header_proof() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let finalized_header_update = Box::new(load_finalized_header_update_fixture());
let mut execution_header_update = Box::new(load_execution_proof_fixture());
execution_header_update.execution_branch[0] = TEST_HASH.into();
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert_ok!(EthereumBeaconClient::submit(
RuntimeOrigin::signed(1),
finalized_header_update
));
assert_err!(
EthereumBeaconClient::verify_execution_proof(&execution_header_update),
Error::<Test>::InvalidExecutionHeaderProof
);
});
}
#[test]
fn verify_execution_proof_that_is_also_finalized_header_which_is_not_stored() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let finalized_header_update = Box::new(load_finalized_header_update_fixture());
let mut execution_header_update = Box::new(load_execution_proof_fixture());
execution_header_update.ancestry_proof = None;
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert_ok!(EthereumBeaconClient::submit(
RuntimeOrigin::signed(1),
finalized_header_update
));
assert_err!(
EthereumBeaconClient::verify_execution_proof(&execution_header_update),
Error::<Test>::ExpectedFinalizedHeaderNotStored
);
});
}
#[test]
fn submit_execution_proof_that_is_also_finalized_header_which_is_stored_but_slots_dont_match() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let finalized_header_update = Box::new(load_finalized_header_update_fixture());
let mut execution_header_update = Box::new(load_execution_proof_fixture());
execution_header_update.ancestry_proof = None;
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert_ok!(EthereumBeaconClient::submit(
RuntimeOrigin::signed(1),
finalized_header_update
));
let block_root: H256 = execution_header_update.header.hash_tree_root().unwrap();
<FinalizedBeaconState<Test>>::insert(
block_root,
CompactBeaconState {
slot: execution_header_update.header.slot + 1,
block_roots_root: Default::default(),
},
);
LatestFinalizedBlockRoot::<Test>::set(block_root);
assert_err!(
EthereumBeaconClient::verify_execution_proof(&execution_header_update),
Error::<Test>::ExpectedFinalizedHeaderNotStored
);
});
}
#[test]
fn verify_execution_proof_not_finalized() {
let checkpoint = Box::new(load_checkpoint_update_fixture());
let finalized_header_update = Box::new(load_finalized_header_update_fixture());
let update = Box::new(load_execution_proof_fixture());
new_tester().execute_with(|| {
assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
assert_ok!(EthereumBeaconClient::submit(
RuntimeOrigin::signed(1),
finalized_header_update
));
<FinalizedBeaconState<Test>>::mutate(<LatestFinalizedBlockRoot<Test>>::get(), |x| {
let prev = x.unwrap();
*x = Some(CompactBeaconState {
slot: update.header.slot - 1,
..prev
});
});
assert_err!(
EthereumBeaconClient::verify_execution_proof(&update),
Error::<Test>::HeaderNotFinalized
);
});
}