Skip to content

Commit e88e115

Browse files
committed
fix: tests for self-anchoring and self-proof storage
1 parent 2f46a1e commit e88e115

6 files changed

Lines changed: 969 additions & 1 deletion

File tree

event-svc/src/blockchain/mod.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,98 @@ pub mod eth_rpc;
1010
pub(crate) fn tx_hash_try_from_cid(cid: Cid) -> anyhow::Result<TxHash> {
1111
Ok(TxHash::from_str(&hex::encode(cid.hash().digest()))?)
1212
}
13+
14+
#[cfg(test)]
15+
mod tests {
16+
use super::*;
17+
use multihash_codetable::{Code, MultihashDigest};
18+
19+
/// Ethereum transaction codec for IPLD (from multicodec table)
20+
/// This matches the constant in anchor-evm/src/proof_builder.rs
21+
const ETH_TX_CODEC: u64 = 0x93;
22+
23+
/// Simulate what ProofBuilder::tx_hash_to_cid does
24+
fn simulate_proof_builder_tx_hash_to_cid(tx_hash: &str) -> Cid {
25+
let hex_str = tx_hash.strip_prefix("0x").unwrap_or(tx_hash);
26+
let tx_bytes = hex::decode(hex_str).unwrap();
27+
let multihash = Code::Keccak256.wrap(&tx_bytes).unwrap();
28+
Cid::new_v1(ETH_TX_CODEC, multihash)
29+
}
30+
31+
#[test]
32+
fn test_tx_hash_roundtrip_self_anchor_to_lookup() {
33+
// critical invariant: what self-anchoring stores must match what lookup queries
34+
let original_tx_hash = "0xa1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
35+
36+
// Step 1: ProofBuilder creates a CID from the tx_hash (simulated here)
37+
let tx_cid = simulate_proof_builder_tx_hash_to_cid(original_tx_hash);
38+
39+
// Step 2: Validation converts CID back to tx_hash for lookup
40+
let lookup_tx_hash = tx_hash_try_from_cid(tx_cid).unwrap().to_string();
41+
42+
// Step 3: Verify they match (both should be 0x-prefixed lowercase hex)
43+
assert_eq!(
44+
original_tx_hash.to_lowercase(),
45+
lookup_tx_hash.to_lowercase(),
46+
"Self-anchored tx_hash must match lookup tx_hash for chain proof discovery to work"
47+
);
48+
}
49+
50+
#[test]
51+
fn test_tx_hash_roundtrip_without_0x_prefix() {
52+
// Test that round-trip works even without 0x prefix in original
53+
let original_tx_hash = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
54+
55+
let tx_cid = simulate_proof_builder_tx_hash_to_cid(original_tx_hash);
56+
let lookup_tx_hash = tx_hash_try_from_cid(tx_cid).unwrap().to_string();
57+
58+
// TxHash::to_string() always adds 0x prefix
59+
assert_eq!(
60+
format!("0x{}", original_tx_hash.to_lowercase()),
61+
lookup_tx_hash.to_lowercase()
62+
);
63+
}
64+
65+
#[test]
66+
fn test_tx_hash_format_matches_alloy_txhash() {
67+
// Verify that our round-trip produces the same format as alloy's TxHash::to_string()
68+
let tx_hash_hex = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
69+
let tx_cid = simulate_proof_builder_tx_hash_to_cid(tx_hash_hex);
70+
71+
let recovered = tx_hash_try_from_cid(tx_cid).unwrap();
72+
73+
// Create TxHash directly from the same hex
74+
let direct = TxHash::from_str(tx_hash_hex).unwrap();
75+
76+
assert_eq!(recovered, direct);
77+
assert_eq!(recovered.to_string(), direct.to_string());
78+
}
79+
80+
#[test]
81+
fn test_tx_hash_roundtrip_mixed_case() {
82+
// Test that mixed-case hex input produces correct round-trip.
83+
// Ethereum tx hashes are case-insensitive, but our storage uses lowercase.
84+
// This verifies the normalization works correctly.
85+
let mixed_case_tx_hash =
86+
"0xAbCdEf1234567890AbCdEf1234567890AbCdEf1234567890AbCdEf1234567890";
87+
88+
// Step 1: ProofBuilder creates a CID (hex decoding is case-insensitive)
89+
let tx_cid = simulate_proof_builder_tx_hash_to_cid(mixed_case_tx_hash);
90+
91+
// Step 2: Validation converts CID back to tx_hash for lookup
92+
let lookup_tx_hash = tx_hash_try_from_cid(tx_cid).unwrap().to_string();
93+
94+
// Step 3: The lookup should produce lowercase (TxHash::to_string() uses lowercase)
95+
assert_eq!(
96+
lookup_tx_hash, "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
97+
"Mixed-case input should normalize to lowercase for lookup"
98+
);
99+
100+
// Step 4: Verify case-insensitive equality with original
101+
assert_eq!(
102+
mixed_case_tx_hash.to_lowercase(),
103+
lookup_tx_hash,
104+
"Normalized tx_hash must match original (case-insensitive)"
105+
);
106+
}
107+
}

