Skip to content

Commit 5cbbbb0

Browse files
committed
feat: store chain timestamp proof data in db and read in pipeline for conclusion events
1 parent fdd2e05 commit 5cbbbb0

24 files changed

Lines changed: 461 additions & 80 deletions

File tree

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ Using the makefile is not necessary during your development cycle, feel free to
1515

1616
However running `make` before publishing a PR will provide a good signal if you PR will pass CI.
1717

18+
### Migrations
19+
20+
If you need to add to the sqlite database schema, you will need to add a migration using the sqlx CLI.
21+
22+
```sh
23+
cargo install sqlx-cli
24+
# use the name of the migration and the source directory
25+
sqlx migrate add -r "chain_proof" --source ./migrations/sqlite
26+
```
27+
28+
After the up and down files are generated, write the apply/revert SQL in the up/down files. This will be applied automatically at startup.
29+
1830
### Testing Specific Changes
1931

2032
The above `make` targets test changes as a whole.

event-svc/src/blockchain/eth_rpc/http.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use std::{
55
sync::{Arc, Mutex},
66
};
77

8-
use crate::eth_rpc::{ChainInclusion, ChainInclusionProof, Error};
8+
use crate::eth_rpc::{
9+
types::ChainProofMetadata, ChainInclusion, ChainInclusionProof, Error, Timestamp,
10+
};
911
use alloy::{
1012
hex,
1113
primitives::{BlockHash, TxHash},
@@ -194,11 +196,6 @@ fn get_root_cid_from_input(input: &str, tx_type: EthProofType) -> anyhow::Result
194196
}
195197
}
196198

