From df75333e10c9c0c031a6d4ae8554265c8f1bd01b Mon Sep 17 00:00:00 2001 From: Trey Del Bonis Date: Thu, 21 May 2026 16:20:44 -0400 Subject: [PATCH] fix(ee-acct-rt): add bookkeeping to update tracked_balance field, tests --- bin/alpen-client/src/prover/spec_acct.rs | 14 +++++- bin/prover-perf/src/programs/alpen_acct.rs | 8 +++- crates/alpen-ee/block-assembly/src/payload.rs | 5 ++- .../src/update_submitter/update_builder.rs | 4 ++ crates/ee-acct-runtime/src/ee_program.rs | 7 +++ crates/ee-acct-runtime/src/update_builder.rs | 24 +++++++++- .../ee-acct-runtime/src/verification_state.rs | 11 +++++ .../ee-acct-runtime/tests/invalid_updates.rs | 9 +++- crates/ee-acct-types/src/extra_data.rs | 8 +++- crates/ee-acct-types/src/state.rs | 44 ++++++++++++++++++- crates/proof-impl/alpen-acct/src/program.rs | 9 +++- 11 files changed, 133 insertions(+), 10 deletions(-) diff --git a/bin/alpen-client/src/prover/spec_acct.rs b/bin/alpen-client/src/prover/spec_acct.rs index 06481dfab9..95239415ba 100644 --- a/bin/alpen-client/src/prover/spec_acct.rs +++ b/bin/alpen-client/src/prover/spec_acct.rs @@ -360,8 +360,18 @@ impl ProofSpec for AcctSpec { let cur_state = ProofState::new(pre_ee_state.compute_state_root(), pre_inbox_idx); let new_state = ProofState::new(post_state_root, new_inbox_idx); - let extra_data = - UpdateExtraData::new(new_tip_blkid, new_tip_state_root, processed_inputs, 0); + let value_sent = update_outputs.compute_total_value().ok_or_else(|| { + PaasError::PermanentFailure(format!( + "overflow computing aggregate value sent for batch {batch_id}" + )) + })?; + let extra_data = UpdateExtraData::new( + new_tip_blkid, + new_tip_state_root, + processed_inputs, + 0, + value_sent, + ); let extra_data_bytes = encode_to_vec(&extra_data) .map_err(|e| PaasError::PermanentFailure(format!("encode extra data: {e}")))?; let ledger_refs = build_ledger_refs_from_da(&da_refs, self.ol_client.as_ref()) diff --git a/bin/prover-perf/src/programs/alpen_acct.rs b/bin/prover-perf/src/programs/alpen_acct.rs index 9f5b6e917a..a5afeccdf5 100644 --- a/bin/prover-perf/src/programs/alpen_acct.rs +++ b/bin/prover-perf/src/programs/alpen_acct.rs @@ -73,7 +73,13 @@ fn prepare_input() -> EeAcctProofInput { // 4. Build pubvals matching the post-state. The witness fixture is a vanilla Ethereum block — // it doesn't touch the EE precompiles that emit subject deposits or output messages, so all // the aggregate fields are empty. - let extra_data = UpdateExtraData::new(chunk_transition.tip_exec_blkid(), tip_state_root, 0, 0); + let extra_data = UpdateExtraData::new( + chunk_transition.tip_exec_blkid(), + tip_state_root, + 0, + 0, + BitcoinAmount::ZERO, + ); let extra_data_bytes = encode_to_vec(&extra_data).expect("encode extra data"); let pub_params = UpdateProofPubParams::new( diff --git a/crates/alpen-ee/block-assembly/src/payload.rs b/crates/alpen-ee/block-assembly/src/payload.rs index cdeca70c7d..2472ef432e 100644 --- a/crates/alpen-ee/block-assembly/src/payload.rs +++ b/crates/alpen-ee/block-assembly/src/payload.rs @@ -3,7 +3,7 @@ use std::num::NonZero; use alloy_primitives::B256; use alpen_ee_common::{DepositInfo, EnginePayload, PayloadBuildAttributes, PayloadBuilderEngine}; use alpen_reth_evm::subject_to_address_unchecked; -use strata_acct_types::Hash; +use strata_acct_types::{BitcoinAmount, Hash}; use strata_ee_acct_types::{EeAccountState, PendingInputEntry, UpdateExtraData}; use tracing::debug; @@ -61,6 +61,9 @@ pub(crate) async fn build_exec_payload( new_tip_state_root, processed_inputs, processed_fincls, + // No outputs are emitted by this path; balance reconciliation deduction + // is zero. + BitcoinAmount::ZERO, ); Ok((payload, update_extra_data)) diff --git a/crates/alpen-ee/sequencer/src/update_submitter/update_builder.rs b/crates/alpen-ee/sequencer/src/update_submitter/update_builder.rs index 1649d7c0e9..9854e74491 100644 --- a/crates/alpen-ee/sequencer/src/update_submitter/update_builder.rs +++ b/crates/alpen-ee/sequencer/src/update_submitter/update_builder.rs @@ -108,11 +108,15 @@ fn build_update_operation( } // 3. Build extra data + let value_sent = outputs + .compute_total_value() + .ok_or_else(|| eyre!("overflow computing aggregate value sent"))?; let extra_data = UpdateExtraData::new( new_tip_blkid, new_tip_state_root, processed_inputs as u32, 0, + value_sent, ); let extra_data_buf = encode_to_vec(&extra_data)?; diff --git a/crates/ee-acct-runtime/src/ee_program.rs b/crates/ee-acct-runtime/src/ee_program.rs index 1de997e416..110072d1c5 100644 --- a/crates/ee-acct-runtime/src/ee_program.rs +++ b/crates/ee-acct-runtime/src/ee_program.rs @@ -69,6 +69,13 @@ impl SnarkAccountProgram for EeSnarkAccountProgram { // mismatched `UpdateExtraData.new_tip_state_root`. state.set_last_exec_state_root(*extra_data.new_tip_state_root()); + // Decrement tracked balance by the aggregate value sent out by chunk + // outputs in this update. The verification path independently + // accumulates this value and checks it against `extra_data.value_sent` + // in `process_chunks_on_acct`, so this subtraction stays bound to what + // the chunks actually emitted. + state.try_subtract_tracked_balance(*extra_data.value_sent())?; + Ok(()) } diff --git a/crates/ee-acct-runtime/src/update_builder.rs b/crates/ee-acct-runtime/src/update_builder.rs index d9b25e7374..c9733da901 100644 --- a/crates/ee-acct-runtime/src/update_builder.rs +++ b/crates/ee-acct-runtime/src/update_builder.rs @@ -4,7 +4,7 @@ //! inputs, allowing the consumer to query available inputs and accept //! validated [`ChunkTransition`]s. -use strata_acct_types::{Hash, MessageEntry}; +use strata_acct_types::{BitcoinAmount, Hash, MessageEntry}; use strata_ee_acct_types::{ EeAccountState, ExecutionEnvironment, PendingInputEntry, UpdateExtraData, }; @@ -49,6 +49,11 @@ pub struct UpdateBuilder<'i, E: ExecutionEnvironment> { /// Number of forced inclusions processed. fincls_processed: usize, + + /// Aggregate value sent out by accepted chunk outputs (transfers + message + /// payload values). Carried into `UpdateExtraData::value_sent` so the + /// finalization step can decrement `tracked_balance` accordingly. + value_sent: BitcoinAmount, } impl<'i, E: ExecutionEnvironment> UpdateBuilder<'i, E> { @@ -85,6 +90,7 @@ impl<'i, E: ExecutionEnvironment> UpdateBuilder<'i, E> { pending_inputs, inputs_consumed: 0, fincls_processed: 0, + value_sent: BitcoinAmount::ZERO, }) } @@ -197,6 +203,20 @@ impl<'i, E: ExecutionEnvironment> UpdateBuilder<'i, E> { // 3. Merge outputs. let outputs = transition.outputs(); + // Sum the value being sent out by this chunk and accumulate into the + // running total carried in `UpdateExtraData::value_sent`. + let chunk_value_sent = outputs + .output_transfers() + .iter() + .map(|t| t.value()) + .chain(outputs.output_messages().iter().map(|m| m.payload().value())) + .try_fold(BitcoinAmount::ZERO, |acc, v| acc.checked_add(v)) + .ok_or(BuilderError::OutputOverflow)?; + self.value_sent = self + .value_sent + .checked_add(chunk_value_sent) + .ok_or(BuilderError::OutputOverflow)?; + self.inner .outputs_mut() .try_extend_transfers( @@ -251,6 +271,7 @@ impl<'i, E: ExecutionEnvironment> UpdateBuilder<'i, E> { self.cur_tip_state_root, self.inputs_consumed as u32, self.fincls_processed as u32, + self.value_sent, ); let (op, coinputs) = self @@ -271,6 +292,7 @@ impl<'i, E: ExecutionEnvironment> UpdateBuilder<'i, E> { self.cur_tip_state_root, self.inputs_consumed as u32, self.fincls_processed as u32, + self.value_sent, ); Ok(self.inner.build_private_input(extra_data)?) diff --git a/crates/ee-acct-runtime/src/verification_state.rs b/crates/ee-acct-runtime/src/verification_state.rs index ffb42c907d..0dc7f7f913 100644 --- a/crates/ee-acct-runtime/src/verification_state.rs +++ b/crates/ee-acct-runtime/src/verification_state.rs @@ -156,6 +156,11 @@ impl<'a, E: ExecutionEnvironment> EeVerificationState<'a, E> { self.cur_balance } + /// Returns the total value sent out by chunk outputs processed so far. + pub fn total_val_sent(&self) -> BitcoinAmount { + self.total_val_sent + } + /// Increases our verification state tracked balance. /// /// This is intended for when we accept a message. @@ -298,6 +303,12 @@ impl<'a, E: ExecutionEnvironment> EeVerificationState<'a, E> { return Err(EnvError::InconsistentChunkIo); } + // Bind the deduction applied to `tracked_balance` in `pre_finalize_state` + // to the aggregate value the chunk outputs actually emitted. + if self.total_val_sent != *extra_data.value_sent() { + return Err(EnvError::InconsistentChunkIo); + } + Ok(()) } diff --git a/crates/ee-acct-runtime/tests/invalid_updates.rs b/crates/ee-acct-runtime/tests/invalid_updates.rs index 3a2ea79566..898f38c93e 100644 --- a/crates/ee-acct-runtime/tests/invalid_updates.rs +++ b/crates/ee-acct-runtime/tests/invalid_updates.rs @@ -111,6 +111,7 @@ fn test_output_mismatch_fails() { initial_state.last_exec_state_root(), 0, 0, + BitcoinAmount::ZERO, ); let extra_data_buf = encode_to_vec(&extra_data).unwrap(); @@ -139,7 +140,13 @@ fn test_extra_data_tip_mismatch() { // Manually construct operation data with a wrong tip. let wrong_tip = Hash::new([0xAA; 32]); - let extra_data = UpdateExtraData::new(wrong_tip, initial_state.last_exec_state_root(), 0, 0); + let extra_data = UpdateExtraData::new( + wrong_tip, + initial_state.last_exec_state_root(), + 0, + 0, + BitcoinAmount::ZERO, + ); let extra_data_buf = encode_to_vec(&extra_data).unwrap(); let operation = UpdateOperationData::new( diff --git a/crates/ee-acct-types/src/extra_data.rs b/crates/ee-acct-types/src/extra_data.rs index 30191729ef..e2fac36a64 100644 --- a/crates/ee-acct-types/src/extra_data.rs +++ b/crates/ee-acct-types/src/extra_data.rs @@ -1,6 +1,6 @@ //! Interpretation of extra data. -use strata_acct_types::Hash; +use strata_acct_types::{BitcoinAmount, Hash}; use strata_codec::impl_type_flat_struct; use strata_snark_acct_runtime::IExtraData; @@ -31,6 +31,12 @@ impl_type_flat_struct! { /// The total number of items to remove from the fincl queue. processed_fincls: u32, + + /// Aggregate value sent out by chunk outputs (transfers + message + /// payloads) processed in this update. Subtracted from the account's + /// tracked balance during state finalization so that the EE-tracked + /// balance stays reconciled with the OL-side ledger view. + value_sent: BitcoinAmount, } } diff --git a/crates/ee-acct-types/src/state.rs b/crates/ee-acct-types/src/state.rs index 42fe3ca077..1f9d4b80f9 100644 --- a/crates/ee-acct-types/src/state.rs +++ b/crates/ee-acct-types/src/state.rs @@ -5,7 +5,10 @@ use strata_identifiers::Hash; use strata_snark_acct_runtime::IInnerState; use tree_hash::{Sha256Hasher, TreeHash}; -use crate::ssz_generated::ssz::state::{EeAccountState, PendingFinclEntry, PendingInputEntry}; +use crate::{ + errors::{EnvError, EnvResult}, + ssz_generated::ssz::state::{EeAccountState, PendingFinclEntry, PendingInputEntry}, +}; impl EeAccountState { pub fn new( @@ -86,6 +89,19 @@ impl EeAccountState { .expect("snarkacct: overflowing balance"); } + /// Subtracts from the tracked balance, returning [`EnvError::InsufficientFunds`] + /// if the result would underflow. + /// + /// On error the balance is left unchanged. + pub fn try_subtract_tracked_balance(&mut self, amt: BitcoinAmount) -> EnvResult<()> { + let new_balance = self + .tracked_balance + .checked_sub(amt) + .ok_or(EnvError::InsufficientFunds)?; + self.tracked_balance = new_balance; + Ok(()) + } + pub fn pending_inputs(&self) -> &[PendingInputEntry] { &self.pending_inputs } @@ -225,6 +241,7 @@ mod tests { use strata_snark_acct_runtime::IInnerState; use super::*; + use crate::errors::EnvError; ssz_proptest!( EeAccountState, @@ -278,5 +295,30 @@ mod tests { assert_ne!(a.compute_state_root(), b.compute_state_root()); assert_ne!(a.last_exec_state_root(), b.last_exec_state_root()); } + + #[test] + fn tracked_balance_add_then_subtract_roundtrips() { + let mut s = EeAccountState::new( + Hash::from([0u8; 32]), + Hash::from([0u8; 32]), + BitcoinAmount::from_sat(100), + Vec::new(), + Vec::new(), + ); + + s.add_tracked_balance(BitcoinAmount::from_sat(50)); + assert_eq!(s.tracked_balance(), BitcoinAmount::from_sat(150)); + + s.try_subtract_tracked_balance(BitcoinAmount::from_sat(75)) + .expect("subtract within balance must succeed"); + assert_eq!(s.tracked_balance(), BitcoinAmount::from_sat(75)); + + // Underflow must error and leave the balance unchanged. + let err = s + .try_subtract_tracked_balance(BitcoinAmount::from_sat(1000)) + .expect_err("subtract beyond balance must fail"); + assert!(matches!(err, EnvError::InsufficientFunds)); + assert_eq!(s.tracked_balance(), BitcoinAmount::from_sat(75)); + } } } diff --git a/crates/proof-impl/alpen-acct/src/program.rs b/crates/proof-impl/alpen-acct/src/program.rs index d51518c1cd..5e20c6cc7c 100644 --- a/crates/proof-impl/alpen-acct/src/program.rs +++ b/crates/proof-impl/alpen-acct/src/program.rs @@ -145,8 +145,13 @@ mod tests { let state_root = initial_state.compute_state_root(); // Extra data: tip stays the same, nothing processed. - let extra_data = - UpdateExtraData::new(initial_blkid, initial_state.last_exec_state_root(), 0, 0); + let extra_data = UpdateExtraData::new( + initial_blkid, + initial_state.last_exec_state_root(), + 0, + 0, + BitcoinAmount::ZERO, + ); let extra_data_bytes = encode_to_vec(&extra_data).expect("encode extra data"); // With zero chunks and no state change, pre == post state root.