Skip to content

Commit 26cc301

Browse files
authored
fix: checkpoints getter and crash due to the usage of default MemTracker in MemoryDB (#126)
1 parent 1ac623d commit 26cc301

File tree

9 files changed

+138
-69
lines changed

9 files changed

+138
-69
lines changed

ethereum-common/src/memory_db.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use super::{keccak_hasher::KeccakHasher, *};
2-
use ::memory_db::HashKey;
2+
use ::memory_db::{HashKey, NoopTracker};
33

4-
pub type MemoryDB = ::memory_db::MemoryDB<KeccakHasher, HashKey<KeccakHasher>, Vec<u8>>;
4+
pub type MemoryDB =
5+
::memory_db::MemoryDB<KeccakHasher, HashKey<KeccakHasher>, Vec<u8>, NoopTracker<Vec<u8>>>;
56

67
pub fn new() -> MemoryDB {
78
memory_db::MemoryDB::from_null_node(&rlp::NULL_RLP, rlp::NULL_RLP.as_ref().into())

gear-programs/checkpoint-light-client/src/wasm/state.rs

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -125,37 +125,26 @@ impl<const N: usize> Checkpoints<N> {
125125

126126
Err(0) => Err(CheckpointError::OutDated),
127127

128-
Err(index) if index < self.slots.len() => {
128+
Err(index) => {
129129
let (index_previous, slot_previous) = self.slots[index - 1];
130-
let (index_next, slot_next) = self.slots[index];
131-
132-
let gap = match (slot_next - slot_previous) % SLOTS_PER_EPOCH {
133-
// both slots are divisable by SLOTS_PER_EPOCH and the distance
134-
// between them is greater than SLOTS_PER_EPOCH
135-
0 if slot_previous + SLOTS_PER_EPOCH < slot_next => true,
136-
_ => false,
137-
};
138-
139-
let offset = ((slot - 1 - slot_previous) / SLOTS_PER_EPOCH + 1) as usize;
140-
let slot_checkpoint = slot_previous + offset as u64 * SLOTS_PER_EPOCH;
141-
if slot_previous % SLOTS_PER_EPOCH != 0
142-
|| slot_next < slot_checkpoint
143-
|| slot_next > slot_checkpoint + SLOTS_PER_EPOCH
144-
|| gap
145-
{
146-
Ok((slot_next, self.checkpoints[index_next]))
147-
} else {
148-
Ok((slot_checkpoint, self.checkpoints[index_previous + offset]))
130+
let maybe_next = self.slots.get(index);
131+
132+
let count = maybe_next
133+
.map(|(index_next, _slot)| *index_next)
134+
.unwrap_or(self.checkpoints.len())
135+
- index_previous;
136+
for i in 1..count {
137+
let slot_next = slot_previous + i as u64 * SLOTS_PER_EPOCH;
138+
if slot <= slot_next {
139+
return Ok((slot_next, self.checkpoints[index_previous + i]));
140+
}
149141
}
150-
}
151142

152-
_ => {
153-
let offset = ((slot - 1 - slot_last) / SLOTS_PER_EPOCH + 1) as usize;
154-
let slot_checkpoint = slot_last + offset as u64 * SLOTS_PER_EPOCH;
155-
let index = index_last + offset;
156-
match self.checkpoints.get(index) {
157-
Some(checkpoint) => Ok((slot_checkpoint, *checkpoint)),
143+
match maybe_next {
158144
None => Err(CheckpointError::NotPresent),
145+
Some((index_next, slot_next)) => {
146+
Ok((*slot_next, self.checkpoints[*index_next]))
147+
}
159148
}
160149
}
161150
}
@@ -423,3 +412,61 @@ fn checkpoints_with_gaps() {
423412

424413
compare_checkpoints(&data, &checkpoints);
425414
}
415+
416+
#[test]
417+
fn checkpoints_get() {
418+
use hex_literal::hex;
419+
420+
const COUNT: usize = 7;
421+
422+
// Holesky
423+
let data = [
424+
(
425+
2_498_432,
426+
hex!("192cbc312720ee203ed023837c7dd7783db6cee1f1b9d57411f348e8a143a308").into(),
427+
),
428+
(
429+
2_498_464,
430+
hex!("b89c6d200193f865b85a3f323b75d2b10346564a330229d8a5c695968206faf1").into(),
431+
),
432+
(
433+
2_498_496,
434+
hex!("4185e76eb0865e9ae5f8ea7601407261d1db9e66ba10818ebe717976d9bf201c").into(),
435+
),
436+
(
437+
2_498_527,
438+
hex!("e722020546e89a17228aa9365e5418aaf09d9c31b014a0b4df911a54702ccd57").into(),
439+
),
440+
(
441+
2_498_560,
442+
hex!("b50cd206a8ba4019baad810bbcd4fe1871be4944ea9cb06e15259376e996afde").into(),
443+
),
444+
(
445+
2_498_592,
446+
hex!("844300ded738bdad37cc202ad4ade0cc79f0e4aa311e8fee5668cb20341c52aa").into(),
447+
),
448+
(
449+
2_498_624,
450+
hex!("aca973372ac65cd5203e1521ba941bbbf836c5e591a9b459ca061c79a5740023").into(),
451+
),
452+
];
453+
assert_eq!(data.len(), COUNT);
454+
455+
let mut checkpoints = Checkpoints::<COUNT>::new();
456+
457+
for (slot, checkpoint) in &data {
458+
checkpoints.push(*slot, *checkpoint);
459+
}
460+
461+
assert!(checkpoints.checkpoint(2_498_625).is_err());
462+
463+
for i in 1..data.len() {
464+
let (slot_previous, _checkpoint) = data[i - 1];
465+
let (expected_slot, expected_checkpoint) = data[i];
466+
for slot in (1 + slot_previous)..=expected_slot {
467+
let (actual_slot, actual_checkpoint) = checkpoints.checkpoint(slot).unwrap();
468+
assert_eq!(actual_slot, expected_slot, "slot = {slot}");
469+
assert_eq!(actual_checkpoint, expected_checkpoint);
470+
}
471+
}
472+
}

gear-programs/checkpoint-light-client/src/wasm/sync_update.rs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ use committee::{Error as CommitteeError, Update as CommitteeUpdate};
33
use gstd::debug;
44

55
pub async fn handle(state: &mut State<STORED_CHECKPOINTS_COUNT>, sync_update: SyncCommitteeUpdate) {
6+
if eth_utils::calculate_epoch(state.finalized_header.slot) + io::sync_update::MAX_EPOCHS_GAP
7+
<= eth_utils::calculate_epoch(sync_update.finalized_header.slot)
8+
{
9+
let result = HandleResult::SyncUpdate(Err(io::sync_update::Error::ReplayBackRequired {
10+
replay_back: state
11+
.replay_back
12+
.as_ref()
13+
.map(|replay_back| meta::ReplayBack {
14+
finalized_header: replay_back.finalized_header.slot,
15+
last_header: replay_back.last_header.slot,
16+
}),
17+
checkpoint: state
18+
.checkpoints
19+
.last()
20+
.expect("The program should be initialized so there is a checkpoint"),
21+
}));
22+
msg::reply(result, 0).expect("Unable to reply with `HandleResult::SyncUpdate::Error`");
23+
24+
return;
25+
}
26+
627
let (finalized_header_update, committee_update) = match verify(
728
&state.network,
829
&state.finalized_header,
@@ -23,28 +44,6 @@ pub async fn handle(state: &mut State<STORED_CHECKPOINTS_COUNT>, sync_update: Sy
2344
};
2445

2546
if let Some(finalized_header) = finalized_header_update {
26-
if eth_utils::calculate_epoch(state.finalized_header.slot) + io::sync_update::MAX_EPOCHS_GAP
27-
<= eth_utils::calculate_epoch(finalized_header.slot)
28-
{
29-
let result =
30-
HandleResult::SyncUpdate(Err(io::sync_update::Error::ReplayBackRequired {
31-
replay_back: state
32-
.replay_back
33-
.as_ref()
34-
.map(|replay_back| meta::ReplayBack {
35-
finalized_header: replay_back.finalized_header.slot,
36-
last_header: replay_back.last_header.slot,
37-
}),
38-
checkpoint: state
39-
.checkpoints
40-
.last()
41-
.expect("The program should be initialized so there is a checkpoint"),
42-
}));
43-
msg::reply(result, 0).expect("Unable to reply with `HandleResult::SyncUpdate::Error`");
44-
45-
return;
46-
}
47-
4847
state
4948
.checkpoints
5049
.push(finalized_header.slot, finalized_header.tree_hash_root());

relayer/src/ethereum_checkpoints/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use futures::{
1212
};
1313
use gclient::{EventProcessor, GearApi, WSAddress};
1414
use parity_scale_codec::Decode;
15-
use reqwest::Client;
15+
use reqwest::{Client, ClientBuilder};
1616
use tokio::{
1717
signal::unix::{self, SignalKind},
1818
sync::mpsc::{self, Sender},
@@ -41,6 +41,7 @@ pub async fn relay(args: RelayCheckpointsArgs) {
4141
let RelayCheckpointsArgs {
4242
program_id,
4343
beacon_endpoint,
44+
beacon_timeout,
4445
vara_domain,
4546
vara_port,
4647
vara_suri,
@@ -59,10 +60,14 @@ pub async fn relay(args: RelayCheckpointsArgs) {
5960
.and_then(|bytes| <[u8; 32]>::try_from(bytes).ok())
6061
.expect("Expecting correct ProgramId");
6162

63+
let client_http = ClientBuilder::new()
64+
.timeout(Duration::from_secs(beacon_timeout))
65+
.build()
66+
.expect("Reqwest client should be created");
67+
6268
let mut signal_interrupt = unix::signal(SignalKind::interrupt()).expect("Set SIGINT handler");
6369

6470
let (sender, mut receiver) = mpsc::channel(SIZE_CHANNEL);
65-
let client_http = Client::new();
6671

6772
sync_update::spawn_receiver(client_http.clone(), beacon_endpoint.clone(), sender);
6873

relayer/src/ethereum_checkpoints/replay_back.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ async fn replay_back_slots(
126126
slots_batch_iter: SlotsBatchIter,
127127
) -> AnyResult<()> {
128128
for (slot_start, slot_end) in slots_batch_iter {
129+
log::debug!("slot_start = {slot_start}, slot_end = {slot_end}");
129130
replay_back_slots_inner(
130131
client_http,
131132
beacon_endpoint,

relayer/src/ethereum_checkpoints/tests/mod.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::utils::{self, slots_batch, FinalityUpdateResponse};
1+
use super::utils::{self, slots_batch, BootstrapResponse, FinalityUpdateResponse, UpdateData};
22
use checkpoint_light_client::WASM_BINARY;
33
use checkpoint_light_client_io::{
44
ethereum_common::{
@@ -19,6 +19,8 @@ const FINALITY_UPDATE_5_254_112: &[u8; 4_940] =
1919
include_bytes!("./sepolia-finality-update-5_254_112.json");
2020
const FINALITY_UPDATE_5_263_072: &[u8; 4_941] =
2121
include_bytes!("./sepolia-finality-update-5_263_072.json");
22+
const UPDATE_640: &[u8; 57_202] = include_bytes!("./sepolia-update-640.json");
23+
const BOOTSTRAP_640: &[u8; 54_328] = include_bytes!("./sepolia-bootstrap-640.json");
2224

2325
async fn common_upload_program(
2426
client: &GearApi,
@@ -65,12 +67,13 @@ async fn init(network: Network) -> Result<()> {
6567

6668
// use the latest finality header as a checkpoint for bootstrapping
6769
let finality_update = utils::get_finality_update(&client_http, RPC_URL).await?;
68-
let current_period = eth_utils::calculate_period(finality_update.finalized_header.slot);
70+
let slot = finality_update.finalized_header.slot;
71+
let current_period = eth_utils::calculate_period(slot);
6972
let mut updates = utils::get_updates(&client_http, RPC_URL, current_period, 1).await?;
7073

7174
println!(
7275
"finality_update slot = {}, period = {}",
73-
finality_update.finalized_header.slot, current_period
76+
slot, current_period
7477
);
7578

7679
let update = match updates.pop() {
@@ -419,11 +422,8 @@ async fn replaying_back() -> Result<()> {
419422
Ok(())
420423
}
421424

422-
#[ignore]
423425
#[tokio::test]
424426
async fn sync_update_requires_replaying_back() -> Result<()> {
425-
let client_http = Client::new();
426-
427427
let finality_update: FinalityUpdateResponse =
428428
serde_json::from_slice(FINALITY_UPDATE_5_263_072).unwrap();
429429
let finality_update = finality_update.data;
@@ -433,18 +433,25 @@ async fn sync_update_requires_replaying_back() -> Result<()> {
433433
);
434434

435435
let slot = finality_update.finalized_header.slot;
436-
let current_period = eth_utils::calculate_period(slot);
437-
let mut updates = utils::get_updates(&client_http, RPC_URL, current_period, 1).await?;
436+
let mut updates: Vec<UpdateData> = serde_json::from_slice(UPDATE_640).unwrap();
438437

439438
let update = match updates.pop() {
440439
Some(update) if updates.is_empty() => update.data,
441440
_ => unreachable!("Requested single update"),
442441
};
443442

444-
let checkpoint = update.finalized_header.tree_hash_root();
445-
let checkpoint_hex = hex::encode(checkpoint);
443+
let BootstrapResponse { data: bootstrap } = serde_json::from_slice(BOOTSTRAP_640).unwrap();
444+
445+
let checkpoint_update = update.finalized_header.tree_hash_root();
446+
let checkpoint_bootstrap = bootstrap.header.tree_hash_root();
447+
assert_eq!(
448+
checkpoint_update,
449+
checkpoint_bootstrap,
450+
"checkpoint_update = {}, checkpoint_bootstrap = {}",
451+
hex::encode(checkpoint_update),
452+
hex::encode(checkpoint_bootstrap)
453+
);
446454

447-
let bootstrap = utils::get_bootstrap(&client_http, RPC_URL, &checkpoint_hex).await?;
448455
let signature = <G2 as ark_serialize::CanonicalDeserialize>::deserialize_compressed(
449456
&update.sync_aggregate.sync_committee_signature.0 .0[..],
450457
)
@@ -497,10 +504,13 @@ async fn sync_update_requires_replaying_back() -> Result<()> {
497504

498505
let (_message_id, payload, _value) = listener.reply_bytes_on(message_id).await?;
499506
let result_decoded = HandleResult::decode(&mut &payload.unwrap()[..]).unwrap();
500-
assert!(matches!(
501-
result_decoded,
502-
HandleResult::SyncUpdate(Err(sync_update::Error::ReplayBackRequired { .. }))
503-
));
507+
assert!(
508+
matches!(
509+
result_decoded,
510+
HandleResult::SyncUpdate(Err(sync_update::Error::ReplayBackRequired { .. }))
511+
),
512+
"result_decoded = {result_decoded:?}"
513+
);
504514

505515
Ok(())
506516
}

relayer/src/ethereum_checkpoints/tests/sepolia-bootstrap-640.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

relayer/src/ethereum_checkpoints/tests/sepolia-update-640.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

relayer/src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,14 @@ struct RelayCheckpointsArgs {
141141
#[arg(long, env = "CHECKPOINT_LIGHT_CLIENT_ADDRESS")]
142142
program_id: String,
143143

144-
/// Specify an endpoint providing Beacon API
144+
/// Specify the endpoint providing Beacon API
145145
#[arg(long, env = "BEACON_ENDPOINT")]
146146
beacon_endpoint: String,
147147

148+
/// Specify the timeout in seconds for requests to the Beacon API endpoint
149+
#[arg(long, default_value = "120", env = "BEACON_TIMEOUT")]
150+
beacon_timeout: u64,
151+
148152
/// Domain of the VARA RPC endpoint
149153
#[arg(long, default_value = "ws://127.0.0.1", env = "VARA_DOMAIN")]
150154
vara_domain: String,

0 commit comments

Comments
 (0)