Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 112 additions & 71 deletions EVM.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ audit-rs:
$(CARGO) audit \
--ignore RUSTSEC-2025-0055 \
--ignore RUSTSEC-2026-0194 \
--ignore RUSTSEC-2026-0195
--ignore RUSTSEC-2026-0195 \
--ignore RUSTSEC-2026-0233 \
--ignore RUSTSEC-2026-0234 \
--ignore RUSTSEC-2026-0235 \
--ignore RUSTSEC-2026-0258

.PHONY: audit
audit: audit-rs
Expand Down
52 changes: 48 additions & 4 deletions binary_port/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::convert::TryFrom;

use casper_types::{
bytesrepr::{self, FromBytes, ToBytes},
Transaction,
BlockIdentifier, Transaction,
};

use crate::get_request::GetRequest;
Expand Down Expand Up @@ -115,6 +115,8 @@ pub enum Command {
TrySpeculativeExec {
/// Transaction to execute.
transaction: Transaction,
/// Block against whose state to execute, or the latest complete block if omitted.
block_identifier: Option<BlockIdentifier>,
},
}

Expand All @@ -137,6 +139,9 @@ impl Command {
},
CommandTag::TrySpeculativeExec => Self::TrySpeculativeExec {
transaction: Transaction::random(rng),
block_identifier: rng
.gen::<bool>()
.then(|| BlockIdentifier::Height(rng.gen())),
},
}
}
Expand All @@ -153,15 +158,24 @@ impl ToBytes for Command {
match self {
Command::Get(inner) => inner.write_bytes(writer),
Command::TryAcceptTransaction { transaction } => transaction.write_bytes(writer),
Command::TrySpeculativeExec { transaction } => transaction.write_bytes(writer),
Command::TrySpeculativeExec {
transaction,
block_identifier,
} => {
transaction.write_bytes(writer)?;
block_identifier.write_bytes(writer)
}
}
}

fn serialized_length(&self) -> usize {
match self {
Command::Get(inner) => inner.serialized_length(),
Command::TryAcceptTransaction { transaction } => transaction.serialized_length(),
Command::TrySpeculativeExec { transaction } => transaction.serialized_length(),
Command::TrySpeculativeExec {
transaction,
block_identifier,
} => transaction.serialized_length() + block_identifier.serialized_length(),
}
}
}
Expand All @@ -181,7 +195,14 @@ impl TryFrom<(CommandTag, &[u8])> for Command {
}
CommandTag::TrySpeculativeExec => {
let (transaction, remainder) = FromBytes::from_bytes(bytes)?;
(Command::TrySpeculativeExec { transaction }, remainder)
let (block_identifier, remainder) = FromBytes::from_bytes(remainder)?;
(
Command::TrySpeculativeExec {
transaction,
block_identifier,
},
remainder,
)
}
};
if !remainder.is_empty() {
Expand Down Expand Up @@ -259,4 +280,27 @@ mod tests {
let bytes = val.to_bytes().expect("should serialize");
assert_eq!(Command::try_from((val.tag(), &bytes[..])), Ok(val));
}

#[test]
fn speculative_exec_block_identifier_roundtrips() {
let rng = &mut TestRng::new();
for block_identifier in [
None,
Some(BlockIdentifier::Height(rng.gen())),
Some(BlockIdentifier::Hash(casper_types::BlockHash::new(
rng.gen(),
))),
] {
let command = Command::TrySpeculativeExec {
transaction: Transaction::random(rng),
block_identifier,
};
let bytes = command.to_bytes().expect("should serialize command");

assert_eq!(
Command::try_from((CommandTag::TrySpeculativeExec, &bytes[..])),
Ok(command)
);
}
}
}
44 changes: 44 additions & 0 deletions binary_port/src/error_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ pub enum ErrorCode {
/// EOA initiators are not valid for V1 transactions.
#[error("invalid initiator address for Transaction::V1")]
InvalidTransactionInvalidInitiatorAddr = 120,
/// EVM transaction has a positive effective priority fee per gas.
#[error("the EVM transaction effective priority fee per gas is unsupported")]
InvalidTransactionEvmPositiveEffectivePriorityFeePerGas = 121,
/// EVM transaction maximum priority fee per gas exceeds its maximum total fee per gas.
#[error("the EVM transaction maximum priority fee per gas exceeds its maximum fee per gas")]
InvalidTransactionEvmMaxPriorityFeePerGasExceedsMaxFeePerGas = 122,
}