event-svc/src/event/service.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ impl EventService {
545545

546546
/// Get the chain proof for a given event from the database.
547547
/// All proofs should have been validated and stored during the event validation phase (v0.55.0+).
548-
async fn discover_chain_proof(
548+
pub(crate) async fn discover_chain_proof(
549549
&self,
550550
event: &ceramic_event::unvalidated::TimeEvent,
551551
) -> std::result::Result<ChainProof, crate::eth_rpc::Error> {
@@ -556,6 +556,9 @@ impl EventService {
556556
.await
557557
.map_err(|e| crate::eth_rpc::Error::Application(e.into()))?
558558
.ok_or_else(|| {
559+
// Note: Using InvalidProof here rather than TxNotFound because:
560+
// - TxNotFound is for "transaction not found on blockchain" (RPC-level)
561+
// - This is "proof not in local database" (local storage issue)
559562
crate::eth_rpc::Error::InvalidProof(format!(
560563
"Chain proof for tx {} not found in database.",
561564
tx_hash

event-svc/src/event/store.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,13 @@ impl ceramic_anchor_service::Store for EventService {
267267
// Persist chain inclusion proof FIRST for self-anchored events.
268268
// This aligns with the external event flow (service.rs persists proofs before events)
269269
// and ensures events are never orphaned without their chain proofs.
270+
//
271+
// NOTE: These operations are NOT wrapped in a single transaction. Failure scenarios:
272+
// - If proof persistence fails: early return, no events inserted (safe)
273+
// - If proof succeeds but event insertion fails: orphaned proof remains in DB
274+
// This is acceptable because:
275+
// 1. Orphaned proofs are harmless (just extra data, no foreign key constraints)
276+
// 2. The proof can be used if the events are retried later
270277
if let Some(chain_data) = chain_inclusion {
271278
let proof: ChainProof = chain_data.into();
272279
self.event_access

event-svc/src/store/sql/entities/chain_proof.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,70 @@ impl From<ceramic_anchor_service::ChainInclusionData> for ChainProof {
4646
}
4747
}
4848
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use super::*;
53+
use ceramic_anchor_service::ChainInclusionData;
54+
55+
#[test]
56+
fn test_chain_proof_from_chain_inclusion_data() {
57+
let data = ChainInclusionData {
58+
chain_id: "eip155:100".to_string(),
59+
transaction_hash: "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1"
60+
.to_string(),
61+
transaction_input: "0x97ad09eb1234567890abcdef1234567890abcdef1234567890abcdef12345678"
62+
.to_string(),
63+
block_hash: "0xdef456abc123def456abc123def456abc123def456abc123def456abc123def4"
64+
.to_string(),
65+
timestamp: 1704067200,
66+
};
67+
68+
let proof: ChainProof = data.into();
69+
70+
assert_eq!(proof.chain_id, "eip155:100");
71+
assert_eq!(
72+
proof.transaction_hash,
73+
"0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1"
74+
);
75+
assert_eq!(
76+
proof.transaction_input,
77+
"0x97ad09eb1234567890abcdef1234567890abcdef1234567890abcdef12345678"
78+
);
79+
assert_eq!(
80+
proof.block_hash,
81+
"0xdef456abc123def456abc123def456abc123def456abc123def456abc123def4"
82+
);
83+
assert_eq!(proof.timestamp, 1704067200);
84+
}
85+
86+
#[test]
87+
fn test_chain_proof_from_chain_inclusion_data_preserves_chain_id_format() {
88+
for chain_id in ["eip155:1", "eip155:100", "eip155:137", "eip155:42161"] {
89+
let data = ChainInclusionData {
90+
chain_id: chain_id.to_string(),
91+
transaction_hash: "0x".to_string() + &"a".repeat(64),
92+
transaction_input: "0x".to_string() + &"b".repeat(72),
93+
block_hash: "0x".to_string() + &"c".repeat(64),
94+
timestamp: 1704067200,
95+
};
96+
97+
let proof: ChainProof = data.into();
98+
assert_eq!(proof.chain_id, chain_id);
99+
}
100+
}
101+
102+
#[test]
103+
fn test_chain_proof_timestamp_zero() {
104+
let data = ChainInclusionData {
105+
chain_id: "eip155:1".to_string(),
106+
transaction_hash: "0x".to_string() + &"a".repeat(64),
107+
transaction_input: "0x".to_string() + &"b".repeat(72),
108+
block_hash: "0x".to_string() + &"c".repeat(64),
109+
timestamp: 0,
110+
};
111+
112+
let proof: ChainProof = data.into();
113+
assert_eq!(proof.timestamp, 0);
114+
}
115+
}

event-svc/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod event;
22
mod migration;
33
mod ordering;
4+
mod self_anchor;
45

56
use std::{str::FromStr, sync::Arc};
67

0 commit comments

Comments
 (0)