Skip to content

Commit

Permalink
Withdrawal intent 32byte pubkey (#288)
Browse files Browse the repository at this point in the history
* use 32 byte calldata for bridge out precompile and withdrawal intent

* require withdrawal to equal 10btc
burn bridge out amount
add checks to funtional test

* resolve pr comments

* address PR comments
  • Loading branch information
sapinb authored Sep 18, 2024
1 parent ccda94a commit c22501f
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 79 deletions.
11 changes: 7 additions & 4 deletions crates/evmexec/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -174,7 +177,7 @@ impl<T: EngineRpc> RpcExecEngineInner<T> {

let withdrawal_intents = rpc_withdrawal_intents
.into_iter()
.map(to_bridge_withdrawal_intents)
.map(to_bridge_withdrawal_intent)
.collect();

let update_output =
Expand Down Expand Up @@ -350,11 +353,11 @@ struct ForkchoiceStatePartial {
pub finalized_block_hash: Option<B256>,
}

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)]
Expand Down
10 changes: 9 additions & 1 deletion crates/reth/node/src/constants.rs
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 4 additions & 2 deletions crates/reth/node/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/reth/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod payload;
mod payload_builder;
mod precompiles;
mod primitives;
mod utils;

pub use engine::ExpressEngineTypes;
pub use node::ExpressEthereumNode;
Expand Down
2 changes: 1 addition & 1 deletion crates/reth/node/src/payload_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
90 changes: 45 additions & 45 deletions crates/reth/node/src/precompiles/bridge.rs
Original file line number Diff line number Diff line change
@@ -1,84 +1,84 @@
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<FixedBytes<32>, 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,
}
}
}

impl<DB: Database> ContextStatefulPrecompile<DB> for BridgeoutPrecompile {
fn call(
&self,
bytes: &Bytes,
dest_pk_bytes: &Bytes,
_gas_limit: u64,
evmctx: &mut revm::InnerEvmContext<DB>,
) -> 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 {
address: BRIDGEOUT_ADDRESS,
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()))
}
}
10 changes: 6 additions & 4 deletions crates/reth/node/src/primitives.rs
Original file line number Diff line number Diff line change
@@ -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,
);
}
17 changes: 17 additions & 0 deletions crates/reth/node/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 12 additions & 9 deletions crates/state/src/bridge_ops.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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
}
}
Expand All @@ -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()
}

Expand Down
3 changes: 3 additions & 0 deletions functional-tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
"l1_reorg_safe_depth": 4,
"target_l2_batch_size": 5,
}

# custom precompiles
PRECOMPILE_BRIDGEOUT_ADDRESS = "0x000000000000000000000000000000000b121d9e"
Loading

0 comments on commit c22501f

Please sign in to comment.