197-
/// Get the expected transaction hash for a given root CID (this is v1 proof type)
198-
fn expected_tx_hash(cid: Cid) -> anyhow::Result<TxHash> {
199-
Ok(TxHash::from_str(&hex::encode(cid.hash().digest()))?)
200-
}
201-
202199
#[async_trait::async_trait]
203200
impl ChainInclusion for HttpEthRpc {
204201
fn chain_id(&self) -> &caip2::ChainId {
@@ -208,7 +205,7 @@ impl ChainInclusion for HttpEthRpc {
208205
/// Get the block chain transaction if it exists with the block timestamp information
209206
async fn get_chain_inclusion_proof(&self, input: &AnchorProof) -> Result<ChainInclusionProof> {
210207
// transaction to blockHash, blockNumber, input
211-
let tx_hash = expected_tx_hash(input.tx_hash())
208+
let tx_hash = crate::blockchain::tx_hash_try_from_cid(input.tx_hash())
212209
.map_err(|e| Error::InvalidArgument(format!("invalid transaction hash: {}", e)))?;
213210
let tx_hash_res = match self.eth_transaction_by_hash(tx_hash).await? {
214211
Some(tx) => tx,
@@ -239,7 +236,8 @@ impl ChainInclusion for HttpEthRpc {
239236
trace!(?blk_hash_res, "blockByHash response");
240237
let tx_type = EthProofType::from_str(input.tx_type())
241238
.map_err(|e| Error::InvalidProof(e.to_string()))?;
242-
let root_cid = get_root_cid_from_input(&tx_hash_res.input.to_string(), tx_type)
239+
let tx_input = tx_hash_res.input.to_string();
240+
let root_cid = get_root_cid_from_input(&tx_input, tx_type)
243241
.map_err(|e| Error::InvalidProof(e.to_string()))?;
244242

245243
if let Some(threshold) = BLOCK_THRESHHOLDS.get(self.chain_id()) {
@@ -252,8 +250,14 @@ impl ChainInclusion for HttpEthRpc {
252250
}
253251

254252
Ok(ChainInclusionProof {
255-
timestamp: blk_hash_res.header.timestamp,
253+
timestamp: Timestamp::from_unix_ts(blk_hash_res.header.timestamp),
254+
block_hash: block_hash.to_string(),
256255
root_cid,
256+
meta_data: ChainProofMetadata {
257+
chain_id: self.chain_id.clone(),
258+
tx_hash: tx_hash.to_string(),
259+
tx_input,
260+
},
257261
})
258262
} else {
259263
Err(Error::TxNotMined {

event-svc/src/blockchain/eth_rpc/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ mod http;
22
mod types;
33

44
pub use http::HttpEthRpc;
5-
pub use types::{BlockHash, ChainInclusion, ChainInclusionProof, Error, EthProofType, TxHash};
5+
pub use types::{
6+
BlockHash, ChainInclusion, ChainInclusionProof, ChainProofMetadata, Error, EthProofType,
7+
Timestamp, TxHash,
8+
};

event-svc/src/blockchain/eth_rpc/types.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,44 @@ impl FromStr for EthProofType {
9292
}
9393
}
9494

95+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96+
/// A timestamp that is able to provide seconds since the unix epoch
97+
pub struct Timestamp(u64);
98+
99+
impl Timestamp {
100+
/// Create a timestamp from a unix epoch timestamp
101+
pub const fn from_unix_ts(ts: u64) -> Self {
102+
Self(ts)
103+
}
104+
105+
/// A unix epoch timestamp
106+
pub fn as_unix_ts(&self) -> u64 {
107+
self.0
108+
}
109+
}
110+
95111
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
96112
/// A proof of time derived from state on the blockchain
97113
pub struct ChainInclusionProof {
98114
/// The timestamp the proof was recorded
99-
pub timestamp: u64,
115+
pub timestamp: Timestamp,
116+
/// The block hash in hex form with '0x' prefix
117+
pub block_hash: String,
100118
/// The root CID of the proof
101119
pub root_cid: Cid,
120+
/// The metadata about the proof and where it's stored
121+
pub meta_data: ChainProofMetadata,
122+
}
123+
124+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
125+
/// Metadata about the proof and where it's stored
126+
pub struct ChainProofMetadata {
127+
/// Chain ID of the proof
128+
pub chain_id: ssi::caip2::ChainId,
129+
/// The transaction hash in hex form with '0x' prefix
130+
pub tx_hash: String,
131+
/// The transaction input in hex form with '0x' prefix
132+
pub tx_input: String,
102133
}
103134

104135
#[async_trait::async_trait]

event-svc/src/blockchain/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
use std::str::FromStr as _;
2+
3+
use alloy::primitives::TxHash;
4+
use ceramic_core::Cid;
5+
16
/// The ethereum RPC provider module
27
pub mod eth_rpc;
8+
9+
/// Get the expected transaction hash for a given root CID (this is v1 proof type)
10+
pub(crate) fn tx_hash_try_from_cid(cid: Cid) -> anyhow::Result<TxHash> {
11+
Ok(TxHash::from_str(&hex::encode(cid.hash().digest()))?)
12+
}

event-svc/src/event/service.rs

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use super::{
1212
};
1313
use async_trait::async_trait;
1414
use ceramic_core::{EventId, Network, NodeId, SerializeExt};
15-
use ceramic_pipeline::{ConclusionData, ConclusionEvent, ConclusionInit, ConclusionTime};
15+
use ceramic_pipeline::{concluder::TimeProof, ConclusionData, ConclusionEvent, ConclusionInit, ConclusionTime};
1616
use ceramic_sql::sqlite::SqlitePool;
1717
use cid::Cid;
1818
use futures::stream::BoxStream;
@@ -21,8 +21,8 @@ use itertools::Itertools;
2121
use recon::ReconItem;
2222
use tracing::{trace, warn};
2323

24-
use crate::event::validator::ChainInclusionProvider;
25-
use crate::store::{EventAccess, EventInsertable, EventRowDelivered};
24+
use crate::store::{ChainProof, EventAccess, EventInsertable, EventRowDelivered};
25+
use crate::{blockchain::tx_hash_try_from_cid, event::validator::ChainInclusionProvider};
2626
use crate::{Error, Result};
2727

2828
/// How many events to select at once to see if they've become deliverable when we have downtime
@@ -269,6 +269,7 @@ impl EventService {
269269
valid,
270270
unvalidated,
271271
invalid,
272+
proofs,
272273
} = self
273274
.event_validator
274275
.validate_events(validation_requirement, to_validate)
@@ -279,6 +280,7 @@ impl EventService {
279280
valid,
280281
unvalidated,
281282
invalid: invalid_events,
283+
proofs,
282284
})
283285
}
284286

@@ -293,6 +295,7 @@ impl EventService {
293295
valid,
294296
unvalidated,
295297
mut invalid,
298+
proofs,
296299
} = self.validate_events(items, validation_req.as_ref()).await?;
297300

298301
let to_insert: Vec<EventInsertable> = valid
@@ -307,6 +310,14 @@ impl EventService {
307310
self.track_pending(unvalidated);
308311
}
309312

313+
// Someday, we may want to have the validation/proof inclusion logic have knowledge of the database and persist/read
314+
// from it directly, rather than only keeping proofs in memory + RPC calls. But for now, it's simpler to persist everything once here
315+
// and then the pipeline is able to read from this table and use the timestamps for conclusion events etc.
316+
let proofs = proofs.into_iter().map(|p| p.into()).collect::<Vec<_>>();
317+
self.event_access
318+
.persist_chain_inclusion_proofs(&proofs)
319+
.await?;
320+
310321
let (new, existed) = self
311322
.persist_events(to_insert, deliverable_req, &mut invalid)
312323
.await?;
@@ -391,16 +402,30 @@ impl EventService {
391402

392403
match event {
393404
ceramic_event::unvalidated::Event::Time(time_event) => {
405+
let proof = match self.discover_chain_proof(&time_event).await {
406+
Ok(proof) => Some(proof),
407+
Err(error) => {
408+
tracing::warn!(
409+
?event_cid,
410+
?error,
411+
"Failed to discover chain proof for time event"
412+
);
413+
None
414+
}
415+
};
416+
394417
Ok(ConclusionEvent::Time(ConclusionTime {
395418
event_cid,
396419
init,
397420
previous: vec![*time_event.prev()],
398421
order: delivered as u64,
399-
before: todo!(),
400-
chain_id: todo!(),
401-
tx_hash: todo!(),
402-
tx_type: todo!(),
403-
root: todo!(),
422+
time_proof: proof.map(|p| TimeProof {
423+
before: p
424+
.timestamp
425+
.try_into()
426+
.expect("conclusion timestamp overflow"),
427+
chain_id: p.chain_id,
428+
}),
404429
}))
405430
}
406431
ceramic_event::unvalidated::Event::Signed(signed_event) => {
@@ -523,6 +548,40 @@ impl EventService {
523548
Ok(())
524549
}
525550
}
551+
552+
/// This is a helper function for migrations to get the chain proof for a given event from the database,
553+
/// or to validate and store it if it doesn't exist.
554+
async fn discover_chain_proof(
555+
&self,
556+
event: &ceramic_event::unvalidated::TimeEvent,
557+
) -> std::result::Result<ChainProof, crate::eth_rpc::Error> {
558+
let tx_hash = event.proof().tx_hash();
559+
let tx_hash = tx_hash_try_from_cid(tx_hash).unwrap().to_string();
560+
let proof = self
561+
.event_access
562+
.get_chain_proof(event.proof().chain_id(), &tx_hash)
563+
.await
564+
.map_err(|e| crate::eth_rpc::Error::Application(e.into()))?;
565+
566+
if let Some(proof) = proof {
567+
return Ok(proof);
568+
}
569+
570+
// try using the RPC provider and store the proof afterward
571+
let proof = self
572+
.event_validator
573+
.time_event_verifier()
574+
.validate_chain_inclusion(event)
575+
.await?;
576+
577+
let proof = ChainProof::from(proof);
578+
self.event_access
579+
.persist_chain_inclusion_proofs(&[proof.clone()])
580+
.await
581+
.map_err(|e| crate::eth_rpc::Error::Application(e.into()))?;
582+
583+
Ok(proof)
584+
}
526585
}
527586

528587
// Small wrapper container around the data field to hold other mutable metadata for the
@@ -579,6 +638,12 @@ pub enum ValidationError {
579638
key: EventId,
580639
reason: String,
581640
},
641+
/// 'Soft error' -> should not kill recon conversation but should not be persisted
642+
/// A time event could not be validated because no RPC provider was available
643+
SoftError {
644+
key: EventId,
645+
reason: String,
646+
},
582647
}
583648

584649
#[derive(Debug, PartialEq, Eq, Default)]

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ use ipld_core::ipld::Ipld;
66
use recon::ReconItem;
77
use tokio::try_join;
88

9-
use crate::blockchain::eth_rpc;
10-
use crate::event::validator::ChainInclusionProvider;
11-
use crate::store::EventAccess;
129
use crate::{
10+
blockchain::eth_rpc,
11+
eth_rpc::ChainInclusionProof,
1312
event::{
1413
service::{ValidationError, ValidationRequirement},
1514
validator::{
1615
grouped::{GroupedEvents, SignedValidationBatch, TimeValidationBatch},
1716
signed::SignedEventValidator,
1817
time::TimeEventValidator,
18+
ChainInclusionProvider,
1919
},
2020
},
21-
store::EventInsertable,
21+
store::{EventAccess, EventInsertable},
2222
Result,
2323
};
2424

@@ -31,6 +31,8 @@ pub struct ValidatedEvents {
3131
pub unvalidated: Vec<UnvalidatedEvent>,
3232
/// Events that failed validation
3333
pub invalid: Vec<ValidationError>,
34+
/// The proofs for the validated time events that can be stored for future time stamping
35+
pub proofs: Vec<ChainInclusionProof>,
3436
}
3537

3638
#[derive(Debug)]
@@ -107,6 +109,7 @@ impl ValidatedEvents {
107109
valid: Vec::with_capacity(valid),
108110
unvalidated: Vec::with_capacity(valid / 4),
109111
invalid: Vec::new(),
112+
proofs: Vec::new(),
110113
}
111114
}
112115

@@ -141,6 +144,11 @@ impl EventValidator {
141144
})
142145
}
143146

147+
/// Get the time event verifier
148+
pub(crate) fn time_event_verifier(&self) -> &TimeEventValidator {
149+
&self.time_event_verifier
150+
}
151+
144152
/// Validates the events with the given validation requirement
145153
/// If the [`ValidationRequirement`] is None, it just returns every event as valid
146154
pub(crate) async fn validate_events(
@@ -159,6 +167,7 @@ impl EventValidator {
159167
.collect(),
160168
unvalidated: Vec::new(),
161169
invalid: Vec::new(),
170+
proofs: Vec::new(),
162171
});
163172
};
164173

@@ -211,8 +220,8 @@ impl EventValidator {
211220
.validate_chain_inclusion(time_event.as_time())
212221
.await
213222
{
214-
Ok(_t) => {
215-
// TODO(AES-345): Someday, we will use `t.as_unix_ts()` and care about the actual timestamp, but for now we just consider it valid
223+
Ok(t) => {
224+
validated_events.proofs.push(t);
216225
validated_events.valid.push(time_event.into());
217226
}
218227
Err(err) => {

0 commit comments

Comments
 (0)