Skip to content

Commit aecf7f5

Browse files
committed
feat: init lifecycle
1 parent fbaa095 commit aecf7f5

File tree

10 files changed

+280
-11
lines changed

10 files changed

+280
-11
lines changed

Cargo.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fendermint/app/src/service/node.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ pub async fn run(
266266
let top_down_manager = TopDownManager::new(
267267
parent_finality_provider.clone(),
268268
parent_finality_votes.clone(),
269+
None, // TODO: Initialize proof cache when service is launched
269270
);
270271

271272
let interpreter = FvmMessagesInterpreter::new(

fendermint/testing/contract-test/tests/gas_market.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async fn tester_with_upgrader(
6767
let bottom_up_manager = BottomUpManager::new(NeverCallClient, None);
6868
let finality_provider = Arc::new(Toggle::disabled());
6969
let vote_tally = VoteTally::empty();
70-
let top_down_manager = TopDownManager::new(finality_provider, vote_tally);
70+
let top_down_manager = TopDownManager::new(finality_provider, vote_tally, None);
7171

7272
let interpreter: FvmMessagesInterpreter<MemoryBlockstore, _> = FvmMessagesInterpreter::new(
7373
bottom_up_manager,

fendermint/testing/contract-test/tests/run_upgrades.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ async fn test_applying_upgrades() {
203203
let bottom_up_manager = BottomUpManager::new(NeverCallClient, None);
204204
let finality_provider = Arc::new(Toggle::disabled());
205205
let vote_tally = VoteTally::empty();
206-
let top_down_manager = TopDownManager::new(finality_provider, vote_tally);
206+
let top_down_manager = TopDownManager::new(finality_provider, vote_tally, None);
207207

208208
let interpreter: FvmMessagesInterpreter<MemoryBlockstore, _> = FvmMessagesInterpreter::new(
209209
bottom_up_manager,

fendermint/vm/interpreter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ fendermint_vm_genesis = { path = "../genesis" }
1818
fendermint_vm_message = { path = "../message" }
1919
fendermint_vm_resolver = { path = "../resolver" }
2020
fendermint_vm_topdown = { path = "../topdown" }
21+
fendermint_vm_topdown_proof_service = { path = "../topdown/proof-service" }
2122
fendermint_crypto = { path = "../../crypto" }
2223
fendermint_eth_hardhat = { path = "../../eth/hardhat" }
2324
fendermint_eth_deployer = { path = "../../eth/deployer" }

fendermint/vm/interpreter/src/fvm/interpreter.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,21 @@ where
279279
.into_iter()
280280
.map(Into::into);
281281

282-
let top_down_iter = self
283-
.top_down_manager
284-
.chain_message_from_finality_or_quorum()
285-
.await
286-
.into_iter();
282+
// Try proof-based finality first (v2)
283+
let top_down_iter =
284+
if let Some(proof_msg) = self.top_down_manager.chain_message_from_proof_cache().await {
285+
tracing::info!("including proof-based parent finality in proposal");
286+
vec![proof_msg].into_iter()
287+
} else {
288+
// Fallback to v1 voting-based approach
289+
tracing::debug!("no proof available, trying v1 voting-based finality");
290+
self.top_down_manager
291+
.chain_message_from_finality_or_quorum()
292+
.await
293+
.into_iter()
294+
.collect::<Vec<_>>()
295+
.into_iter()
296+
};
287297

288298
let mut all_msgs = top_down_iter
289299
.chain(signed_msgs_iter)
@@ -337,7 +347,28 @@ where
337347
for msg in msgs {
338348
match fvm_ipld_encoding::from_slice::<ChainMessage>(&msg) {
339349
Ok(chain_msg) => match chain_msg {
350+
ChainMessage::Ipc(IpcMessage::ParentFinalityWithProof(bundle)) => {
351+
// DETERMINISTIC VERIFICATION - all validators reach same decision
352+
match self.top_down_manager.verify_proof_bundle(&bundle).await {
353+
Ok(()) => {
354+
tracing::debug!(
355+
instance = bundle.certificate.instance_id,
356+
height = bundle.finality.height,
357+
"proof bundle verified - accepting"
358+
);
359+
}
360+
Err(e) => {
361+
tracing::warn!(
362+
error = %e,
363+
instance = bundle.certificate.instance_id,
364+
"proof bundle verification failed - rejecting block"
365+
);
366+
return Ok(AttestMessagesResponse::Reject);
367+
}
368+
}
369+
}
340370
ChainMessage::Ipc(IpcMessage::TopDownExec(finality)) => {
371+
// v1 voting-based finality (kept for backward compatibility)
341372
if !self.top_down_manager.is_finality_valid(finality).await {
342373
return Ok(AttestMessagesResponse::Reject);
343374
}
@@ -458,7 +489,19 @@ where
458489
})
459490
}
460491
ChainMessage::Ipc(ipc_msg) => match ipc_msg {
492+
IpcMessage::ParentFinalityWithProof(bundle) => {
493+
// NEW: Execute proof-based topdown finality (v2)
494+
let applied_message = self
495+
.top_down_manager
496+
.execute_proof_based_topdown(state, bundle)
497+
.await?;
498+
Ok(ApplyMessageResponse {
499+
applied_message,
500+
domain_hash: None,
501+
})
502+
}
461503
IpcMessage::TopDownExec(p) => {
504+
// OLD: v1 voting-based execution (kept for backward compatibility)
462505
let applied_message =
463506
self.top_down_manager.execute_topdown_msg(state, p).await?;
464507
Ok(ApplyMessageResponse {

fendermint/vm/interpreter/src/fvm/topdown.rs

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,24 @@ where
3939
votes: VoteTally,
4040
// Gateway caller for IPC gateway interactions
4141
gateway_caller: GatewayCaller<DB>,
42+
// Proof cache for F3-based parent finality (optional for gradual rollout)
43+
proof_cache: Option<std::sync::Arc<fendermint_vm_topdown_proof_service::ProofCache>>,
4244
}
4345

4446
impl<DB> TopDownManager<DB>
4547
where
4648
DB: Blockstore + Clone + 'static + Send + Sync,
4749
{
48-
pub fn new(provider: TopDownFinalityProvider, votes: VoteTally) -> Self {
50+
pub fn new(
51+
provider: TopDownFinalityProvider,
52+
votes: VoteTally,
53+
proof_cache: Option<std::sync::Arc<fendermint_vm_topdown_proof_service::ProofCache>>,
54+
) -> Self {
4955
Self {
5056
provider,
5157
votes,
5258
gateway_caller: GatewayCaller::default(),
59+
proof_cache,
5360
}
5461
}
5562

@@ -117,6 +124,76 @@ where
117124
})))
118125
}
119126

127+
/// Query proof cache for next uncommitted proof and create a chain message with proof bundle.
128+
///
129+
/// This is the v2 proof-based approach that replaces voting with cryptographic verification.
130+
///
131+
/// Returns `None` if:
132+
/// - Proof cache is not configured
133+
/// - No proof available for next height
134+
/// - Cache is temporarily empty (graceful degradation)
135+
pub async fn chain_message_from_proof_cache(&self) -> Option<ChainMessage> {
136+
let cache = self.proof_cache.as_ref()?;
137+
138+
// Get next uncommitted proof (instance after last_committed)
139+
let entry = cache.get_next_uncommitted()?;
140+
141+
tracing::debug!(
142+
instance_id = entry.instance_id,
143+
epochs = ?entry.finalized_epochs,
144+
"found proof in cache for proposal"
145+
);
146+
147+
// Extract highest epoch as the finality height
148+
let height = entry.highest_epoch()? as ChainEpoch;
149+
150+
// Extract block hash from the proof bundle
151+
// The proof bundle contains the parent tipset information
152+
// For now, we use an empty block hash as a placeholder
153+
// TODO: Extract actual block hash from certificate or proof bundle
154+
let block_hash = vec![];
155+
156+
Some(ChainMessage::Ipc(IpcMessage::ParentFinalityWithProof(
157+
fendermint_vm_message::ipc::ParentFinalityProofBundle {
158+
finality: ParentFinality { height, block_hash },
159+
certificate: entry.certificate,
160+
proof_bundle: entry.proof_bundle,
161+
},
162+
)))
163+
}
164+
165+
/// Deterministically verify a proof bundle against F3 certificate.
166+
///
167+
/// This performs cryptographic verification of:
168+
/// 1. Storage proofs (contract state at parent height - completeness via topDownNonce)
169+
/// 2. Event proofs (emitted events at parent height)
170+
/// 3. Certificate chain continuity (validates against F3CertManager state)
171+
///
172+
/// All correct validators will reach the same decision (deterministic).
173+
pub async fn verify_proof_bundle(
174+
&self,
175+
bundle: &fendermint_vm_message::ipc::ParentFinalityProofBundle,
176+
) -> anyhow::Result<()> {
177+
use fendermint_vm_topdown_proof_service::verify_proof_bundle;
178+
179+
// Step 1: Verify cryptographic proofs (storage + events)
180+
verify_proof_bundle(&bundle.proof_bundle, &bundle.certificate)
181+
.context("proof bundle cryptographic verification failed")?;
182+
183+
// Step 2: TODO - Verify certificate chain continuity
184+
// Query F3CertManager for last committed instance
185+
// Ensure bundle.certificate.instance_id == last_committed + 1
186+
// This requires querying the F3CertManager actor state
187+
188+
tracing::debug!(
189+
instance_id = bundle.certificate.instance_id,
190+
height = bundle.finality.height,
191+
"proof bundle verified successfully"
192+
);
193+
194+
Ok(())
195+
}
196+
120197
pub async fn update_voting_power_table(&self, power_updates: &PowerUpdates) {
121198
let power_updates_mapped: Vec<_> = power_updates
122199
.0
@@ -127,6 +204,127 @@ where
127204
atomically(|| self.votes.update_power_table(power_updates_mapped.clone())).await
128205
}
129206

207+
/// Execute proof-based topdown finality (v2).
208+
///
209+
/// Steps:
210+
/// 1. Commit parent finality to gateway
211+
/// 2. Update F3CertManager actor with new certificate (TODO)
212+
/// 3. Extract and execute topdown effects (messages + validator changes)
213+
/// 4. Mark instance as committed in cache
214+
/// 5. Update local state (provider + votes)
215+
pub async fn execute_proof_based_topdown(
216+
&self,
217+
state: &mut FvmExecState<DB>,
218+
bundle: fendermint_vm_message::ipc::ParentFinalityProofBundle,
219+
) -> anyhow::Result<AppliedMessage> {
220+
if !self.provider.is_enabled() {
221+
bail!("cannot execute IPC top-down message: parent provider disabled");
222+
}
223+
224+
// Convert to IPCParentFinality
225+
let finality =
226+
IPCParentFinality::new(bundle.finality.height, bundle.finality.block_hash.clone());
227+
228+
tracing::debug!(
229+
finality = finality.to_string(),
230+
instance = bundle.certificate.instance_id,
231+
"executing proof-based topdown finality"
232+
);
233+
234+
// Step 1: Commit parent finality (same as v1)
235+
let (prev_height, _prev_finality) = self
236+
.commit_finality(state, finality.clone())
237+
.await
238+
.context("failed to commit finality")?;
239+
240+
tracing::debug!(
241+
previous_height = prev_height,
242+
current_height = finality.height,
243+
"committed parent finality"
244+
);
245+
246+
// Step 2: TODO - Update F3CertManager actor
247+
// self.update_f3_cert_manager(state, &bundle.certificate)?;
248+
249+
// Step 3: Execute topdown effects
250+
// For now, we use the existing v1 path to fetch messages/changes from the provider
251+
// TODO: Extract from proof bundle instead
252+
let (execution_fr, execution_to) = (prev_height + 1, finality.height);
253+
254+
let validator_changes = self
255+
.provider
256+
.validator_changes_from(execution_fr, execution_to)
257+
.await
258+
.context("failed to fetch validator changes")?;
259+
260+
tracing::debug!(
261+
from = execution_fr,
262+
to = execution_to,
263+
change_count = validator_changes.len(),
264+
"fetched validator changes"
265+
);
266+
267+
self.gateway_caller
268+
.store_validator_changes(state, validator_changes)
269+
.context("failed to store validator changes")?;
270+
271+
let msgs = self
272+
.provider
273+
.top_down_msgs_from(execution_fr, execution_to)
274+
.await
275+
.context("failed to fetch top down messages")?;
276+
277+
tracing::debug!(
278+
message_count = msgs.len(),
279+
start = execution_fr,
280+
end = execution_to,
281+
"fetched topdown messages"
282+
);
283+
284+
let ret = self
285+
.execute_topdown_msgs(state, msgs)
286+
.await
287+
.context("failed to execute top down messages")?;
288+
289+
tracing::debug!("applied topdown messages");
290+
291+
// Step 4: Mark instance as committed in cache
292+
if let Some(cache) = &self.proof_cache {
293+
cache.mark_committed(bundle.certificate.instance_id);
294+
tracing::debug!(
295+
instance = bundle.certificate.instance_id,
296+
"marked instance as committed in cache"
297+
);
298+
}
299+
300+
// Step 5: Update state (same as v1)
301+
let local_block_height = state.block_height() as u64;
302+
let proposer = state
303+
.block_producer()
304+
.map(|id| hex::encode(id.serialize_compressed()));
305+
let proposer_ref = proposer.as_deref();
306+
307+
atomically(|| {
308+
self.provider.set_new_finality(finality.clone())?;
309+
self.votes.set_finalized(
310+
finality.height,
311+
finality.block_hash.clone(),
312+
proposer_ref,
313+
Some(local_block_height),
314+
)?;
315+
Ok(())
316+
})
317+
.await;
318+
319+
tracing::info!(
320+
instance = bundle.certificate.instance_id,
321+
height = finality.height,
322+
"proof-based topdown finality executed successfully"
323+
);
324+
325+
Ok(ret)
326+
}
327+
130328
// TODO Karel - separate this huge function and clean up
131329
pub async fn execute_topdown_msg(
132330
&self,

fendermint/vm/message/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ ipc-api = { path = "../../../ipc/api" }
3030
fendermint_crypto = { path = "../../crypto" }
3131
fendermint_vm_encoding = { path = "../encoding" }
3232
fendermint_vm_actor_interface = { path = "../actor_interface" }
33+
fendermint_vm_topdown_proof_service = { path = "../topdown/proof-service" }
3334
fendermint_testing = { path = "../../testing", optional = true }
35+
proofs = { git = "https://github.com/consensus-shipyard/ipc-filecoin-proofs.git", branch = "proofs" }
3436

3537
[dev-dependencies]
3638
ethers = { workspace = true }

fendermint/vm/message/src/chain.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{ipc::IpcMessage, signed::SignedMessage};
1212
/// signatures are stripped from the messages, to save space. Tendermint Core will
1313
/// not do this for us (perhaps with ABCI++ Vote Extensions we could do it), though.
1414
#[allow(clippy::large_enum_variant)]
15-
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
15+
#[derive(Clone, Debug, Serialize, Deserialize)]
1616
pub enum ChainMessage {
1717
/// A message that can be passed on to the FVM as-is.
1818
Signed(SignedMessage),

0 commit comments

Comments
 (0)