Skip to content

Commit

Permalink
chore: Require replaying back if there is a gap in checkpoints (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
gshep authored Jul 18, 2024
1 parent 7d5b4de commit 06a794e
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 38 deletions.
8 changes: 5 additions & 3 deletions ethereum-common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>, D::Error>
Expand Down
7 changes: 7 additions & 0 deletions gear-programs/checkpoint-light-client/io/src/sync_update.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,4 +26,8 @@ pub enum Error {
InvalidFinalityProof,
InvalidNextSyncCommitteeProof,
InvalidPublicKeys,
ReplayBackRequired {
replayed_slot: Option<Slot>,
checkpoint: (Slot, Hash256),
},
}
146 changes: 111 additions & 35 deletions gear-programs/checkpoint-light-client/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -177,7 +179,28 @@ fn map_public_keys(compressed_public_keys: &[BLSPubKey]) -> Vec<ArkScale<G1TypeI
.collect()
}

fn create_sync_update(update: Update) -> 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 = <G2 as ark_serialize::CanonicalDeserialize>::deserialize_compressed(
&update.sync_aggregate.sync_committee_signature.0 .0[..],
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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 = <G2 as ark_serialize::CanonicalDeserialize>::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(())
}
Original file line number Diff line number Diff line change
@@ -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"}}
19 changes: 19 additions & 0 deletions gear-programs/checkpoint-light-client/src/wasm/sync_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ pub async fn handle(state: &mut State<STORED_CHECKPOINTS_COUNT>, 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());
Expand Down

0 comments on commit 06a794e

Please sign in to comment.