diff --git a/crates/evmexec/src/engine.rs b/crates/evmexec/src/engine.rs index 058115bf3..a64daf477 100644 --- a/crates/evmexec/src/engine.rs +++ b/crates/evmexec/src/engine.rs @@ -5,7 +5,10 @@ use alpen_express_eectl::{ errors::{EngineError, EngineResult}, messages::{ELDepositData, ExecPayloadData, Op, PayloadEnv}, }; -use alpen_express_primitives::buf::Buf64; +use alpen_express_primitives::{ + buf::Buf32, + l1::{BitcoinAmount, XOnlyPk}, +}; use alpen_express_state::{ block::L2BlockBundle, bridge_ops, @@ -174,7 +177,7 @@ impl RpcExecEngineInner { let withdrawal_intents = rpc_withdrawal_intents .into_iter() - .map(to_bridge_withdrawal_intents) + .map(to_bridge_withdrawal_intent) .collect(); let update_output = @@ -350,11 +353,11 @@ struct ForkchoiceStatePartial { pub finalized_block_hash: Option, } -fn to_bridge_withdrawal_intents( +fn to_bridge_withdrawal_intent( rpc_withdrawal_intent: express_reth_node::WithdrawalIntent, ) -> bridge_ops::WithdrawalIntent { let express_reth_node::WithdrawalIntent { amt, dest_pk } = rpc_withdrawal_intent; - bridge_ops::WithdrawalIntent::new(amt, Buf64(dest_pk)) + bridge_ops::WithdrawalIntent::new(BitcoinAmount::from_sat(amt), XOnlyPk::new(Buf32(dest_pk))) } #[cfg(test)] diff --git a/crates/reth/node/src/constants.rs b/crates/reth/node/src/constants.rs index ca6b14f89..23cda5a35 100644 --- a/crates/reth/node/src/constants.rs +++ b/crates/reth/node/src/constants.rs @@ -1 +1,9 @@ -pub use crate::precompiles::bridge::BRIDGEOUT_ADDRESS; +use revm_primitives::{address, Address, U256}; + +use crate::utils::{u256_from, WEI_PER_BTC}; + +/// The address for the Bridgeout precompile contract. +pub const BRIDGEOUT_ADDRESS: Address = address!("000000000000000000000000000000000b121d9e"); + +/// The fixed withdrawal amount in wei (10 BTC equivalent). +pub const FIXED_WITHDRAWAL_WEI: U256 = u256_from(10 * WEI_PER_BTC); diff --git a/crates/reth/node/src/evm.rs b/crates/reth/node/src/evm.rs index 23f896679..6839cc91b 100644 --- a/crates/reth/node/src/evm.rs +++ b/crates/reth/node/src/evm.rs @@ -10,6 +10,8 @@ use revm::{ }; use revm_primitives::{Address, AnalysisKind, Bytes, CfgEnvWithHandlerCfg, Env, TxEnv, U256}; +use crate::{constants::FIXED_WITHDRAWAL_WEI, precompiles}; + /// Custom EVM configuration #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] @@ -33,9 +35,9 @@ impl ExpressEvmConfig { handler.pre_execution.load_precompiles = Arc::new(move || { let mut precompiles = ContextPrecompiles::new(PrecompileSpecId::from_spec_id(spec_id)); precompiles.extend([( - crate::precompiles::bridge::BRIDGEOUT_ADDRESS, + precompiles::bridge::BRIDGEOUT_ADDRESS, ContextPrecompile::ContextStateful(Arc::new( - crate::precompiles::bridge::BridgeoutPrecompile::default(), + precompiles::bridge::BridgeoutPrecompile::new(FIXED_WITHDRAWAL_WEI), )), )]); precompiles diff --git a/crates/reth/node/src/lib.rs b/crates/reth/node/src/lib.rs index a56bfd64b..6df34312e 100644 --- a/crates/reth/node/src/lib.rs +++ b/crates/reth/node/src/lib.rs @@ -6,6 +6,7 @@ mod payload; mod payload_builder; mod precompiles; mod primitives; +mod utils; pub use engine::ExpressEngineTypes; pub use node::ExpressEthereumNode; diff --git a/crates/reth/node/src/payload_builder.rs b/crates/reth/node/src/payload_builder.rs index 7f26debd7..9abc3be18 100644 --- a/crates/reth/node/src/payload_builder.rs +++ b/crates/reth/node/src/payload_builder.rs @@ -399,7 +399,7 @@ where }; Some(WithdrawalIntent { amt: evt.amount, - dest_pk: evt.dest_pk.as_ref().try_into().unwrap(), + dest_pk: evt.dest_pk, }) }) .collect(); diff --git a/crates/reth/node/src/precompiles/bridge.rs b/crates/reth/node/src/precompiles/bridge.rs index 903998de5..30b54b8ee 100644 --- a/crates/reth/node/src/precompiles/bridge.rs +++ b/crates/reth/node/src/precompiles/bridge.rs @@ -1,26 +1,30 @@ +use std::array::TryFromSliceError; + use revm::{ContextStatefulPrecompile, Database}; use revm_primitives::{ - address, Address, Bytes, Log, LogData, PrecompileError, PrecompileErrors, PrecompileOutput, + Bytes, FixedBytes, Log, LogData, PrecompileError, PrecompileErrors, PrecompileOutput, PrecompileResult, U256, }; -use crate::primitives::WithdrawalIntentEvent; +pub use crate::constants::BRIDGEOUT_ADDRESS; +use crate::{primitives::WithdrawalIntentEvent, utils::wei_to_sats}; -// TODO: address? -pub const BRIDGEOUT_ADDRESS: Address = address!("000000000000000000000000000000000b121d9e"); -const MIN_WITHDRAWAL_WEI: u128 = 1_000_000_000_000_000_000u128; +/// Ensure that input is exactly 32 bytes +fn try_into_pubkey(maybe_pubkey: &Bytes) -> Result, TryFromSliceError> { + maybe_pubkey.as_ref().try_into() +} /// Custom precompile to burn rollup native token and add bridge out intent of equal amount. /// Bridge out intent is created during block payload generation. /// This precompile validates transaction and burns the bridge out amount. pub struct BridgeoutPrecompile { - min_withdrawal_wei: U256, + fixed_withdrawal_wei: U256, } -impl Default for BridgeoutPrecompile { - fn default() -> Self { +impl BridgeoutPrecompile { + pub fn new(fixed_withdrawal_wei: U256) -> Self { Self { - min_withdrawal_wei: U256::from(MIN_WITHDRAWAL_WEI), + fixed_withdrawal_wei, } } } @@ -28,47 +32,33 @@ impl Default for BridgeoutPrecompile { impl ContextStatefulPrecompile for BridgeoutPrecompile { fn call( &self, - bytes: &Bytes, + dest_pk_bytes: &Bytes, _gas_limit: u64, evmctx: &mut revm::InnerEvmContext, ) -> PrecompileResult { - // ensure valid calldata - if bytes.len() != 64 { - return Err(PrecompileErrors::Error(PrecompileError::other( - "invalid data", - ))); - } + // Validate the length of the destination public key + let dest_pk = try_into_pubkey(dest_pk_bytes) + .map_err(|_| PrecompileError::other("Invalid public key length: expected 32 bytes"))?; - // ensure minimum bridgeout amount - let value = evmctx.env.tx.value; - if value < self.min_withdrawal_wei { - return Err(PrecompileErrors::Error(PrecompileError::other( - "below min withdrawal amt", - ))); + // Verify that the transaction value matches the required withdrawal amount + let withdrawal_amount = evmctx.env.tx.value; + if withdrawal_amount != self.fixed_withdrawal_wei { + return Err(PrecompileError::other( + "Invalid withdrawal value: must be exactly 10 BTC in wei", + ) + .into()); } - let (sats, rem) = value.div_rem(U256::from(10_000_000_000u128)); + // Convert wei to satoshis + let (sats, _) = wei_to_sats(withdrawal_amount); - if !rem.is_zero() { - // ensure there are no leftovers that get lost. - // is this important? - return Err(PrecompileErrors::Error(PrecompileError::other( - "value must be exact sats", - ))); - } + // Try converting sats (U256) into u64 amount + let amount: u64 = sats.try_into().map_err(|_| PrecompileErrors::Fatal { + msg: "Withdrawal amount exceeds maximum allowed value".into(), + })?; - let Ok(amount) = sats.try_into() else { - // should never happen. 2^64 ~ 8700 x total_btc_stats - return Err(PrecompileErrors::Error(PrecompileError::other( - "above max withdrawal amt", - ))); - }; - - // log bridge withdrawal intent - let evt = WithdrawalIntentEvent { - amount, - dest_pk: bytes.clone(), - }; + // Log the bridge withdrawal intent + let evt = WithdrawalIntentEvent { amount, dest_pk }; let logdata = LogData::from(&evt); evmctx.journaled_state.log(Log { @@ -76,9 +66,19 @@ impl ContextStatefulPrecompile for BridgeoutPrecompile { data: logdata, }); - // TODO: burn value + // Burn value sent to bridge by adjusting the account balance of bridge precompile + let (account, _) = evmctx + .load_account(BRIDGEOUT_ADDRESS) + // Error case should never occur + .map_err(|_| PrecompileErrors::Fatal { + msg: "Failed to load BRIDGEOUT_ADDRESS account".into(), + })?; + + account.info.balance = U256::ZERO; + + // TODO: Properly calculate and deduct gas for the bridge out operation + let gas_cost = 0; - // TODO: gas for bridge out, using 0 gas currently - Ok(PrecompileOutput::new(0, Bytes::new())) + Ok(PrecompileOutput::new(gas_cost, Bytes::new())) } } diff --git a/crates/reth/node/src/primitives.rs b/crates/reth/node/src/primitives.rs index 50d7e8b4c..96576c657 100644 --- a/crates/reth/node/src/primitives.rs +++ b/crates/reth/node/src/primitives.rs @@ -1,22 +1,24 @@ use alloy_sol_types::sol; -use reth_primitives::B512; +use reth_primitives::B256; use serde::{Deserialize, Serialize}; /// Type for withdrawal_intents in rpc. /// Distinct from [`bridge_ops::WithdrawalIntents`] as this will live in reth repo eventually #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct WithdrawalIntent { - /// Amount of currency to be withdrawn. + /// Amount to be withdrawn in sats. pub amt: u64, /// Destination public key for the withdrawal - pub dest_pk: B512, + pub dest_pk: B256, } sol! { #[allow(missing_docs)] event WithdrawalIntentEvent( + /// Withdrawal amount in sats uint64 amount, - bytes dest_pk, + /// 32 bytes pubkey for withdrawal address in L1 + bytes32 dest_pk, ); } diff --git a/crates/reth/node/src/utils.rs b/crates/reth/node/src/utils.rs new file mode 100644 index 000000000..bbd8db190 --- /dev/null +++ b/crates/reth/node/src/utils.rs @@ -0,0 +1,17 @@ +use revm_primitives::U256; + +pub const fn u256_from(val: u128) -> U256 { + U256::from_limbs([(val & ((1 << 64) - 1)) as u64, (val >> 64) as u64, 0, 0]) +} + +/// Number of wei per rollup BTC (1e18). +pub const WEI_PER_BTC: u128 = 1_000_000_000_000_000_000u128; + +/// Number of wei per satoshi (1e10). +const WEI_PER_SAT: U256 = u256_from(10_000_000_000u128); + +/// Converts wei to satoshis. +/// Returns a tuple of (satoshis, remainder_in_wei). +pub fn wei_to_sats(wei: U256) -> (U256, U256) { + wei.div_rem(WEI_PER_SAT) +} diff --git a/crates/state/src/bridge_ops.rs b/crates/state/src/bridge_ops.rs index ae32e0a19..829675e8d 100644 --- a/crates/state/src/bridge_ops.rs +++ b/crates/state/src/bridge_ops.rs @@ -1,6 +1,9 @@ //! Types for managing pending bridging operations in the CL state. -use alpen_express_primitives::{buf::Buf64, l1::BitcoinAmount}; +use alpen_express_primitives::{ + buf::Buf32, + l1::{BitcoinAmount, XOnlyPk}, +}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; @@ -10,26 +13,26 @@ pub const WITHDRAWAL_DENOMINATION: BitcoinAmount = BitcoinAmount::from_int_btc(1 #[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct WithdrawalIntent { /// Quantity of L1 asset, for Bitcoin this is sats. - amt: u64, + amt: BitcoinAmount, /// Destination public key for the withdrawal - pub dest_pk: Buf64, + pub dest_pk: XOnlyPk, } impl WithdrawalIntent { - pub fn new(amt: u64, dest_pk: Buf64) -> Self { + pub fn new(amt: BitcoinAmount, dest_pk: XOnlyPk) -> Self { Self { amt, dest_pk } } - pub fn into_parts(&self) -> (u64, Buf64) { - (self.amt, self.dest_pk) + pub fn as_parts(&self) -> (u64, &Buf32) { + (self.amt.to_sat(), self.dest_pk.buf32()) } - pub fn amt(&self) -> &u64 { + pub fn amt(&self) -> &BitcoinAmount { &self.amt } - pub fn dest_pk(&self) -> &Buf64 { + pub fn dest_pk(&self) -> &XOnlyPk { &self.dest_pk } } @@ -44,7 +47,7 @@ pub struct WithdrawalBatch { impl WithdrawalBatch { /// Gets the total value of the batch. This must be less than the size of /// the utxo it's assigned to. - pub fn get_total_value(&self) -> u64 { + pub fn get_total_value(&self) -> BitcoinAmount { self.intents.iter().map(|wi| wi.amt).sum() } diff --git a/functional-tests/constants.py b/functional-tests/constants.py index c6ed69a01..137fa5e45 100644 --- a/functional-tests/constants.py +++ b/functional-tests/constants.py @@ -19,3 +19,6 @@ "l1_reorg_safe_depth": 4, "target_l2_batch_size": 5, } + +# custom precompiles +PRECOMPILE_BRIDGEOUT_ADDRESS = "0x000000000000000000000000000000000b121d9e" diff --git a/functional-tests/fn_el_bridge_precompile.py b/functional-tests/fn_el_bridge_precompile.py index 0cd70455c..26ebebd30 100644 --- a/functional-tests/fn_el_bridge_precompile.py +++ b/functional-tests/fn_el_bridge_precompile.py @@ -1,7 +1,22 @@ +import os import time import flexitest from web3 import Web3 +from web3._utils.events import get_event_data + +from constants import PRECOMPILE_BRIDGEOUT_ADDRESS + +withdrawal_intent_event_abi = { + "anonymous": False, + "inputs": [ + {"indexed": False, "internalType": "uint64", "name": "amount", "type": "uint64"}, + {"indexed": False, "internalType": "bytes", "name": "dest_pk", "type": "bytes32"}, + ], + "name": "WithdrawalIntentEvent", + "type": "event", +} +event_signature_text = "WithdrawalIntentEvent(uint64,bytes32)" @flexitest.register @@ -14,27 +29,29 @@ def main(self, ctx: flexitest.RunContext): web3: Web3 = reth.create_web3() source = web3.address - dest = web3.to_checksum_address("0x000000000000000000000000000000000b121d9e") + dest = web3.to_checksum_address(PRECOMPILE_BRIDGEOUT_ADDRESS) # 64 bytes - data = "0x" + "00" * 64 + dest_pk = os.urandom(32).hex() + print(dest_pk) assert web3.is_connected(), "cannot connect to reth" original_block_no = web3.eth.block_number - original_balance = web3.eth.get_balance(dest) + original_bridge_balance = web3.eth.get_balance(dest) + original_source_balance = web3.eth.get_balance(source) - print(original_block_no, original_balance) + assert original_bridge_balance == 0 - # 1 eth - to_transfer = 1_000_000_000_000_000_000 + # 10 rollup btc as wei + to_transfer_wei = 10_000_000_000_000_000_000 txid = web3.eth.send_transaction( { "to": dest, - "value": hex(to_transfer), + "value": hex(to_transfer_wei), "gas": hex(100000), "from": source, - "data": data, + "data": dest_pk, } ) print(txid.to_0x_hex()) @@ -43,15 +60,29 @@ def main(self, ctx: flexitest.RunContext): time.sleep(2) receipt = web3.eth.get_transaction_receipt(txid) - # print(receipt) assert receipt.status == 1, "precompile transaction failed" assert len(receipt.logs) == 1, "no logs or invalid logs" - final_block_no = web3.eth.block_number - final_balance = web3.eth.get_balance(dest) + event_signature_hash = web3.keccak(text=event_signature_text).hex() + log = receipt.logs[0] + assert web3.to_checksum_address(log.address) == dest + assert log.topics[0].hex() == event_signature_hash + event_data = get_event_data(web3.codec, withdrawal_intent_event_abi, log) - print(final_block_no, final_balance) + # 1 rollup btc = 10**18 wei + to_transfer_sats = to_transfer_wei // 10_000_000_000 + + assert event_data.args.amount == to_transfer_sats + assert event_data.args.dest_pk.hex() == dest_pk + + final_block_no = web3.eth.block_number + final_bridge_balance = web3.eth.get_balance(dest) + final_source_balance = web3.eth.get_balance(source) assert original_block_no < final_block_no, "not building blocks" - assert original_balance + to_transfer == final_balance, "balance not updated" + assert final_bridge_balance == 0, "bridge out funds not burned" + total_gas_price = receipt.gasUsed * receipt.effectiveGasPrice + assert ( + final_source_balance == original_source_balance - to_transfer_wei - total_gas_price + ), "final balance incorrect"