Skip to content

Commit ed1c00d

Browse files
committed
fix: allow fallback validation to accept codec-shifted digests in chain proofs
1 parent 54a2122 commit ed1c00d

File tree

1 file changed

+134
-1
lines changed
  • event-svc/src/event/validator

1 file changed

+134
-1
lines changed

event-svc/src/event/validator/time.rs

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,35 @@ impl TimeEventValidator {
9090
// Compare the root hash in the TimeEvent's AnchorProof to the root hash that was actually
9191
// included in the transaction onchain. We compare hashes (not full CIDs) because the
9292
// blockchain only stores the hash - the codec is not preserved on-chain.
93-
if chain_proof.root_cid.hash() != event.proof().root().hash() {
93+
let chain_digest = chain_proof.root_cid.hash().digest();
94+
let event_proof_root = event.proof().root();
95+
let proof_digest = event_proof_root.hash().digest();
96+
97+
if chain_digest != proof_digest {
98+
// During clay self-anchor rollout, some anchors were made with the incorrect data.
99+
// anchor-evm left the codec byte (0x20) as a prefix, resulting in the last byte of
100+
// data being discarded. As a fallback, we allow matching on 31 bytes before 2026-02-01
101+
if chain_id.to_string() == "eip155:100"
102+
&& chain_proof.timestamp.as_unix_ts() < 1769904000u64
103+
&& chain_digest.first() == Some(&0x20)
104+
{
105+
warn!(
106+
"falling back to relaxed check for codec-shifted anchor (chain digest={}, proof digest={})",
107+
hex::encode(chain_digest),
108+
hex::encode(proof_digest),
109+
);
110+
111+
if chain_digest.get(1..) == proof_digest.get(..31) {
112+
warn!("relaxed check passed, accepting proof with shifted digest");
113+
return Ok(chain_proof);
114+
}
115+
116+
return Err(eth_rpc::Error::InvalidProof(format!(
117+
"relaxed check failed: shifted digest mismatch (chain digest={}, proof digest={})",
118+
hex::encode(chain_digest),
119+
hex::encode(proof_digest)
120+
)));
121+
}
94122
return Err(eth_rpc::Error::InvalidProof(format!(
95123
"the root hash is not in the transaction (anchor proof root={}, blockchain transaction root={})",
96124
event.proof().root(),
@@ -347,4 +375,109 @@ mod test {
347375
},
348376
}
349377
}
378+
379+
/// Create a Gnosis chain time event for testing the codec-shifted digest fallback
380+
fn time_event_gnosis() -> unvalidated::TimeEvent {
381+
unvalidated::Builder::time()
382+
.with_id(
383+
Cid::from_str("bagcqcerar2aga7747dm6fota3iipogz4q55gkaamcx2weebs6emvtvie2oha")
384+
.unwrap(),
385+
)
386+
.with_tx(
387+
"eip155:100".into(), // Gnosis chain
388+
Cid::from_str("bagjqcgzadp7fstu7fz5tfi474ugsjqx5h6yvevn54w5m4akayhegdsonwciq")
389+
.unwrap(),
390+
"f(bytes32)".into(),
391+
)
392+
.with_root(0, ipld_core::ipld! {[Cid::from_str("bagcqcerae5oqoglzjjgz53enwsttl7mqglp5eoh2llzbbvfktmzxleeiffbq").unwrap(), Ipld::Null, Cid::from_str("bafyreifjkogkhyqvr2gtymsndsfg3wpr7fg4q5r3opmdxoddfj4s2dyuoa").unwrap()]})
393+
.build()
394+
.expect("should be valid time event")
395+
}
396+
397+
/// Creates a CID with a shifted digest simulating the anchor-evm bug:
398+
/// [0x20, original[0..31]] instead of [original[0..32]]
399+
fn create_shifted_digest_cid(original_cid: &Cid) -> Cid {
400+
let original_digest = original_cid.hash().digest();
401+
let mut shifted = [0u8; 32];
402+
shifted[0] = 0x20; // codec byte that was accidentally included
403+
shifted[1..].copy_from_slice(&original_digest[..31]);
404+
405+
let mh = multihash::Multihash::<64>::wrap(0x12, &shifted).expect("valid multihash");
406+
Cid::new_v1(original_cid.codec(), mh)
407+
}
408+
409+
async fn get_mock_gnosis_provider(
410+
input: unvalidated::AnchorProof,
411+
root_cid: Cid,
412+
timestamp: Timestamp,
413+
) -> TimeEventValidator {
414+
let mut mock_provider = MockEthRpcProviderTest::new();
415+
let chain_id = caip2::ChainId::from_str("eip155:100").expect("eip155:100 is a valid chain");
416+
417+
mock_provider
418+
.expect_chain_id()
419+
.once()
420+
.return_const(chain_id.clone());
421+
mock_provider
422+
.expect_get_chain_inclusion_proof()
423+
.once()
424+
.with(predicate::eq(input))
425+
.return_once(move |_| {
426+
Ok(eth_rpc::ChainInclusionProof {
427+
timestamp,
428+
root_cid,
429+
block_hash: "0x0".to_string(),
430+
metadata: ChainProofMetadata {
431+
chain_id,
432+
tx_hash: "0x0".to_string(),
433+
tx_input: "0x0".to_string(),
434+
},
435+
})
436+
});
437+
TimeEventValidator::new_with_providers(vec![Arc::new(mock_provider)])
438+
}
439+
440+
#[test(tokio::test)]
441+
async fn valid_proof_codec_shifted_digest_gnosis() {
442+
let event = time_event_gnosis();
443+
let shifted_root = create_shifted_digest_cid(&event.proof().root());
444+
// Timestamp before 2026-02-01 cutoff (1769904000)
445+
let old_timestamp = Timestamp::from_unix_ts(1700000000);
446+
447+
let verifier =
448+
get_mock_gnosis_provider(event.proof().clone(), shifted_root, old_timestamp.clone())
449+
.await;
450+
451+
match verifier.validate_chain_inclusion(&event).await {
452+
Ok(proof) => {
453+
assert_eq!(proof.timestamp, old_timestamp);
454+
}
455+
Err(e) => panic!("should have passed with relaxed check: {:?}", e),
456+
}
457+
}
458+
459+
#[test(tokio::test)]
460+
async fn invalid_proof_codec_shifted_after_cutoff() {
461+
let event = time_event_gnosis();
462+
let shifted_root = create_shifted_digest_cid(&event.proof().root());
463+
// Timestamp AFTER 2026-02-01 cutoff - should reject
464+
let future_timestamp = Timestamp::from_unix_ts(1769904001);
465+
466+
let verifier =
467+
get_mock_gnosis_provider(event.proof().clone(), shifted_root, future_timestamp).await;
468+
469+
match verifier.validate_chain_inclusion(&event).await {
470+
Ok(v) => panic!("should have failed after cutoff: {:?}", v),
471+
Err(e) => match e {
472+
eth_rpc::Error::InvalidProof(msg) => {
473+
assert!(
474+
msg.contains("the root hash is not in the transaction"),
475+
"{}",
476+
msg
477+
);
478+
}
479+
err => panic!("got wrong error: {:?}", err),
480+
},
481+
}
482+
}
350483
}

0 commit comments

Comments
 (0)