impl TryFrom<u16> for ErrorCode {
Expand Down Expand Up @@ -409,6 +415,12 @@ impl From<InvalidTransaction> for ErrorCode {
InvalidTransaction::Evm(EvmTransactionError::InvalidNonce { .. }) => {
ErrorCode::InvalidTransactionEvmInvalidNonce
}
InvalidTransaction::Evm(EvmTransactionError::PositiveEffectivePriorityFeePerGas {
..
}) => ErrorCode::InvalidTransactionEvmPositiveEffectivePriorityFeePerGas,
InvalidTransaction::Evm(
EvmTransactionError::MaxPriorityFeePerGasExceedsMaxFeePerGas { .. },
) => ErrorCode::InvalidTransactionEvmMaxPriorityFeePerGasExceedsMaxFeePerGas,
_ => ErrorCode::InvalidTransactionOrDeployUnspecified,
}
}
Expand Down Expand Up @@ -648,6 +660,38 @@ mod tests {
);
}

#[test]
fn evm_positive_effective_priority_fee_has_specific_error_code() {
let error =
InvalidTransaction::Evm(EvmTransactionError::PositiveEffectivePriorityFeePerGas {
priority_fee_per_gas: 1,
});
let code = ErrorCode::from(error);

assert_eq!(
code,
ErrorCode::InvalidTransactionEvmPositiveEffectivePriorityFeePerGas
);
assert_eq!(code as u16, 121);
}

#[test]
fn evm_priority_cap_above_max_fee_has_specific_error_code() {
let error = InvalidTransaction::Evm(
EvmTransactionError::MaxPriorityFeePerGasExceedsMaxFeePerGas {
max_priority_fee_per_gas: 2,
max_fee_per_gas: 1,
},
);
let code = ErrorCode::from(error);

assert_eq!(
code,
ErrorCode::InvalidTransactionEvmMaxPriorityFeePerGasExceedsMaxFeePerGas
);
assert_eq!(code as u16, 122);
}

