Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
- If Wasm contract fixtures are missing, run `make build-contracts-rs`.
- Keep `resources/local/chainspec.toml.in` in sync when editing chainspecs; run `./generate-chainspec.sh` when `resources/local/chainspec.toml` is missing or stale.
- Treat idempotent system contract/predeploy upserts in protocol upgrade handlers as standard activation behavior, not as an alternative to `global_state_update`.
- Do not turn malformed or invalid network-reachable input, including fetched transactions and proposed blocks, into a fatal reactor error. Reject it or record a per-item failure; reserve fatal announcements for internal invariants or unrecoverable local state.
30 changes: 10 additions & 20 deletions EVM.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,19 +749,10 @@ cargo build -p casper-sidecar
`casper-devnet` checkout, not from this workspace.

The devnet tool needs a custom asset named `evm` that points at the debug node
and sidecar binaries built above, plus the local chainspec and config files
from this workspace. Use a node config where
`[binary_port_server].allow_request_speculative_exec = true`; the checked-in local
config defaults this to `false`, so copy `resources/local/config.toml` and
enable it in the copy used for this custom asset.

For example:

```bash
export EVM_DEVNET_NODE_CONFIG=/tmp/casper-node-evm-devnet-config.toml
cp "$CASPER_NODE_WORKSPACE/resources/local/config.toml" "$EVM_DEVNET_NODE_CONFIG"
# Edit $EVM_DEVNET_NODE_CONFIG so allow_request_speculative_exec = true.
```
and sidecar binaries built above. Use the EVM-specific chainspec and node config
from `resources/evm`. They enable EVM execution, addressable entities, and
speculative execution and contain the higher temporary request limits required
by sidecar until its caching is improved.

From a separate `casper-devnet` checkout, register the asset with:

Expand All @@ -770,8 +761,8 @@ cd /path/to/casper-devnet
cargo run -- assets add evm \
--casper-node "$CASPER_NODE_WORKSPACE/target/debug/casper-node" \
--casper-sidecar "$CASPER_SIDECAR_WORKSPACE/target/debug/casper-sidecar" \
--chainspec "$CASPER_NODE_WORKSPACE/resources/local/chainspec.toml" \
--node-config "$EVM_DEVNET_NODE_CONFIG" \
--chainspec "$CASPER_NODE_WORKSPACE/resources/evm/chainspec.toml" \
--node-config "$CASPER_NODE_WORKSPACE/resources/evm/config-example.toml" \
--sidecar-config "$CASPER_SIDECAR_WORKSPACE/resources/example_configs/default_rpc_only_config.toml"
```

Expand All @@ -781,8 +772,8 @@ If `casper-devnet` is already installed on `PATH`, the equivalent command is:
casper-devnet assets add evm \
--casper-node "$CASPER_NODE_WORKSPACE/target/debug/casper-node" \
--casper-sidecar "$CASPER_SIDECAR_WORKSPACE/target/debug/casper-sidecar" \
--chainspec "$CASPER_NODE_WORKSPACE/resources/local/chainspec.toml" \
--node-config "$EVM_DEVNET_NODE_CONFIG" \
--chainspec "$CASPER_NODE_WORKSPACE/resources/evm/chainspec.toml" \
--node-config "$CASPER_NODE_WORKSPACE/resources/evm/config-example.toml" \
--sidecar-config "$CASPER_SIDECAR_WORKSPACE/resources/example_configs/default_rpc_only_config.toml"
```

Expand All @@ -804,8 +795,7 @@ After the `evm` asset is registered, start the network from any directory where
the `casper-devnet` binary is available:

```bash
casper-devnet start --custom-asset evm --force-setup \
--chainspec-override evm.enabled=true
casper-devnet start --custom-asset evm --force-setup
```

