Skip to content
87 changes: 80 additions & 7 deletions anchor-evm/src/evm_transaction_manager.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::time::Duration;

use alloy::{
hex,
network::EthereumWallet,
primitives::{Address, FixedBytes, U256},
providers::{Provider, ProviderBuilder},
Expand All @@ -9,7 +10,9 @@ use alloy::{
};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use ceramic_anchor_service::{DetachedTimeEvent, MerkleNodes, RootTimeEvent, TransactionManager};
use ceramic_anchor_service::{
ChainInclusionData, DetachedTimeEvent, MerkleNodes, RootTimeEvent, TransactionManager,
};
use ceramic_core::{Cid, SerializeExt};
use tokio::time::{interval, sleep};
use tracing::{debug, info, warn};
Expand Down Expand Up @@ -82,6 +85,18 @@ pub struct EvmTransactionManager {
config: EvmConfig,
}

/// Result of submitting and confirming an anchor transaction
struct AnchorResult {
/// The transaction hash (0x-prefixed)
tx_hash: String,
/// The block hash containing the transaction
block_hash: String,
/// The block timestamp (Unix timestamp in seconds)
timestamp: u64,
/// The transaction input data (0x-prefixed function selector + hash)
tx_input: String,
}

impl EvmTransactionManager {
/// Create a new EVM transaction manager
pub async fn new(config: EvmConfig) -> Result<Self> {
Expand Down Expand Up @@ -134,7 +149,7 @@ impl EvmTransactionManager {
}

/// Submit an anchor transaction and wait for confirmation with retry logic
async fn submit_and_wait(&self, root_cid: Cid) -> Result<String> {
async fn submit_and_wait(&self, root_cid: Cid) -> Result<AnchorResult> {
info!(
"Anchoring root CID: {} on chain {}",
root_cid, self.config.chain_id
Expand Down Expand Up @@ -255,7 +270,29 @@ impl EvmTransactionManager {
let gas_used = starting_balance.saturating_sub(ending_balance);
info!("Total gas cost: {} wei", gas_used);
}
return Ok(tx_hash);

// Get block hash from receipt
let block_hash = receipt
.block_hash
.ok_or_else(|| anyhow!("Transaction receipt missing block hash"))?;

// Fetch block to get timestamp (false = don't include full transactions)
let block = provider
.get_block_by_number(block_number.into(), false)
.await?
.ok_or_else(|| anyhow!("Block {} not found", block_number))?;

// Construct tx_input: function selector (0x97ad09eb) + 32-byte hash
let root_hash = Self::cid_to_bytes32(&root_cid)?;
let tx_input =
format!("0x97ad09eb{}", hex::encode(root_hash.as_slice()));

return Ok(AnchorResult {
tx_hash,
block_hash: format!("0x{:x}", block_hash),
timestamp: block.header.timestamp,
tx_input,
});
}
Err(e) => {
warn!("Transaction confirmation failed: {}", e);
Expand Down Expand Up @@ -287,7 +324,7 @@ impl EvmTransactionManager {
|| error_str.contains("replacement transaction underpriced"))
{
for prev_tx in previous_tx_hashes.iter().rev() {
if let Ok(Some(_)) = provider
if let Ok(Some(prev_receipt)) = provider
.get_transaction_receipt(prev_tx.parse().unwrap_or_default())
.await
{
Expand All @@ -297,7 +334,32 @@ impl EvmTransactionManager {
{
info!("Ending wallet balance: {} wei", ending_balance);
}
return Ok(prev_tx.clone());

// Get block info from the previous receipt
let block_hash = prev_receipt.block_hash.ok_or_else(|| {
anyhow!("Previous transaction receipt missing block hash")
})?;
let block_number = prev_receipt.block_number.ok_or_else(|| {
anyhow!("Previous transaction receipt missing block number")
})?;

// Fetch block to get timestamp (false = don't include full transactions)
let block = provider
.get_block_by_number(block_number.into(), false)
.await?
.ok_or_else(|| anyhow!("Block {} not found", block_number))?;

// Construct tx_input from root_cid
let root_hash = Self::cid_to_bytes32(&root_cid)?;
let tx_input =
format!("0x97ad09eb{}", hex::encode(root_hash.as_slice()));

return Ok(AnchorResult {
tx_hash: prev_tx.clone(),
block_hash: format!("0x{:x}", block_hash),
timestamp: block.header.timestamp,
tx_input,
});
}
}
}
Expand Down Expand Up @@ -428,10 +490,11 @@ impl EvmTransactionManager {
impl TransactionManager for EvmTransactionManager {
async fn anchor_root(&self, root: Cid) -> Result<RootTimeEvent> {
// Submit transaction and wait for confirmation
let tx_hash = self.submit_and_wait(root).await?;
let anchor_result = self.submit_and_wait(root).await?;

// Build anchor proof from transaction details
let proof = ProofBuilder::build_proof(self.config.chain_id, tx_hash, root)?;
let proof =
ProofBuilder::build_proof(self.config.chain_id, anchor_result.tx_hash.clone(), root)?;
let proof_cid = proof.to_cid()?;

// Create detached time event
Expand All @@ -441,12 +504,22 @@ impl TransactionManager for EvmTransactionManager {
proof: proof_cid,
};

// Create chain inclusion data for persistence
let chain_inclusion = ChainInclusionData {
chain_id: format!("eip155:{}", self.config.chain_id),
transaction_hash: anchor_result.tx_hash,
transaction_input: anchor_result.tx_input,
block_hash: anchor_result.block_hash,
timestamp: anchor_result.timestamp,
};

// Return root time event with no additional remote Merkle nodes
// (all nodes are local since we built the entire tree)
Ok(RootTimeEvent {
proof,
detached_time_event,
remote_merkle_nodes: MerkleNodes::default(),
chain_inclusion: Some(chain_inclusion),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions anchor-remote/src/cas_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ async fn parse_anchor_response(anchor_response: String) -> Result<CasResponsePar
proof: proof.expect("proof should be present"),
detached_time_event: detached_time_event.expect("detached time event should be present"),
remote_merkle_nodes,
chain_inclusion: None,
})))
}

Expand Down
1 change: 1 addition & 0 deletions anchor-remote/src/test-data/anchor_response.test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Receipt {
"Cid(bafyreigab6skaymtdwkpee4yzkhhglqzl7kbd2wrv24r2uqk3iol44gfga): [Some(Cid(bafyreia776z4jdg5zgycivcpr3q6lcu6llfowkrljkmq3bex2k5hkzat54)), Some(Cid(bagcqcera24qtqvh7yjbm72iqov56tmiubltdfn2rwjgcokkxjbovbih6x24q))]",
"Cid(bafyreigay6frqlwu7dzhf4vch3oxeavkciyogkosbqrttljzgraghl3mo4): [Some(Cid(bafyreigab6skaymtdwkpee4yzkhhglqzl7kbd2wrv24r2uqk3iol44gfga)), Some(Cid(bafyreid6fo2bz67eza23ulv3l6d7uwu4nd5rmsdx3clijnelebkqscdiqa))]",
],
chain_inclusion: None,
}
19 changes: 18 additions & 1 deletion anchor-service/src/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use tracing::info;
use ceramic_core::{EventId, SerializeExt};
use ceramic_event::unvalidated::{AnchorProof, ProofEdge, RawTimeEvent, TimeEvent};

use crate::transaction_manager::ChainInclusionData;

/// AnchorRequest for a Data Event on a Stream
#[derive(Clone, PartialEq, Eq, Serialize)]
pub struct AnchorRequest {
Expand Down Expand Up @@ -101,6 +103,8 @@ pub struct TimeEventBatch {
pub proof: AnchorProof,
/// The Time Events
pub raw_time_events: RawTimeEvents,
/// Chain inclusion data for self-anchored events (None for remote CAS)
pub chain_inclusion: Option<ChainInclusionData>,
}

impl std::fmt::Debug for TimeEventBatch {
Expand All @@ -109,6 +113,7 @@ impl std::fmt::Debug for TimeEventBatch {
.field("merkle_nodes", &self.merkle_nodes)
.field("proof", &self.proof)
.field("raw_time_events", &self.raw_time_events)
.field("chain_inclusion", &self.chain_inclusion)
.finish()
}
}
Expand All @@ -125,12 +130,19 @@ impl TimeEventBatch {
merkle_nodes,
proof,
raw_time_events,
chain_inclusion,
} = self;
let events = raw_time_events
.events
.into_iter()
.map(|(anchor_request, time_event)| {
Self::build_time_event_insertable(&proof, &merkle_nodes, time_event, anchor_request)
Self::build_time_event_insertable(
&proof,
&merkle_nodes,
time_event,
anchor_request,
chain_inclusion.clone(),
)
})
.collect::<Result<Vec<_>>>()?;
Ok(events)
Expand All @@ -142,6 +154,7 @@ impl TimeEventBatch {
merkle_nodes: &MerkleNodes,
time_event: RawTimeEvent,
anchor_request: AnchorRequest,
chain_inclusion: Option<ChainInclusionData>,
) -> Result<TimeEventInsertable> {
let time_event_cid = time_event.to_cid().context(format!(
"could not serialize time event for {} with batch proof {}",
Expand Down Expand Up @@ -180,6 +193,7 @@ impl TimeEventBatch {
))?,
cid: time_event_cid,
event: TimeEvent::new(time_event.clone(), proof.clone(), blocks_in_path),
chain_inclusion,
})
}

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

impl std::fmt::Debug for TimeEventInsertable {
Expand All @@ -231,6 +247,7 @@ impl std::fmt::Debug for TimeEventInsertable {
.field("event_id", &self.event_id.to_string())
.field("cid", &self.cid.to_string())
.field("event", &self.event)
.field("chain_inclusion", &self.chain_inclusion)
.finish()
}
}
2 changes: 2 additions & 0 deletions anchor-service/src/anchor_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,15 @@ impl AnchorService {
proof,
detached_time_event,
mut remote_merkle_nodes,
chain_inclusion,
} = self.tx_manager.anchor_root(root_cid).await?;
let time_events = build_time_events(anchor_requests, &detached_time_event, count)?;
remote_merkle_nodes.extend(local_merkle_nodes);
Ok(TimeEventBatch {
merkle_nodes: remote_merkle_nodes,
proof,
raw_time_events: time_events,
chain_inclusion,
})
}

Expand Down
1 change: 1 addition & 0 deletions anchor-service/src/cas_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ impl TransactionManager for MockCas {
proof,
},
remote_merkle_nodes: Default::default(),
chain_inclusion: None,
})
}
}
Expand Down
4 changes: 3 additions & 1 deletion anchor-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ mod transaction_manager;
pub use anchor::{AnchorRequest, MerkleNode, MerkleNodes, TimeEventBatch, TimeEventInsertable};
pub use anchor_batch::{AnchorService, Store};
pub use cas_mock::{MockAnchorEventService, MockCas};
pub use transaction_manager::{DetachedTimeEvent, RootTimeEvent, TransactionManager};
pub use transaction_manager::{
ChainInclusionData, DetachedTimeEvent, RootTimeEvent, TransactionManager,
};
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,5 @@ TimeEventBatch {
},
),
],
chain_inclusion: None,
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ TimeEventBatch {
},
),
],
chain_inclusion: None,
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,5 @@ TimeEventBatch {
},
),
],
chain_inclusion: None,
}
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,5 @@ TimeEventBatch {
},
),
],
chain_inclusion: None,
}
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,5 @@ TimeEventBatch {
},
),
],
chain_inclusion: None,
}
10 changes: 10 additions & 0 deletions anchor-service/src/test-data/test_anchor_service_run.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B75170171122072D0B0181556FFCE636242C424188FF826014A651DD225D511200E9C85374865",
Expand Down Expand Up @@ -70,6 +71,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B7517017112206FEF93C0B67F1C93B2574F46BFBC7274592E8C2349921722BB426117B6082726",
Expand Down Expand Up @@ -106,6 +108,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B7517017112204AC042CCE1C67A1523D01720FA4B5AAB5E5E3632DC10DC1451C9F19B86901610",
Expand Down Expand Up @@ -142,6 +145,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B7517017112200FDC7F813BADA92906BC21E1C9523E2C08FDB6D370EDD8F8517AF15E50CEDC68",
Expand Down Expand Up @@ -178,6 +182,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B751701711220D93A47DF5839FC52001D506F5D54ABFBBAAA75AE81801CE68017BE8B674D2527",
Expand Down Expand Up @@ -214,6 +219,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B7517017112208FD9C6536FEEFE3026D8838398261AE9EB8F250B16E8195358F5893364D53FCA",
Expand Down Expand Up @@ -250,6 +256,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B751701711220F84574367985116A160937E9DFA2FAF7A02013A001C4030DE78EF71C580B0F41",
Expand Down Expand Up @@ -286,6 +293,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B75170171122031F627FD20CF062DBC6F34FE250D077C8711F1EF4B18F8E4777D0B021C13A067",
Expand Down Expand Up @@ -314,6 +322,7 @@
],
],
},
chain_inclusion: None,
},
TimeEventInsertable {
event_id: "CE010500BA25076D730241E745CC7C072FF729EA683B7517017112205502894B152BB78E4EE8E5C665AE5D92AF339642A10EB481F7FC877CF117F0FE",
Expand Down Expand Up @@ -342,5 +351,6 @@
],
],
},
chain_inclusion: None,
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
},
blocks_in_path: [],
},
chain_inclusion: None,
},
]
Loading
Loading