Skip to content

Commit e999d41

Browse files
committed
fix: self-anchoring never stored inclusion proofs, causing concluder failures
1 parent 931d681 commit e999d41

17 files changed

Lines changed: 169 additions & 10 deletions

anchor-evm/src/evm_transaction_manager.rs

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::time::Duration;
22

33
use alloy::{
4+
hex,
45
network::EthereumWallet,
56
primitives::{Address, FixedBytes, U256},
67
providers::{Provider, ProviderBuilder},
@@ -9,7 +10,9 @@ use alloy::{
910
};
1011
use anyhow::{anyhow, Result};
1112
use async_trait::async_trait;
12-
use ceramic_anchor_service::{DetachedTimeEvent, MerkleNodes, RootTimeEvent, TransactionManager};
13+
use ceramic_anchor_service::{
14+
ChainInclusionData, DetachedTimeEvent, MerkleNodes, RootTimeEvent, TransactionManager,
15+
};
1316
use ceramic_core::{Cid, SerializeExt};
1417
use tokio::time::{interval, sleep};
1518
use tracing::{debug, info, warn};
@@ -82,6 +85,18 @@ pub struct EvmTransactionManager {
8285
config: EvmConfig,
8386
}
8487

88+
/// Result of submitting and confirming an anchor transaction
89+
struct AnchorResult {
90+
/// The transaction hash (0x-prefixed)
91+
tx_hash: String,
92+
/// The block hash containing the transaction
93+
block_hash: String,
94+
/// The block timestamp (Unix timestamp in seconds)
95+
timestamp: u64,
96+
/// The transaction input data (0x-prefixed function selector + hash)
97+
tx_input: String,
98+
}
99+
85100
impl EvmTransactionManager {
86101
/// Create a new EVM transaction manager
87102
pub async fn new(config: EvmConfig) -> Result<Self> {
@@ -134,7 +149,7 @@ impl EvmTransactionManager {
134149
}
135150

136151
/// Submit an anchor transaction and wait for confirmation with retry logic
137-
async fn submit_and_wait(&self, root_cid: Cid) -> Result<String> {
152+
async fn submit_and_wait(&self, root_cid: Cid) -> Result<AnchorResult> {
138153
info!(
139154
"Anchoring root CID: {} on chain {}",
140155
root_cid, self.config.chain_id
@@ -255,7 +270,28 @@ impl EvmTransactionManager {
255270
let gas_used = starting_balance.saturating_sub(ending_balance);
256271
info!("Total gas cost: {} wei", gas_used);
257272
}
258-
return Ok(tx_hash);
273+
274+
// Get block hash from receipt
275+
let block_hash = receipt
276+
.block_hash
277+
.ok_or_else(|| anyhow!("Transaction receipt missing block hash"))?;
278+
279+
// Fetch block to get timestamp (false = don't include full transactions)
280+
let block = provider
281+
.get_block_by_number(block_number.into(), false)
282+
.await?
283+
.ok_or_else(|| anyhow!("Block {} not found", block_number))?;
284+
285+
// Construct tx_input: function selector (0x97ad09eb) + 32-byte hash
286+
let root_hash = Self::cid_to_bytes32(&root_cid)?;
287+
let tx_input = format!("0x97ad09eb{}", hex::encode(root_hash.as_slice()));
288+
289+
return Ok(AnchorResult {
290+
tx_hash,
291+
block_hash: format!("0x{:x}", block_hash),
292+
timestamp: block.header.timestamp,
293+
tx_input,
294+
});
259295
}
260296
Err(e) => {
261297
warn!("Transaction confirmation failed: {}", e);
@@ -287,7 +323,7 @@ impl EvmTransactionManager {
287323
|| error_str.contains("replacement transaction underpriced"))
288324
{
289325
for prev_tx in previous_tx_hashes.iter().rev() {
290-
if let Ok(Some(_)) = provider
326+
if let Ok(Some(prev_receipt)) = provider
291327
.get_transaction_receipt(prev_tx.parse().unwrap_or_default())
292328
.await
293329
{
@@ -297,7 +333,32 @@ impl EvmTransactionManager {
297333
{
298334
info!("Ending wallet balance: {} wei", ending_balance);
299335
}
300-
return Ok(prev_tx.clone());
336+
337+
// Get block info from the previous receipt
338+
let block_hash = prev_receipt.block_hash.ok_or_else(|| {
339+
anyhow!("Previous transaction receipt missing block hash")
340+
})?;
341+
let block_number = prev_receipt.block_number.ok_or_else(|| {
342+
anyhow!("Previous transaction receipt missing block number")
343+
})?;
344+
345+
// Fetch block to get timestamp (false = don't include full transactions)
346+
let block = provider
347+
.get_block_by_number(block_number.into(), false)
348+
.await?
349+
.ok_or_else(|| anyhow!("Block {} not found", block_number))?;
350+
351+
// Construct tx_input from root_cid
352+
let root_hash = Self::cid_to_bytes32(&root_cid)?;
353+
let tx_input =
354+
format!("0x97ad09eb{}", hex::encode(root_hash.as_slice()));
355+
356+
return Ok(AnchorResult {
357+
tx_hash: prev_tx.clone(),
358+
block_hash: format!("0x{:x}", block_hash),
359+
timestamp: block.header.timestamp,
360+
tx_input,
361+
});
301362
}
302363
}
303364
}
@@ -428,10 +489,11 @@ impl EvmTransactionManager {
428489
impl TransactionManager for EvmTransactionManager {
429490
async fn anchor_root(&self, root: Cid) -> Result<RootTimeEvent> {
430491
// Submit transaction and wait for confirmation
431-
let tx_hash = self.submit_and_wait(root).await?;
492+
let anchor_result = self.submit_and_wait(root).await?;
432493

433494
// Build anchor proof from transaction details
434-
let proof = ProofBuilder::build_proof(self.config.chain_id, tx_hash, root)?;
495+
let proof =
496+
ProofBuilder::build_proof(self.config.chain_id, anchor_result.tx_hash.clone(), root)?;
435497
let proof_cid = proof.to_cid()?;
436498

437499
// Create detached time event
@@ -441,12 +503,22 @@ impl TransactionManager for EvmTransactionManager {
441503
proof: proof_cid,
442504
};
443505

506+
// Create chain inclusion data for persistence
507+
let chain_inclusion = ChainInclusionData {
508+
chain_id: format!("eip155:{}", self.config.chain_id),
509+
transaction_hash: anchor_result.tx_hash,
510+
transaction_input: anchor_result.tx_input,
511+
block_hash: anchor_result.block_hash,
512+
timestamp: anchor_result.timestamp,
513+
};
514+
444515
// Return root time event with no additional remote Merkle nodes
445516
// (all nodes are local since we built the entire tree)
446517
Ok(RootTimeEvent {
447518
proof,
448519
detached_time_event,
449520
remote_merkle_nodes: MerkleNodes::default(),
521+
chain_inclusion: Some(chain_inclusion),
450522
})
451523
}
452524
}

anchor-remote/src/cas_remote.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ async fn parse_anchor_response(anchor_response: String) -> Result<CasResponsePar
220220
proof: proof.expect("proof should be present"),
221221
detached_time_event: detached_time_event.expect("detached time event should be present"),
222222
remote_merkle_nodes,
223+
chain_inclusion: None,
223224
})))
224225
}
225226

anchor-remote/src/test-data/anchor_response.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ Receipt {
1919
"Cid(bafyreigab6skaymtdwkpee4yzkhhglqzl7kbd2wrv24r2uqk3iol44gfga): [Some(Cid(bafyreia776z4jdg5zgycivcpr3q6lcu6llfowkrljkmq3bex2k5hkzat54)), Some(Cid(bagcqcera24qtqvh7yjbm72iqov56tmiubltdfn2rwjgcokkxjbovbih6x24q))]",
2020
"Cid(bafyreigay6frqlwu7dzhf4vch3oxeavkciyogkosbqrttljzgraghl3mo4): [Some(Cid(bafyreigab6skaymtdwkpee4yzkhhglqzl7kbd2wrv24r2uqk3iol44gfga)), Some(Cid(bafyreid6fo2bz67eza23ulv3l6d7uwu4nd5rmsdx3clijnelebkqscdiqa))]",
2121
],
22+
chain_inclusion: None,
2223
}

anchor-service/src/anchor.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use tracing::info;
99
use ceramic_core::{EventId, SerializeExt};
1010
use ceramic_event::unvalidated::{AnchorProof, ProofEdge, RawTimeEvent, TimeEvent};
1111

12+
use crate::transaction_manager::ChainInclusionData;
13+
1214
/// AnchorRequest for a Data Event on a Stream
1315
#[derive(Clone, PartialEq, Eq, Serialize)]
1416
pub struct AnchorRequest {
@@ -101,6 +103,8 @@ pub struct TimeEventBatch {
101103
pub proof: AnchorProof,
102104
/// The Time Events
103105
pub raw_time_events: RawTimeEvents,
106+
/// Chain inclusion data for self-anchored events (None for remote CAS)
107+
pub chain_inclusion: Option<ChainInclusionData>,
104108
}
105109

106110
impl std::fmt::Debug for TimeEventBatch {
@@ -109,6 +113,7 @@ impl std::fmt::Debug for TimeEventBatch {
109113
.field("merkle_nodes", &self.merkle_nodes)
110114
.field("proof", &self.proof)
111115
.field("raw_time_events", &self.raw_time_events)
116+
.field("chain_inclusion", &self.chain_inclusion)
112117
.finish()
113118
}
114119
}
@@ -125,12 +130,19 @@ impl TimeEventBatch {
125130
merkle_nodes,
126131
proof,
127132
raw_time_events,
133+
chain_inclusion,
128134
} = self;
129135
let events = raw_time_events
130136
.events
131137
.into_iter()
132138
.map(|(anchor_request, time_event)| {
133-
Self::build_time_event_insertable(&proof, &merkle_nodes, time_event, anchor_request)
139+
Self::build_time_event_insertable(
140+
&proof,
141+
&merkle_nodes,
142+
time_event,
143+
anchor_request,
144+
chain_inclusion.clone(),
145+
)
134146
})
135147
.collect::<Result<Vec<_>>>()?;
136148
Ok(events)
@@ -142,6 +154,7 @@ impl TimeEventBatch {
142154
merkle_nodes: &MerkleNodes,
143155
time_event: RawTimeEvent,
144156
anchor_request: AnchorRequest,
157+
chain_inclusion: Option<ChainInclusionData>,
145158
) -> Result<TimeEventInsertable> {
146159
let time_event_cid = time_event.to_cid().context(format!(
147160
"could not serialize time event for {} with batch proof {}",
@@ -180,6 +193,7 @@ impl TimeEventBatch {
180193
))?,
181194
cid: time_event_cid,
182195
event: TimeEvent::new(time_event.clone(), proof.clone(), blocks_in_path),
196+
chain_inclusion,
183197
})
184198
}
185199

@@ -223,6 +237,8 @@ pub struct TimeEventInsertable {
223237
pub cid: Cid,
224238
/// The parsed structure containing the Time Event
225239
pub event: TimeEvent,
240+
/// Chain inclusion data for self-anchored events (None for remote CAS)
241+
pub chain_inclusion: Option<ChainInclusionData>,
226242
}
227243

228244
impl std::fmt::Debug for TimeEventInsertable {
@@ -231,6 +247,7 @@ impl std::fmt::Debug for TimeEventInsertable {
231247
.field("event_id", &self.event_id.to_string())
232248
.field("cid", &self.cid.to_string())
233249
.field("event", &self.event)
250+
.field("chain_inclusion", &self.chain_inclusion)
234251
.finish()
235252
}
236253
}

anchor-service/src/anchor_batch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,15 @@ impl AnchorService {
192192
proof,
193193
detached_time_event,
194194
mut remote_merkle_nodes,
195+
chain_inclusion,
195196
} = self.tx_manager.anchor_root(root_cid).await?;
196197
let time_events = build_time_events(anchor_requests, &detached_time_event, count)?;
197198
remote_merkle_nodes.extend(local_merkle_nodes);
198199
Ok(TimeEventBatch {
199200
merkle_nodes: remote_merkle_nodes,
200201
proof,
201202
raw_time_events: time_events,
203+
chain_inclusion,
202204
})
203205
}
204206

anchor-service/src/cas_mock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ impl TransactionManager for MockCas {
3232
proof,
3333
},
3434
remote_merkle_nodes: Default::default(),
35+
chain_inclusion: None,
3536
})
3637
}
3738
}

anchor-service/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ mod transaction_manager;
1212
pub use anchor::{AnchorRequest, MerkleNode, MerkleNodes, TimeEventBatch, TimeEventInsertable};
1313
pub use anchor_batch::{AnchorService, Store};
1414
pub use cas_mock::{MockAnchorEventService, MockCas};
15-
pub use transaction_manager::{DetachedTimeEvent, RootTimeEvent, TransactionManager};
15+
pub use transaction_manager::{ChainInclusionData, DetachedTimeEvent, RootTimeEvent, TransactionManager};

anchor-service/src/test-data/test_anchor_batch_with_10_requests.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,5 @@ TimeEventBatch {
185185
},
186186
),
187187
],
188+
chain_inclusion: None,
188189
}

anchor-service/src/test-data/test_anchor_batch_with_1_request.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ TimeEventBatch {
2222
},
2323
),
2424
],
25+
chain_inclusion: None,
2526
}

anchor-service/src/test-data/test_anchor_batch_with_less_than_pow2_requests.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,5 @@ TimeEventBatch {
275275
},
276276
),
277277
],
278+
chain_inclusion: None,
278279
}

0 commit comments

Comments
 (0)