The `evm` custom asset uses the debug node binary from
Expand Down Expand Up @@ -1207,4 +1197,4 @@ cargo build -p casper-sidecar
[eip-7702]: https://eips.ethereum.org/EIPS/eip-7702
[eip-7840]: https://eips.ethereum.org/EIPS/eip-7840
[execution-apis]: https://ethereum.github.io/execution-apis/
[geth-pubsub]: https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub
[geth-pubsub]: https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub
5 changes: 4 additions & 1 deletion executor/evm/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Error types returned by the Casper EVM executor.

use casper_storage::{block_store::BlockStoreError, tracking_copy::TrackingCopyError};
use casper_types::Key;
use casper_types::{EvmTransactionError, Key};

use crate::account_state::AccountStorageError;

Expand Down Expand Up @@ -40,6 +40,9 @@ pub enum Error {
/// Failed to apply EVM state changes to the tracking copy.
#[error("failed to apply EVM state changes: {0}")]
State(String),
/// revm rejected the transaction during pre-execution validation.
#[error("EVM transaction validation failed: {0}")]
InvalidTransaction(#[source] EvmTransactionError),
}

/// Errors emitted by the revm database adapter.
Expand Down
82 changes: 78 additions & 4 deletions executor/evm/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ use casper_storage::{
global_state::{error::Error as GlobalStateError, state::StateReader},
TrackingCopy,
};
use casper_types::{EvmConfig, EvmSpec, Key, StoredValue};
use casper_types::{EvmConfig, EvmSpec, EvmTransaction, EvmTransactionError, Key, StoredValue};
use revm::{
context::CfgEnv,
context_interface::result::{EVMError, ExecutionResult as RevmExecutionResult, ResultGas},
context_interface::result::{
EVMError, ExecutionResult as RevmExecutionResult,
InvalidTransaction as RevmInvalidTransaction, ResultGas,
},
handler::{Handler, MainnetHandler},
primitives::{hardfork::SpecId, Bytes, U256},
Context, ExecuteEvm, MainBuilder, MainContext, SystemCallEvm,
};

use crate::{
db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, DbError, Error, ExecuteKind,
ExecuteRequest, ExecutionOutcome, Result, SystemCallRequest,
db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, BlockContext, DbError, Error,
ExecuteKind, ExecuteRequest, ExecutionOutcome, Result, SystemCallRequest,
};

/// Executes EVM transactions and calls against a Casper tracking copy.
Expand Down Expand Up @@ -46,6 +50,60 @@ impl EvmExecutor {
&self.config
}

/// Validates an EVM transaction without executing it or applying state changes.
pub fn validate_transaction<R, S>(
&self,
data_access_layer: &DataAccessLayer<S>,
tracking_copy: &mut TrackingCopy<R>,
block_context: BlockContext,
transaction: &EvmTransaction,
) -> Result<()>
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
if !self.config.enabled {
return Err(Error::Disabled);
}
if self.config.wei_per_mote == 0 {
return Err(Error::InvalidWeiPerMote);
}

let Some(actual) = transaction.chain_id() else {
return Err(Error::MissingChainId);
};
if actual != self.config.chain_id {
return Err(Error::ChainIdMismatch {
expected: self.config.chain_id,
actual,
});
}

let kind = ExecuteKind::Transaction(Box::new(transaction.clone()));
let tx_env = tx::build_tx_env(&self.config, &kind)?;
let block = block_context.to_revm_block(&self.config)?;
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)
.with_tx(tx_env)
.modify_cfg_chained(|cfg| {
configure_evm_cfg(cfg, &self.config, EvmExecutionMode::Checked);
})
.build_mainnet()
.with_precompiles(CasperEvmPrecompiles::new(spec_id(self.config.spec)));

// Use revm's own validation phases so this stays in sync as Ethereum
// transaction preconditions grow. Journal mutations made while loading
// and checking the caller are discarded with this temporary EVM.
let handler = MainnetHandler::default();
let mut initial_gas = handler
.validate(&mut evm)
.map_err(map_revm_validation_error)?;
handler
.validate_against_state_and_deduct_caller(&mut evm, &mut initial_gas)
.map_err(map_revm_validation_error)
}

