@@ -8,7 +8,10 @@ use reth_evm::execute::BlockExecutionOutput;
88use reth_primitives:: { Receipt as EthereumReceipt , RecoveredBlock , TransactionSigned } ;
99use reth_primitives_traits:: Block ;
1010use reth_trie:: { HashedPostState , KeccakKeyHasher } ;
11+ use revm_primitives:: Address ;
12+ use strata_acct_types:: SubjectId ;
1113use strata_ee_acct_types:: { EnvError , EnvResult , ExecPayload } ;
14+ use strata_ee_chain_types:: BlockInputs ;
1215
1316use crate :: types:: EvmBlock ;
1417
@@ -38,3 +41,200 @@ pub(crate) fn compute_hashed_post_state(
3841) -> HashedPostState {
3942 HashedPostState :: from_bundle_state :: < KeccakKeyHasher > ( & execution_output. state . state )
4043}
44+
45+ /// Converts a SubjectId (32 bytes) to an EVM address (20 bytes).
46+ fn subject_id_to_address ( subject_id : SubjectId ) -> Address {
47+ let subject_bytes: [ u8 ; 32 ] = subject_id. into ( ) ;
48+ // Take the last 20 bytes to form an address
49+ let mut address_bytes = [ 0u8 ; 20 ] ;
50+ address_bytes. copy_from_slice ( & subject_bytes[ 12 ..32 ] ) ;
51+ Address :: from ( address_bytes)
52+ }
53+
54+ /// Converts satoshis to gwei for EVM compatibility.
55+ ///
56+ /// In Alpen: 1 BTC = 10^8 sats = 10^9 gwei
57+ /// Therefore: 1 sat = 10 gwei
58+ ///
59+ /// Per EIP-4895, withdrawal amounts are stored in Gwei (not Wei).
60+ fn sats_to_gwei ( sats : u64 ) -> Option < u64 > {
61+ sats. checked_mul ( 10 )
62+ }
63+
64+ /// Validates that deposits from BlockInputs match the withdrawals field in the block.
65+ ///
66+ /// In Alpen, the EIP-4895 withdrawals field is hijacked to represent deposits from the
67+ /// orchestration layer. This function ensures that the authenticated deposits in BlockInputs
68+ /// match what's committed in the block's withdrawals field.
69+ ///
70+ /// # Warning
71+ /// **Deposits and withdrawals must be in the same order.** This function performs a
72+ /// pairwise comparison using `zip()`, so the nth deposit must match the nth withdrawal.
73+ /// Any reordering will cause validation to fail.
74+ ///
75+ /// # Mapping
76+ /// - `Withdrawal.address` ← last 20 bytes of `SubjectDepositData.dest` (SubjectId)
77+ /// - `Withdrawal.amount` ← `SubjectDepositData.value` in Gwei
78+ /// - `Withdrawal.index` and `Withdrawal.validator_index` are ignored (not meaningful for deposits)
79+ ///
80+ /// # Returns
81+ /// - `Ok(())` if deposits match
82+ /// - `Err(EnvError::InvalidBlock)` if there's a mismatch in count, address, or amount
83+ pub ( crate ) fn validate_deposits_against_block (
84+ block : & RecoveredBlock < AlloyBlock < TransactionSigned > > ,
85+ inputs : & BlockInputs ,
86+ ) -> EnvResult < ( ) > {
87+ // Get withdrawals from the block body (this is where deposits are stored)
88+ // Access the sealed block's body withdrawals through the nested structure
89+ let block_withdrawals = block
90+ . sealed_block ( )
91+ . body ( )
92+ . withdrawals
93+ . as_ref ( )
94+ . map ( |w| w. as_slice ( ) )
95+ . unwrap_or ( & [ ] ) ;
96+
97+ let subject_deposits = inputs. subject_deposits ( ) ;
98+
99+ // Check counts match
100+ if block_withdrawals. len ( ) != subject_deposits. len ( ) {
101+ return Err ( EnvError :: InvalidBlock ) ;
102+ }
103+
104+ // Validate each deposit matches the corresponding withdrawal
105+ for ( withdrawal, deposit) in block_withdrawals. iter ( ) . zip ( subject_deposits. iter ( ) ) {
106+ // Convert SubjectId to Address (last 20 bytes)
107+ let expected_address = subject_id_to_address ( deposit. dest ( ) ) ;
108+
109+ // Convert satoshis to gwei (1 sat = 10 gwei, per 1 BTC = 10^8 sats = 10^9 gwei)
110+ let expected_amount =
111+ sats_to_gwei ( deposit. value ( ) . to_sat ( ) ) . ok_or ( EnvError :: InvalidBlock ) ?;
112+
113+ // Validate address and amount match
114+ if withdrawal. address != expected_address || withdrawal. amount != expected_amount {
115+ return Err ( EnvError :: InvalidBlock ) ;
116+ }
117+
118+ // Note: withdrawal.index and withdrawal.validator_index are not validated
119+ // as they are not meaningful in the deposit context
120+ }
121+
122+ Ok ( ( ) )
123+ }
124+
125+ #[ cfg( test) ]
126+ mod tests {
127+ use alloy_consensus:: { BlockBody , Header } ;
128+ use alloy_eips:: eip4895:: Withdrawal ;
129+ use reth_primitives:: RecoveredBlock ;
130+ use strata_acct_types:: { BitcoinAmount , SubjectId } ;
131+ use strata_ee_chain_types:: { BlockInputs , SubjectDepositData } ;
132+
133+ use super :: * ;
134+
135+ #[ test]
136+ fn test_validate_deposits_valid_match ( ) {
137+ // Create subject ID where last 20 bytes form an address
138+ let mut subject_bytes = [ 0u8 ; 32 ] ;
139+ subject_bytes[ 31 ] = 0x42 ; // Last byte
140+ let subject_id = SubjectId :: new ( subject_bytes) ;
141+ let expected_address = subject_id_to_address ( subject_id) ;
142+
143+ // Create a block with matching withdrawal
144+ let header = Header :: default ( ) ;
145+ let body = BlockBody {
146+ transactions : vec ! [ ] ,
147+ ommers : vec ! [ ] ,
148+ withdrawals : Some (
149+ vec ! [ Withdrawal {
150+ index: 0 ,
151+ validator_index: 0 ,
152+ address: expected_address,
153+ amount: 100 , // 10 sats * 10 = 100 gwei
154+ } ]
155+ . into ( ) ,
156+ ) ,
157+ } ;
158+ let block = AlloyBlock { header, body } ;
159+ let recovered_block: RecoveredBlock < AlloyBlock < TransactionSigned > > =
160+ block. try_into_recovered ( ) . unwrap ( ) ;
161+
162+ // Create matching deposit input
163+ let mut inputs = BlockInputs :: new_empty ( ) ;
164+ let deposit = SubjectDepositData :: new ( subject_id, BitcoinAmount :: from_sat ( 10 ) ) ;
165+ inputs. add_subject_deposit ( deposit) ;
166+
167+ // Should succeed - perfect match
168+ let result = validate_deposits_against_block ( & recovered_block, & inputs) ;
169+ assert ! ( result. is_ok( ) ) ;
170+ }
171+
172+ #[ test]
173+ fn test_validate_deposits_address_mismatch ( ) {
174+ let subject_id = SubjectId :: new ( [ 1u8 ; 32 ] ) ;
175+
176+ // Create a block with different address
177+ let header = Header :: default ( ) ;
178+ let body = BlockBody {
179+ transactions : vec ! [ ] ,
180+ ommers : vec ! [ ] ,
181+ withdrawals : Some (
182+ vec ! [ Withdrawal {
183+ index: 0 ,
184+ validator_index: 0 ,
185+ address: Address :: ZERO , // Wrong address
186+ amount: 100 ,
187+ } ]
188+ . into ( ) ,
189+ ) ,
190+ } ;
191+ let block = AlloyBlock { header, body } ;
192+ let recovered_block: RecoveredBlock < AlloyBlock < TransactionSigned > > =
193+ block. try_into_recovered ( ) . unwrap ( ) ;
194+
195+ // Create deposit with different address
196+ let mut inputs = BlockInputs :: new_empty ( ) ;
197+ let deposit = SubjectDepositData :: new ( subject_id, BitcoinAmount :: from_sat ( 10 ) ) ;
198+ inputs. add_subject_deposit ( deposit) ;
199+
200+ // Should fail - address mismatch
201+ let result = validate_deposits_against_block ( & recovered_block, & inputs) ;
202+ assert ! ( result. is_err( ) ) ;
203+ }
204+
205+ #[ test]
206+ fn test_validate_deposits_amount_mismatch ( ) {
207+ let mut subject_bytes = [ 0u8 ; 32 ] ;
208+ subject_bytes[ 31 ] = 0x42 ;
209+ let subject_id = SubjectId :: new ( subject_bytes) ;
210+ let expected_address = subject_id_to_address ( subject_id) ;
211+
212+ // Create a block with wrong amount
213+ let header = Header :: default ( ) ;
214+ let body = BlockBody {
215+ transactions : vec ! [ ] ,
216+ ommers : vec ! [ ] ,
217+ withdrawals : Some (
218+ vec ! [ Withdrawal {
219+ index: 0 ,
220+ validator_index: 0 ,
221+ address: expected_address,
222+ amount: 200 , // Wrong amount (should be 100)
223+ } ]
224+ . into ( ) ,
225+ ) ,
226+ } ;
227+ let block = AlloyBlock { header, body } ;
228+ let recovered_block: RecoveredBlock < AlloyBlock < TransactionSigned > > =
229+ block. try_into_recovered ( ) . unwrap ( ) ;
230+
231+ // Create deposit with different amount
232+ let mut inputs = BlockInputs :: new_empty ( ) ;
233+ let deposit = SubjectDepositData :: new ( subject_id, BitcoinAmount :: from_sat ( 10 ) ) ; // 10 sats = 100 gwei
234+ inputs. add_subject_deposit ( deposit) ;
235+
236+ // Should fail - amount mismatch
237+ let result = validate_deposits_against_block ( & recovered_block, & inputs) ;
238+ assert ! ( result. is_err( ) ) ;
239+ }
240+ }
0 commit comments