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

chore: Require replaying back if there is a gap in checkpoints #92

Merged
merged 6 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
},
}
150 changes: 113 additions & 37 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 All @@ -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!();
Expand All @@ -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 All @@ -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!();
Expand All @@ -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
Loading