/// Executes an EVM transaction or call against the supplied tracking copy.
pub fn execute<R, S>(
&self,
Expand Down Expand Up @@ -234,3 +292,19 @@ fn map_revm_error(error: EVMError<DbError>) -> Error {
other => Error::Revm(other.to_string()),
}
}

fn map_revm_validation_error(error: EVMError<DbError>) -> Error {
match error {
EVMError::Transaction(
RevmInvalidTransaction::NonceTooHigh { tx, state }
| RevmInvalidTransaction::NonceTooLow { tx, state },
) => Error::InvalidTransaction(EvmTransactionError::InvalidNonce {
expected: state,
actual: tx,
}),
EVMError::Transaction(error) => {
Error::InvalidTransaction(EvmTransactionError::Validation(error.to_string()))
}
other => map_revm_error(other),
}
}
65 changes: 56 additions & 9 deletions executor/evm/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn apply_account<R>(
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
// Check how to deal with Key::Balance after selfdestruct
// A self-destruct removes persisted EVM state while preserving any linked Casper account.
let address = tx::from_revm_address(address);
let account_key = Key::Evm(EvmAddr::Account(address));

Expand Down Expand Up @@ -149,12 +149,29 @@ where
for key in storage_keys {
tracking_copy.prune(key);
}
// A contract created and destroyed within one transaction has no persisted balance or EVM
// metadata to remove. Avoid emitting prunes for those missing keys: scratch-state commits
// reject such transforms instead of treating them as no-ops.
if !matches!(identity, Some(account_state::AccountIdentity::Account(_))) {
tracking_copy.prune(Key::Balance(main_purse.addr()));
prune_if_exists(tracking_copy, Key::Balance(main_purse.addr()))?;
}
prune_if_exists(tracking_copy, account_key)?;
prune_if_exists(tracking_copy, Key::Evm(EvmAddr::Nonce(address)))?;
prune_if_exists(tracking_copy, Key::Evm(EvmAddr::CodeHash(address)))?;
Ok(())
}

fn prune_if_exists<R>(tracking_copy: &mut TrackingCopy<R>, key: Key) -> Result<(), Error>
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
if tracking_copy
.read(&key)
.map_err(|error| Error::State(error.to_string()))?
.is_some()
{
tracking_copy.prune(key);
}
tracking_copy.prune(account_key);
tracking_copy.prune(Key::Evm(EvmAddr::Nonce(address)));
tracking_copy.prune(Key::Evm(EvmAddr::CodeHash(address)));
Ok(())
}

