diff --git a/ethereum-common/src/utils.rs b/ethereum-common/src/utils.rs index c00c787e..215bc1e5 100644 --- a/ethereum-common/src/utils.rs +++ b/ethereum-common/src/utils.rs @@ -1,10 +1,12 @@ use super::*; use core::str::FromStr; -pub fn calculate_period(slot: u64) -> u64 { - let epoch = slot / SLOTS_PER_EPOCH; +pub fn calculate_epoch(slot: u64) -> u64 { + slot / SLOTS_PER_EPOCH +} - epoch / EPOCHS_PER_SYNC_COMMITTEE +pub fn calculate_period(slot: u64) -> u64 { + calculate_epoch(slot) / EPOCHS_PER_SYNC_COMMITTEE } pub fn decode_hex_bytes<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/gear-programs/checkpoint-light-client/io/src/sync_update.rs b/gear-programs/checkpoint-light-client/io/src/sync_update.rs index a0b23048..f852f8e8 100644 --- a/gear-programs/checkpoint-light-client/io/src/sync_update.rs +++ b/gear-programs/checkpoint-light-client/io/src/sync_update.rs @@ -1,5 +1,8 @@ use super::*; +// The constant defines how many epochs may be skipped. +pub const MAX_EPOCHS_GAP: u64 = 3; + #[derive(Debug, Clone, Encode, Decode, TypeInfo)] pub struct SyncCommitteeUpdate { pub signature_slot: u64, @@ -23,4 +26,8 @@ pub enum Error { InvalidFinalityProof, InvalidNextSyncCommitteeProof, InvalidPublicKeys, + ReplayBackRequired { + replayed_slot: Option, + checkpoint: (Slot, Hash256), + }, } diff --git a/gear-programs/checkpoint-light-client/src/tests/mod.rs b/gear-programs/checkpoint-light-client/src/tests/mod.rs index 1b72132e..747420f0 100644 --- a/gear-programs/checkpoint-light-client/src/tests/mod.rs +++ b/gear-programs/checkpoint-light-client/src/tests/mod.rs @@ -9,7 +9,7 @@ use checkpoint_light_client_io::{ network::Network, utils as eth_utils, SLOTS_PER_EPOCH, }, - replay_back, + replay_back, sync_update, tree_hash::TreeHash, ArkScale, BeaconBlockHeader, G1TypeInfo, G2TypeInfo, Handle, HandleResult, Init, SyncCommitteeUpdate, @@ -26,6 +26,8 @@ const RPC_URL: &str = "http://127.0.0.1:5052"; const FINALITY_UPDATE_5_254_112: &[u8; 4_940] = include_bytes!("./sepolia-finality-update-5_254_112.json"); +const FINALITY_UPDATE_5_263_072: &[u8; 4_941] = + include_bytes!("./sepolia-finality-update-5_263_072.json"); #[derive(Deserialize)] #[serde(untagged)] @@ -177,7 +179,28 @@ fn map_public_keys(compressed_public_keys: &[BLSPubKey]) -> Vec SyncCommitteeUpdate { +fn sync_update_from_finality( + signature: G2, + finality_update: FinalityUpdate, +) -> SyncCommitteeUpdate { + SyncCommitteeUpdate { + signature_slot: finality_update.signature_slot, + attested_header: finality_update.attested_header, + finalized_header: finality_update.finalized_header, + sync_aggregate: finality_update.sync_aggregate, + sync_committee_next_aggregate_pubkey: None, + sync_committee_signature: G2TypeInfo(signature).into(), + sync_committee_next_pub_keys: None, + sync_committee_next_branch: None, + finality_branch: finality_update + .finality_branch + .into_iter() + .map(|BytesFixed(array)| array.0) + .collect::<_>(), + } +} + +fn sync_update_from_update(update: Update) -> SyncCommitteeUpdate { let signature = ::deserialize_compressed( &update.sync_aggregate.sync_committee_signature.0 .0[..], ) @@ -268,7 +291,7 @@ async fn init_and_updating() -> Result<()> { let checkpoint_hex = hex::encode(checkpoint); let bootstrap = get_bootstrap(&mut client_http, &checkpoint_hex).await?; - let sync_update = create_sync_update(update); + let sync_update = sync_update_from_update(update); let pub_keys = map_public_keys(&bootstrap.current_sync_committee.pubkeys.0); let init = Init { @@ -289,7 +312,7 @@ async fn init_and_updating() -> Result<()> { let program_id = upload_program(&client, &mut listener, init).await?; - println!("program_id = {:?}", hex::encode(&program_id)); + println!("program_id = {:?}", hex::encode(program_id)); println!(); println!(); @@ -303,7 +326,7 @@ async fn init_and_updating() -> Result<()> { match updates.pop() { Some(update) if updates.is_empty() && update.data.finalized_header.slot >= slot => { println!("update sync committee"); - let payload = Handle::SyncUpdate(create_sync_update(update.data)); + let payload = Handle::SyncUpdate(sync_update_from_update(update.data)); let gas_limit = client .calculate_handle_gas(None, program_id.into(), payload.encode(), 0, true) .await? @@ -335,21 +358,7 @@ async fn init_and_updating() -> Result<()> { continue; }; - let payload = Handle::SyncUpdate(SyncCommitteeUpdate { - signature_slot: update.signature_slot, - attested_header: update.attested_header, - finalized_header: update.finalized_header, - sync_aggregate: update.sync_aggregate, - sync_committee_next_aggregate_pubkey: None, - sync_committee_signature: G2TypeInfo(signature).into(), - sync_committee_next_pub_keys: None, - sync_committee_next_branch: None, - finality_branch: update - .finality_branch - .into_iter() - .map(|BytesFixed(array)| array.0) - .collect::<_>(), - }); + let payload = Handle::SyncUpdate(sync_update_from_finality(signature, update)); let gas_limit = client .calculate_handle_gas(None, program_id.into(), payload.encode(), 0, true) @@ -405,7 +414,7 @@ async fn replaying_back() -> Result<()> { println!("bootstrap slot = {}", bootstrap.header.slot); println!("update slot = {}", update.finalized_header.slot); - let sync_update = create_sync_update(update); + let sync_update = sync_update_from_update(update); let slot_start = sync_update.finalized_header.slot; let slot_end = finality_update.finalized_header.slot; println!( @@ -432,7 +441,7 @@ async fn replaying_back() -> Result<()> { let program_id = upload_program(&client, &mut listener, init).await?; - println!("program_id = {:?}", hex::encode(&program_id)); + println!("program_id = {:?}", hex::encode(program_id)); println!(); println!(); @@ -456,21 +465,7 @@ async fn replaying_back() -> Result<()> { .unwrap(); let payload = Handle::ReplayBackStart { - sync_update: SyncCommitteeUpdate { - signature_slot: finality_update.signature_slot, - attested_header: finality_update.attested_header, - finalized_header: finality_update.finalized_header, - sync_aggregate: finality_update.sync_aggregate, - sync_committee_next_aggregate_pubkey: None, - sync_committee_signature: G2TypeInfo(signature).into(), - sync_committee_next_pub_keys: None, - sync_committee_next_branch: None, - finality_branch: finality_update - .finality_branch - .into_iter() - .map(|BytesFixed(array)| array.0) - .collect::<_>(), - }, + sync_update: sync_update_from_finality(signature, finality_update), headers, }; @@ -563,3 +558,84 @@ async fn replaying_back() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn sync_update_requires_replaying_back() -> Result<()> { + let mut client_http = Client::new(); + + let finality_update: FinalityUpdateResponse = + serde_json::from_slice(FINALITY_UPDATE_5_263_072).unwrap(); + let finality_update = finality_update.data; + println!( + "finality_update slot = {}", + finality_update.finalized_header.slot + ); + + let slot = finality_update.finalized_header.slot; + let current_period = eth_utils::calculate_period(slot); + let mut updates = get_updates(&mut client_http, current_period, 1).await?; + + let update = match updates.pop() { + Some(update) if updates.is_empty() => update.data, + _ => unreachable!("Requested single update"), + }; + + let checkpoint = update.finalized_header.tree_hash_root(); + let checkpoint_hex = hex::encode(checkpoint); + + let bootstrap = get_bootstrap(&mut client_http, &checkpoint_hex).await?; + let sync_update = sync_update_from_update(update); + + let pub_keys = map_public_keys(&bootstrap.current_sync_committee.pubkeys.0); + let init = Init { + network: Network::Sepolia, + sync_committee_current_pub_keys: Box::new(FixedArray(pub_keys.try_into().unwrap())), + sync_committee_current_aggregate_pubkey: bootstrap.current_sync_committee.aggregate_pubkey, + sync_committee_current_branch: bootstrap + .current_sync_committee_branch + .into_iter() + .map(|BytesFixed(bytes)| bytes.0) + .collect(), + update: sync_update, + }; + + let client = GearApi::dev().await?; + let mut listener = client.subscribe().await?; + + let program_id = upload_program(&client, &mut listener, init).await?; + + println!("program_id = {:?}", hex::encode(program_id)); + + println!(); + println!(); + + println!( + "slot = {slot:?}, attested slot = {:?}, signature slot = {:?}", + finality_update.attested_header.slot, finality_update.signature_slot + ); + let signature = ::deserialize_compressed( + &finality_update.sync_aggregate.sync_committee_signature.0 .0[..], + ) + .unwrap(); + + let payload = Handle::SyncUpdate(sync_update_from_finality(signature, finality_update)); + + let gas_limit = client + .calculate_handle_gas(None, program_id.into(), payload.encode(), 0, true) + .await? + .min_limit; + println!("finality_update gas_limit {gas_limit:?}"); + + let (message_id, _) = client + .send_message(program_id.into(), payload, gas_limit, 0) + .await?; + + let (_message_id, payload, _value) = listener.reply_bytes_on(message_id).await?; + let result_decoded = HandleResult::decode(&mut &payload.unwrap()[..]).unwrap(); + assert!(matches!( + result_decoded, + HandleResult::SyncUpdate(Err(sync_update::Error::ReplayBackRequired { .. })) + )); + + Ok(()) +} diff --git a/gear-programs/checkpoint-light-client/src/tests/sepolia-finality-update-5_263_072.json b/gear-programs/checkpoint-light-client/src/tests/sepolia-finality-update-5_263_072.json new file mode 100644 index 00000000..c0a99324 --- /dev/null +++ b/gear-programs/checkpoint-light-client/src/tests/sepolia-finality-update-5_263_072.json @@ -0,0 +1 @@ +{"version":"deneb","data":{"attested_header":{"beacon":{"slot":"5263147","proposer_index":"728","parent_root":"0x639b8361bdbf0a5afe1285e1bc1ef87f07e92f47e6cfc707f28e8d53004f2ab1","state_root":"0xa9e1d2ea0d223b9367f1492a374fb24770ac9e920ab980a6f0a33e240ba08017","body_root":"0x4355cd9218f154ab80a2994cb166c580ec17ff1f5d7c2599e018db7e0d06472b"},"execution":{"parent_hash":"0x5ed7237de6824f726cfdc36c40e112d390a126574729b570e210157f685f99ae","fee_recipient":"0x9b7e335088762ad8061c04d08c37902abc8acb87","state_root":"0x708f24d0915404d413e5d2f97d1ae2dd0d06361b8d65330149451183ca51575f","receipts_root":"0x7ea6187cce4ed7bfdc1d1c374c09a105a2a85c4b8515dc7e56f1f2173cc915a0","logs_bloom":"0x4008002c0441c94122114a02c61000604045020e9004510200c08b2050822bc2048a014120820009c43200d3020498f03212005d3ab000361102520920a44c60181282e1c58c2023c001822a11e0924b0405510002045102a0071a31010af0015091484c4a425d8400192d5024009980820a04e3000500280808623019a101408720073086739910a4894800111b10605c0064887e42a81580198148d580011826090023b00431002940d0880104900208d3881c5a52491104b0020250404028f00680221938080cd5f93d00590e8041185b0320940c88150b00956d04456a0001180032891810401a295221a50200310208b4002a3b0002a200405048984100","prev_randao":"0x767788cc922f3aeef9b6371cbd2d37af4c99aecd4b0c769066c5c050998e0559","block_number":"6147945","gas_limit":"30000000","gas_used":"14720002","timestamp":"1718891364","extra_data":"0xd883010d0b846765746888676f312e32312e36856c696e7578","base_fee_per_gas":"141363940","block_hash":"0x4fc065529fffc15033b44c025fcfd979d1495f5080563763ba245457b449b31e","transactions_root":"0xa126b04aa1a1ed174c4d06dfd3767f034c8901e5eb10af15350856ca2ef499da","withdrawals_root":"0x55b354df720ba5f7144291cdeb103667dcff4c2168b85b9a3ce3a5cea8a3e13b","blob_gas_used":"131072","excess_blob_gas":"0"},"execution_branch":["0x72b53b70b88a54ccb7fe7559449d67561ca83be6237fa75cd85f126fed4ab19a","0x3ddc2367d261f389dc6ad05bc2e3f2cc78929344b606339dc5c4abee89bd16d1","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x9b37a9317948b93b9a017347c255ab237527efaa0516e3cb75d7e1dd00ba9d95"]},"finalized_header":{"beacon":{"slot":"5263072","proposer_index":"974","parent_root":"0x63480487508bb6158ca3ad4cf4e867be9150a52f0a0c7cffffe0f972775bf83b","state_root":"0xbdaabacebafaff88a6d1bbd3bfc8b48999e33649306ed7c01631f7b989e06e77","body_root":"0xe42380ede88307b7b6676981457a00eac0d61da34000722fbbfee969886c1e35"},"execution":{"parent_hash":"0xb750f52a78d6f87930b364bdc00b0cd44840d6574b68d49fbc6d09066b41dcf3","fee_recipient":"0xe276bc378a527a8792b353cdca5b5e53263dfb9e","state_root":"0x1b37a610ee1345c7e208a55ac21ee107f880b102058172b06ece55dd6e69466e","receipts_root":"0x993e31dc0388bbfb1f1b34d7542d50efde503a992945f8b2116c3876efc73b3b","logs_bloom":"0x2418040c18014099051402121af0493cca3b489711041c417781a2b143876402045818c65a8fc0827a12120326464ea4ac177b565ea0015e91b9520800e412c155210241ef14284a4042104ba10043500443754c3634418ea0063a9ac352b0404dc11a110aa132c08ac8cd6013b55d63547859c39026100ad851029824890257164d6c310072c831b7817c0b20aaa220100258308144f30c500b702d84e4000a232ba01291081112325109410154588aa0d882709044e48004d5f2030296028360408ee2587800084213a4428c861a9a8220a1a816dbd845425cd0d088006124641c04a041490524212d504036828224260117904bf124024319805001e14039","prev_randao":"0x4e0443497740f6cabeda7ed619fee27efcd1b429965c3014c20ac8a0f8810251","block_number":"6147870","gas_limit":"30000000","gas_used":"13294600","timestamp":"1718890464","extra_data":"0x","base_fee_per_gas":"17802559","block_hash":"0x750de0f923b8e73d5052792a6b1877ed853901a603312ecef1d291c1deaa7ea8","transactions_root":"0x51e3a5c17dd005249eca446353188566e458c14038893d5b98427fff11567380","withdrawals_root":"0xcff1d0dd8c8a607148aa0277757084ae56358981450c425f7568c3b1fd8717db","blob_gas_used":"524288","excess_blob_gas":"0"},"execution_branch":["0x843b3a323c070a7d8b1328c0383f1c440f0b775e52bd1fd562ed453f0abf6821","0xd7e277737a0a26f1cf43b6f2b719a2c70da959584c47d1a2025cb3471874961d","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0xef5b6af042eb654221c15a7881a72025ed6d0cec82e319e2f574b245c8259cd4"]},"finality_branch":["0x7782020000000000000000000000000000000000000000000000000000000000","0x3d4be5d019ba15ea3ef304a83b8a067f2e79f46a3fac8069306a6c814a0a35eb","0xe87f373959d7a663ae7a22fcdd74021e7cc7d5d259bdf612454a20715827ca9a","0x51570dae610bcd959de827c2ac03ec17fe6d07e5f46220f0f834091a38e3a4a8","0xaa41acb397add848b91480eaa3079b7e22d1c584f7664d740b6c92fd446a380f","0x8ee91c6d246f41f845ebd16c25c832c19461cc1ce265857f2f02edfe04b68334"],"sync_aggregate":{"sync_committee_bits":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","sync_committee_signature":"0x82f3a33e7c136921f2c4af30554c02906724b1e50388f289401a39396efc6aafe7787711cb64b21a832ebf02a421e1ef189ab542a0a24ff9323c5b1445d4d08b779fc3c87b0961da37eb5983468a999fe0ea2cfad00662fbfc0a054ff6093923"},"signature_slot":"5263148"}} \ No newline at end of file diff --git a/gear-programs/checkpoint-light-client/src/wasm/sync_update.rs b/gear-programs/checkpoint-light-client/src/wasm/sync_update.rs index 4d2732f0..c09b286b 100644 --- a/gear-programs/checkpoint-light-client/src/wasm/sync_update.rs +++ b/gear-programs/checkpoint-light-client/src/wasm/sync_update.rs @@ -23,6 +23,25 @@ pub async fn handle(state: &mut State, sync_update: Sy }; if let Some(finalized_header) = finalized_header_update { + if eth_utils::calculate_epoch(state.finalized_header.slot) + io::sync_update::MAX_EPOCHS_GAP + <= eth_utils::calculate_epoch(finalized_header.slot) + { + let result = + HandleResult::SyncUpdate(Err(io::sync_update::Error::ReplayBackRequired { + replayed_slot: state + .replay_back + .as_ref() + .map(|replay_back| replay_back.last_header.slot), + checkpoint: state + .checkpoints + .last() + .expect("The program should be initialized so there is a checkpoint"), + })); + msg::reply(result, 0).expect("Unable to reply with `HandleResult::SyncUpdate::Error`"); + + return; + } + state .checkpoints .push(finalized_header.slot, finalized_header.tree_hash_root());