#[test]
fn invalid_v1_eoa_initiator_has_specific_error_code() {
let code = ErrorCode::from(InvalidTransactionV1::InvalidInitiatorAddr);
Expand Down
20 changes: 13 additions & 7 deletions executor/evm/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ where
{
data_access_layer: &'a DataAccessLayer<S>,
tracking_copy: &'a mut TrackingCopy<R>,
wei_per_mote: u64,
}

impl<'a, R, S> CasperDb<'a, R, S>
Expand All @@ -31,17 +32,21 @@ where
pub(crate) fn new(
data_access_layer: &'a DataAccessLayer<S>,
tracking_copy: &'a mut TrackingCopy<R>,
wei_per_mote: u64,
) -> Self {
Self {
data_access_layer,
tracking_copy,
wei_per_mote,
}
}

fn balance(&mut self, main_purse: casper_types::URef) -> Result<U256, DbError> {
let key = Key::Balance(main_purse.addr());
match self.tracking_copy.read(&key)? {
Some(StoredValue::CLValue(cl_value)) => cl_value_to_u256(key, cl_value),
Some(StoredValue::CLValue(cl_value)) => {
cl_value_to_u256(key, cl_value, self.wei_per_mote)
}
Some(stored_value) => Err(DbError::TypeMismatch {
key: Box::new(key),
expected: "StoredValue::CLValue(U512)",
Expand Down Expand Up @@ -197,19 +202,20 @@ where
}
}

fn cl_value_to_u256(key: Key, cl_value: CLValue) -> Result<U256, DbError> {
let balance = cl_value
fn cl_value_to_u256(key: Key, cl_value: CLValue, wei_per_mote: u64) -> Result<U256, DbError> {
let balance_motes = cl_value
.into_t::<U512>()
.map_err(|error| DbError::BalanceDecode {
key: Box::new(key),
error: error.to_string(),
})?;

if balance.bits() > 256 {
return Err(DbError::BalanceOverflow { key: Box::new(key) });
}
let balance_wei = balance_motes
.checked_mul(U512::from(wei_per_mote))
.filter(|balance| balance.bits() <= 256)
.ok_or_else(|| DbError::BalanceOverflow { key: Box::new(key) })?;

let mut bytes = [0u8; 64];
balance.to_big_endian(&mut bytes);
balance_wei.to_big_endian(&mut bytes);
Ok(U256::from_be_slice(&bytes[32..]))
}
7 changes: 5 additions & 2 deletions executor/evm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub enum Error {
/// EVM execution is disabled in the chainspec configuration.
#[error("EVM execution is disabled")]
Disabled,
/// EVM wei-to-mote conversion ratio is invalid.
#[error("EVM wei_per_mote must be greater than zero")]
InvalidWeiPerMote,
/// Signed EVM transaction does not include an EIP-155 replay-protection chain id.
#[error("EVM transaction is missing replay-protection chain id")]
MissingChainId,
Expand Down Expand Up @@ -65,8 +68,8 @@ pub enum DbError {
/// Decode error text.
error: String,
},
/// A Casper balance does not fit into EVM U256.
#[error("Casper balance at {key} does not fit into EVM U256")]
/// A Casper balance, after scaling from motes to wei, does not fit into EVM U256.
#[error("Casper balance at {key}, scaled to wei, does not fit into EVM U256")]
BalanceOverflow {
/// Balance key that was read.
key: Box<Key>,
Expand Down
36 changes: 25 additions & 11 deletions executor/evm/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ impl EvmExecutor {
if !self.config.enabled {
return Err(Error::Disabled);
}
if self.config.wei_per_mote == 0 {
return Err(Error::InvalidWeiPerMote);
}

if let ExecuteKind::Transaction(transaction) = &request.kind {
let Some(actual) = transaction.chain_id() else {
Expand All @@ -83,7 +86,7 @@ impl EvmExecutor {
};

let result_and_state = {
let db = CasperDb::new(data_access_layer, tracking_copy);
let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote);
let mut evm = Context::mainnet()
.with_db(db)
.with_block(block)
Expand All @@ -96,15 +99,17 @@ impl EvmExecutor {
evm.transact(tx_env).map_err(map_revm_error)?
};

let outcome = ExecutionOutcome::from_revm_result(&result_and_state.result);
let mut state = result_and_state.state;
// revm skips the upfront fee debit but still applies the
// post-execution gas reimbursement and beneficiary reward.
let disabled_fee_transfers =
disabled_fee_transfers(&self.config, &request, &result_and_state.result);
state::remove_disabled_fee_transfers(&mut state, disabled_fee_transfers)?;
state::apply(tracking_copy, state)?;
Ok(outcome)
let dust_motes = state::apply(tracking_copy, state, self.config.wei_per_mote)?;
Ok(ExecutionOutcome::from_revm_result(
&result_and_state.result,
dust_motes,
))
}

/// Executes a system call against the supplied tracking copy.
Expand All @@ -120,10 +125,13 @@ impl EvmExecutor {
if !self.config.enabled {
return Err(Error::Disabled);
}
if self.config.wei_per_mote == 0 {
return Err(Error::InvalidWeiPerMote);
}

let block = request.block.to_revm_block(&self.config)?;
let result_and_state = {
let db = CasperDb::new(data_access_layer, tracking_copy);
let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote);
let mut evm = Context::mainnet()
.with_db(db)
.with_block(block)
Expand All @@ -140,9 +148,15 @@ impl EvmExecutor {
.map_err(map_revm_error)?
};

let outcome = ExecutionOutcome::from_revm_result(&result_and_state.result);
state::apply(tracking_copy, result_and_state.state)?;
Ok(outcome)
let dust_motes = state::apply(
tracking_copy,
result_and_state.state,
self.config.wei_per_mote,
)?;
Ok(ExecutionOutcome::from_revm_result(
&result_and_state.result,
dust_motes,
))
}
}

Expand Down Expand Up @@ -185,9 +199,9 @@ fn disabled_fee_transfers(
),
};

let reimbursed_gas = gas_limit
.saturating_sub(gas.total_gas_spent())
.saturating_add(gas.inner_refunded());
// `tx_gas_used` applies both refunds and the EIP-7623 calldata floor, matching
// the amount revm uses when reimbursing the caller after execution.
let reimbursed_gas = gas_limit.saturating_sub(gas.tx_gas_used());
let caller_reimbursement = U256::from(effective_gas_price) * U256::from(reimbursed_gas);
let coinbase_gas_price = effective_gas_price.saturating_sub(base_fee);
let beneficiary_reward = U256::from(coinbase_gas_price) * U256::from(gas.tx_gas_used());
Expand Down
Loading
Loading