Skip to content

Commit 818edfb

Browse files
committed
feat: add proofs service skeleton
1 parent 0763b89 commit 818edfb

File tree

12 files changed

+1242
-0
lines changed

12 files changed

+1242
-0
lines changed

Cargo.lock

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ members = [
3535
"fendermint/testing/*-test",
3636
"fendermint/tracing",
3737
"fendermint/vm/*",
38+
"fendermint/vm/topdown/proof-service",
3839
"fendermint/actors",
3940
"fendermint/actors-custom-car",
4041
"fendermint/actors-builtin-car",
@@ -184,6 +185,8 @@ tracing-appender = "0.2.3"
184185
text-tables = "0.3.1"
185186
url = { version = "2.4.1", features = ["serde"] }
186187
zeroize = "1.6"
188+
parking_lot = "0.12"
189+
humantime-serde = "1.1"
187190

188191
# Vendored for cross-compilation, see https://github.com/cross-rs/cross/wiki/Recipes#openssl
189192
# Make sure every top level build target actually imports this dependency, and don't end up
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[package]
2+
name = "fendermint_vm_topdown_proof_service"
3+
description = "Proof generator service for F3-based parent finality"
4+
version = "0.1.0"
5+
edition.workspace = true
6+
license.workspace = true
7+
authors.workspace = true
8+
9+
[dependencies]
10+
anyhow = { workspace = true }
11+
async-trait = { workspace = true }
12+
tokio = { workspace = true, features = ["sync", "time", "macros"] }
13+
tracing = { workspace = true }
14+
serde = { workspace = true }
15+
thiserror = { workspace = true }
16+
parking_lot = { workspace = true }
17+
url = { workspace = true }
18+
base64 = { workspace = true }
19+
humantime-serde = { workspace = true }
20+
cid = { workspace = true }
21+
multihash = { workspace = true }
22+
23+
# Fendermint
24+
fendermint_actor_f3_cert_manager = { path = "../../../actors/f3-cert-manager" }
25+
fendermint_vm_genesis = { path = "../../genesis" }
26+
27+
# IPC
28+
ipc-provider = { path = "../../../../ipc/provider" }
29+
ipc-api = { path = "../../../../ipc/api" }
30+
31+
# FVM
32+
fvm_shared = { workspace = true }
33+
fvm_ipld_encoding = { workspace = true }
34+
35+
# Proofs library (will be added from git)
36+
# proofs = { git = "https://github.com/consensus-shipyard/ipc-filecoin-proofs", branch = "proofs" }
37+
38+
[dev-dependencies]
39+
tokio = { workspace = true, features = ["test-util", "rt-multi-thread"] }
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2022-2024 Protocol Labs
2+
// SPDX-License-Identifier: Apache-2.0, MIT
3+
//! Proof bundle assembler
4+
5+
use crate::types::{CacheEntry, ProofBundlePlaceholder};
6+
use anyhow::{Context, Result};
7+
use cid::Cid;
8+
use fendermint_actor_f3_cert_manager::types::F3Certificate;
9+
use fvm_shared::clock::ChainEpoch;
10+
use ipc_provider::lotus::message::f3::F3CertificateResponse;
11+
use std::str::FromStr;
12+
use std::time::SystemTime;
13+
14+
/// Assembles proof bundles from F3 certificates and parent chain data
15+
pub struct ProofAssembler {
16+
/// Source RPC URL (for metadata)
17+
source_rpc: String,
18+
}
19+
20+
impl ProofAssembler {
21+
/// Create a new proof assembler
22+
pub fn new(source_rpc: String) -> Self {
23+
Self { source_rpc }
24+
}
25+
26+
/// Assemble a complete proof bundle for an F3 certificate
27+
///
28+
/// This will eventually:
29+
/// 1. Extract tipsets from ECChain
30+
/// 2. Call ipc-filecoin-proofs::generate_proof_bundle()
31+
/// 3. Build complete CacheEntry
32+
///
33+
/// For now, we create a placeholder bundle
34+
pub async fn assemble_proof(&self, lotus_cert: &F3CertificateResponse) -> Result<CacheEntry> {
35+
tracing::debug!(
36+
instance_id = lotus_cert.gpbft_instance,
37+
"Assembling proof bundle"
38+
);
39+
40+
// Extract finalized epochs from ECChain
41+
let finalized_epochs: Vec<ChainEpoch> = lotus_cert
42+
.ec_chain
43+
.iter()
44+
.map(|entry| entry.epoch)
45+
.collect();
46+
47+
if finalized_epochs.is_empty() {
48+
anyhow::bail!("F3 certificate has empty ECChain");
49+
}
50+
51+
tracing::debug!(
52+
instance_id = lotus_cert.gpbft_instance,
53+
epochs = ?finalized_epochs,
54+
"Extracted epochs from certificate"
55+
);
56+
57+
// Convert Lotus certificate to actor format
58+
let actor_cert = self.convert_lotus_to_actor_cert(lotus_cert)?;
59+
60+
// TODO: Generate actual proof bundle using ipc-filecoin-proofs
61+
// For now, create a placeholder
62+
let highest_epoch = finalized_epochs.iter().max().copied().unwrap();
63+
let bundle = ProofBundlePlaceholder {
64+
parent_height: highest_epoch as u64,
65+
data: vec![],
66+
};
67+
68+
let entry = CacheEntry {
69+
instance_id: lotus_cert.gpbft_instance,
70+
finalized_epochs,
71+
bundle,
72+
actor_certificate: actor_cert,
73+
generated_at: SystemTime::now(),
74+
source_rpc: self.source_rpc.clone(),
75+
};
76+
77+
tracing::info!(
78+
instance_id = entry.instance_id,
79+
epochs_count = entry.finalized_epochs.len(),
80+
"Assembled proof bundle"
81+
);
82+
83+
Ok(entry)
84+
}
85+
86+
/// Convert Lotus F3 certificate to actor certificate format
87+
fn convert_lotus_to_actor_cert(
88+
&self,
89+
lotus_cert: &F3CertificateResponse,
90+
) -> Result<F3Certificate> {
91+
// Extract all epochs from ECChain
92+
let finalized_epochs: Vec<ChainEpoch> = lotus_cert
93+
.ec_chain
94+
.iter()
95+
.map(|entry| entry.epoch)
96+
.collect();
97+
98+
if finalized_epochs.is_empty() {
99+
anyhow::bail!("Empty ECChain in certificate");
100+
}
101+
102+
// Power table CID from last entry in ECChain
103+
// CIDMap.cid is Option<String>, need to parse it
104+
let power_table_cid_str = lotus_cert
105+
.ec_chain
106+
.last()
107+
.context("Empty ECChain")?
108+
.power_table
109+
.cid
110+
.as_ref()
111+
.context("PowerTable CID is None")?;
112+
113+
let power_table_cid =
114+
Cid::from_str(power_table_cid_str).context("Failed to parse power table CID")?;
115+
116+
// Decode signature from base64
117+
use base64::Engine;
118+
let signature = base64::engine::general_purpose::STANDARD
119+
.decode(&lotus_cert.signature)
120+
.context("Failed to decode certificate signature")?;
121+
122+
// Encode full Lotus certificate as CBOR
123+
// This preserves the entire ECChain for verification
124+
let certificate_data =
125+
fvm_ipld_encoding::to_vec(lotus_cert).context("Failed to encode certificate data")?;
126+
127+
Ok(F3Certificate {
128+
instance_id: lotus_cert.gpbft_instance,
129+
finalized_epochs,
130+
power_table_cid,
131+
signature,
132+
certificate_data,
133+
})
134+
}
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
use cid::Cid;
141+
use ipc_provider::lotus::message::f3::{ECChainEntry, F3CertificateResponse, SupplementalData};
142+
use ipc_provider::lotus::message::CIDMap;
143+
use multihash::{Code, MultihashDigest};
144+
145+
fn create_test_lotus_cert(instance: u64, epochs: Vec<i64>) -> F3CertificateResponse {
146+
let power_table_cid = Cid::new_v1(0x55, Code::Blake2b256.digest(b"test"));
147+
let cid_map = CIDMap {
148+
cid: Some(power_table_cid.to_string()),
149+
};
150+
151+
let ec_chain: Vec<ECChainEntry> = epochs
152+
.into_iter()
153+
.map(|epoch| ECChainEntry {
154+
key: vec![],
155+
epoch,
156+
power_table: cid_map.clone(),
157+
commitments: String::new(),
158+
})
159+
.collect();
160+
161+
F3CertificateResponse {
162+
gpbft_instance: instance,
163+
ec_chain,
164+
supplemental_data: SupplementalData {
165+
commitments: String::new(),
166+
power_table: cid_map,
167+
},
168+
signers: vec![],
169+
signature: {
170+
use base64::Engine;
171+
base64::engine::general_purpose::STANDARD.encode(b"test_signature")
172+
},
173+
}
174+
}
175+
176+
#[tokio::test]
177+
async fn test_assemble_proof() {
178+
let assembler = ProofAssembler::new("http://test".to_string());
179+
180+
let lotus_cert = create_test_lotus_cert(100, vec![500, 501, 502, 503]);
181+
182+
let result = assembler.assemble_proof(&lotus_cert).await;
183+
assert!(result.is_ok());
184+
185+
let entry = result.unwrap();
186+
assert_eq!(entry.instance_id, 100);
187+
assert_eq!(entry.finalized_epochs, vec![500, 501, 502, 503]);
188+
assert_eq!(entry.highest_epoch(), Some(503));
189+
assert_eq!(entry.actor_certificate.instance_id, 100);
190+
}
191+
192+
#[tokio::test]
193+
async fn test_assemble_proof_empty_ec_chain() {
194+
let assembler = ProofAssembler::new("http://test".to_string());
195+
196+
let lotus_cert = create_test_lotus_cert(100, vec![]);
197+
198+
let result = assembler.assemble_proof(&lotus_cert).await;
199+
assert!(result.is_err());
200+
}
201+
202+
#[test]
203+
fn test_convert_lotus_to_actor_cert() {
204+
let assembler = ProofAssembler::new("http://test".to_string());
205+
206+
let lotus_cert = create_test_lotus_cert(42, vec![100, 101, 102]);
207+
208+
let result = assembler.convert_lotus_to_actor_cert(&lotus_cert);
209+
assert!(result.is_ok());
210+
211+
let actor_cert = result.unwrap();
212+
assert_eq!(actor_cert.instance_id, 42);
213+
assert_eq!(actor_cert.finalized_epochs, vec![100, 101, 102]);
214+
assert!(!actor_cert.signature.is_empty());
215+
assert!(!actor_cert.certificate_data.is_empty());
216+
}
217+
}

0 commit comments

Comments
 (0)