Expand Down Expand Up @@ -214,24 +231,54 @@ fn resolve_balances(
let wei_per_mote = U512::from(wei_per_mote);
let mut balances = AddressMap::with_capacity_and_hasher(state.len(), Default::default());
let mut aggregate_remainder_wei = U512::zero();
let mut aggregate_original_balance_wei = U512::zero();
let mut aggregate_final_balance_wei = U512::zero();
for (address, account) in state {
let balance_wei = u256_to_u512(account.info.balance);
balances.insert(*address, balance_wei / wei_per_mote);
aggregate_original_balance_wei = aggregate_original_balance_wei
.checked_add(u256_to_u512(account.original_info.balance))
.ok_or_else(|| {
Error::State("aggregate original EVM balance overflowed U512".to_string())
})?;
aggregate_final_balance_wei = aggregate_final_balance_wei
.checked_add(balance_wei)
.ok_or_else(|| {
Error::State("aggregate final EVM balance overflowed U512".to_string())
})?;
aggregate_remainder_wei = aggregate_remainder_wei
.checked_add(balance_wei % wei_per_mote)
.ok_or_else(|| {
Error::State("aggregate EVM balance remainder overflowed U512".to_string())
})?;
}

if aggregate_remainder_wei % wei_per_mote != U512::zero() {
// A self-destruct can remove wei which never appear in the final account balances. Combine
// that amount with the remainders discarded while converting final balances to motes. The
// original balances came from mote-denominated Casper purses, so the combined amount must be
// an exact number of motes even when neither component is independently representable.
let destroyed_balance_wei = aggregate_original_balance_wei
.checked_sub(aggregate_final_balance_wei)
.ok_or_else(|| {
Error::State(format!(
"aggregate EVM balance increased from {aggregate_original_balance_wei} wei to \
{aggregate_final_balance_wei} wei"
))
})?;
let discarded_balance_wei = aggregate_remainder_wei
.checked_add(destroyed_balance_wei)
.ok_or_else(|| {
Error::State("aggregate discarded EVM balance overflowed U512".to_string())
})?;

if discarded_balance_wei % wei_per_mote != U512::zero() {
return Err(Error::State(format!(
"aggregate EVM balance remainder {aggregate_remainder_wei} wei is not divisible by \
"aggregate discarded EVM balance {discarded_balance_wei} wei is not divisible by \
{wei_per_mote} wei per mote"
)));
}

Ok((balances, aggregate_remainder_wei / wei_per_mote))
Ok((balances, discarded_balance_wei / wei_per_mote))
}

#[cfg(test)]
Expand Down Expand Up @@ -277,7 +324,7 @@ mod tests {
assert!(matches!(
resolve_balances(&state, 10),
Err(Error::State(message))
if message.contains("aggregate EVM balance remainder 1 wei is not divisible")
if message.contains("aggregate discarded EVM balance 1 wei is not divisible")
));
}
}
46 changes: 46 additions & 0 deletions executor/evm/tests/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,13 @@ fn one_wei_transfer_init_code(recipient: evm::Address, terminal: &[u8]) -> Vec<u
init_code_returning(runtime)
}

fn one_wei_transfer_then_selfdestruct_init_code(recipient: evm::Address) -> Vec<u8> {
let mut init_code = Vec::new();
append_one_wei_call(&mut init_code, recipient);
init_code.extend([opcode::ADDRESS, opcode::SELFDESTRUCT]);
init_code
}

fn return_call_value_to_caller_init_code() -> Vec<u8> {
let runtime = vec![
opcode::PUSH1,
Expand Down Expand Up @@ -1603,6 +1610,45 @@ fn internal_one_wei_transfer_reports_one_aggregate_dust_mote() {
assert_eq!(read_balance(&mut tracking_copy, recipient), U512::zero());
}

#[test]
fn selfdestruct_after_one_wei_transfer_reports_one_dust_mote() {
let executor = executor(EvmSpec::Prague);
let recipient = evm::Address::new([0x42; 20]);
let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy();
let tx = TxLegacy {
chain_id: Some(7),
nonce: 0,
gas_price: 1,
gas_limit: 200_000,
to: TxKind::Create,
value: U256::from(DEFAULT_WEI_PER_MOTE),
input: one_wei_transfer_then_selfdestruct_init_code(recipient).into(),
};
let tx = tx.into_signed(Signature::test_signature().with_parity(true));
let transaction = EvmTransaction::from_signed_rlp(
TxEnvelope::from(tx).encoded_2718(),
Timestamp::zero(),
casper_types::TimeDiff::from_seconds(60),
)
.expect("transaction should decode");
seed_evm_balance(&mut tracking_copy, transaction.from(), U512::from(10u64));

let outcome = executor
.execute(
&data_access_layer,
&mut tracking_copy,
ExecuteRequest {
block: block(),
kind: ExecuteKind::Transaction(Box::new(transaction)),
},
)
.expect("self-destructed wei and final balance remainders should aggregate into motes");

assert_eq!(outcome.status, ExecutionStatus::Success);
assert_eq!(outcome.dust_motes, U512::one());
assert_eq!(read_balance(&mut tracking_copy, recipient), U512::zero());
}

#[test]
fn recombined_internal_wei_produces_no_dust() {
let executor = executor(EvmSpec::Prague);
Expand Down
Loading
Loading