Skip to content
This repository has been archived by the owner on Nov 25, 2024. It is now read-only.

Commit

Permalink
feat: add AUTHCALL instruction (#18)
Browse files Browse the repository at this point in the history
* feat: implement AUTHCALL instruction

* added tests

* comments
  • Loading branch information
fgimenez authored Mar 29, 2024
1 parent 12ed3a1 commit 0d4d8f0
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 30 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

7 changes: 6 additions & 1 deletion bin/alphanet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ clap = { workspace = true, features = ["derive"] }
tikv-jemallocator = { version = "0.5", optional = true }

[features]
default = ["jemalloc"]
default = [
"jemalloc",
"reth/optimism",
"reth-node-optimism/optimism",
]

asm-keccak = []

jemalloc = ["dep:tikv-jemallocator"]
Expand Down
8 changes: 8 additions & 0 deletions crates/instructions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,13 @@ secp256k1 = { version = "0.28.2", default-features = false, features = [
"rand-std"
] }

[features]
optimism = [
"revm/optimism",
"revm-interpreter/optimism",
"revm-precompile/optimism",
"revm-primitives/optimism",
]

[lints]
workspace = true
214 changes: 196 additions & 18 deletions crates/instructions/src/eip3074.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use crate::{context::InstructionsContext, BoxedInstructionWithOpCode};
use revm::{Database, Evm};
use revm_interpreter::{pop, resize_memory, InstructionResult, Interpreter};
use revm_interpreter::{
gas,
instructions::host::{calc_call_gas, get_memory_input_and_out_ranges},
pop, pop_address, resize_memory, CallContext, CallInputs, CallScheme, InstructionResult,
Interpreter, InterpreterAction, Transfer,
};
use revm_precompile::secp256k1::ecrecover;
use revm_primitives::{alloy_primitives::B512, keccak256, Address, B256};
use revm_primitives::{
alloy_primitives::B512, keccak256, spec_to_generic, Address, SpecId, B256, U256,
};

const AUTH_OPCODE: u8 = 0xF6;
const AUTHCALL_OPCODE: u8 = 0xF7;
Expand Down Expand Up @@ -53,6 +60,9 @@ fn compose_msg(chain_id: u64, nonce: u64, invoker_address: Address, commit: B256
keccak256(msg.as_slice())
}

// AUTH instruction, see EIP-3074:
//
// <https://eips.ethereum.org/EIPS/eip-3074#auth-0xf6>
fn auth_instruction<EXT, DB: Database>(
interp: &mut Interpreter,
evm: &mut Evm<'_, EXT, DB>,
Expand Down Expand Up @@ -117,12 +127,81 @@ fn auth_instruction<EXT, DB: Database>(
}
}

// AUTHCALL instruction, see EIP-3074:
//
// <https://eips.ethereum.org/EIPS/eip-3074#authcall-0xf7>
fn authcall_instruction<EXT, DB: Database>(
interp: &mut Interpreter,
_evm: &mut Evm<'_, EXT, DB>,
_ctx: &InstructionsContext,
evm: &mut Evm<'_, EXT, DB>,
ctx: &InstructionsContext,
) {
interp.gas.record_cost(133);
let authorized = match ctx.get(AUTHORIZED_VAR_NAME) {
Some(address) => Address::from_slice(&address),
None => {
interp.instruction_result = InstructionResult::Stop;
return;
}
};

pop!(interp, local_gas_limit);
pop_address!(interp, to);
// max gas limit is not possible in real ethereum situation.
let local_gas_limit = u64::try_from(local_gas_limit).unwrap_or(u64::MAX);

pop!(interp, value);
if interp.is_static && value != U256::ZERO {
interp.instruction_result = InstructionResult::CallNotAllowedInsideStatic;
return;
}

let Some((input, return_memory_offset)) = get_memory_input_and_out_ranges(interp) else {
return;
};

// calc_call_gas requires a generic SPEC argument, spec_to_generic! provides
// it by using the spec ID set in the evm.
let Some(mut gas_limit) = spec_to_generic!(
evm.spec_id(),
calc_call_gas::<Evm<'_, EXT, DB>, SPEC>(
interp,
evm,
to,
value != U256::ZERO,
local_gas_limit,
true,
true
)
) else {
return;
};

gas!(interp, gas_limit);

// add call stipend if there is value to be transferred.
if value != U256::ZERO {
gas_limit = gas_limit.saturating_add(gas::CALL_STIPEND);
}

// Call host to interact with target contract
interp.next_action = InterpreterAction::Call {
inputs: Box::new(CallInputs {
contract: to,
transfer: Transfer { source: interp.contract.address, target: to, value },
input,
gas_limit,
context: CallContext {
address: to,
// set caller to the authorized address.
caller: authorized,
code_address: to,
apparent_value: value,
scheme: CallScheme::Call,
},
is_static: interp.is_static,
return_memory_offset,
}),
};
interp.instruction_result = InstructionResult::CallOrCreate;
}

#[cfg(test)]
Expand All @@ -133,7 +212,7 @@ mod tests {
InMemoryDB,
};
use revm_interpreter::{Contract, SharedMemory, Stack};
use revm_primitives::{Account, Bytecode, Bytes, U256};
use revm_primitives::{address, Account, Bytecode, Bytes};
use secp256k1::{rand, Context, Message, PublicKey, Secp256k1, SecretKey, Signing};
use std::convert::Infallible;

Expand All @@ -149,7 +228,7 @@ mod tests {
B256::ZERO.into(),
);

let interpreter = Interpreter::new(Box::new(contract), 3000000, true);
let interpreter = Interpreter::new(Box::new(contract), 3000000, false);
assert_eq!(interpreter.gas.spend(), 0);

interpreter
Expand All @@ -173,15 +252,34 @@ mod tests {
(secret_key, Address::from_slice(&hash[12..]))
}

fn setup_stack(stack: &mut Stack, authority: Address) {
fn setup_auth_stack(stack: &mut Stack, authority: Address) {
let offset = 0;
let length = 97;
stack.push(U256::from(length)).unwrap();
stack.push(U256::from(offset)).unwrap();
stack.push_b256(B256::left_padding_from(authority.as_slice())).unwrap();
}

fn setup_shared_memory(shared_memory: &mut SharedMemory, y_parity: i32, r: &B256, s: &B256) {
fn setup_authcall_stack(stack: &mut Stack, gas: u64, to: Address, value: u64) {
let ret_offset = 0;
let ret_length = 64;
let args_offset = 64;
let args_length = 64;
stack.push(U256::from(ret_length)).unwrap();
stack.push(U256::from(ret_offset)).unwrap();
stack.push(U256::from(args_length)).unwrap();
stack.push(U256::from(args_offset)).unwrap();
stack.push(U256::from(value)).unwrap();
stack.push_b256(B256::left_padding_from(to.as_slice())).unwrap();
stack.push(U256::from(gas)).unwrap();
}

fn setup_auth_shared_memory(
shared_memory: &mut SharedMemory,
y_parity: i32,
r: &B256,
s: &B256,
) {
shared_memory.resize(100);
shared_memory.set_byte(0, y_parity.try_into().unwrap());
shared_memory.set_word(1, r);
Expand Down Expand Up @@ -226,13 +324,13 @@ mod tests {
let secp = Secp256k1::new();
let (secret_key, authority) = setup_authority(secp.clone());

setup_stack(&mut interpreter.stack, authority);
setup_auth_stack(&mut interpreter.stack, authority);

let msg = default_msg();

let (y_parity, r, s) = generate_signature(secp, secret_key, msg);

setup_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &s);
setup_auth_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &s);

let mut evm = setup_evm();
let context = InstructionsContext::default();
Expand All @@ -257,7 +355,7 @@ mod tests {
let secp = Secp256k1::new();
let (_, authority) = setup_authority(secp.clone());

setup_stack(&mut interpreter.stack, authority);
setup_auth_stack(&mut interpreter.stack, authority);

let mut evm = setup_evm();

Expand All @@ -278,13 +376,13 @@ mod tests {
let (secret_key, _) = setup_authority(secp.clone());
let (_, non_authority) = setup_authority(secp.clone());

setup_stack(&mut interpreter.stack, non_authority);
setup_auth_stack(&mut interpreter.stack, non_authority);

let msg = default_msg();

let (y_parity, r, s) = generate_signature(secp, secret_key, msg);

setup_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &s);
setup_auth_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &s);

let mut evm = setup_evm();

Expand All @@ -302,13 +400,13 @@ mod tests {
let secp = Secp256k1::new();
let (secret_key, authority) = setup_authority(secp.clone());

setup_stack(&mut interpreter.stack, authority);
setup_auth_stack(&mut interpreter.stack, authority);

let msg = default_msg();

let (y_parity, r, s) = generate_signature(secp, secret_key, msg);

setup_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &s);
setup_auth_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &s);

let mut evm = setup_evm();
evm.context.evm.journaled_state.state.insert(authority, Account::default());
Expand All @@ -331,13 +429,13 @@ mod tests {
let secp = Secp256k1::new();
let (secret_key, authority) = setup_authority(secp.clone());

setup_stack(&mut interpreter.stack, authority);
setup_auth_stack(&mut interpreter.stack, authority);

let msg = default_msg();

let (y_parity, r, _) = generate_signature(secp, secret_key, msg);

setup_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &B256::ZERO);
setup_auth_shared_memory(&mut interpreter.shared_memory, y_parity, &r, &B256::ZERO);

let mut evm = setup_evm();

Expand All @@ -349,4 +447,84 @@ mod tests {
let expected_gas = FIXED_FEE_GAS + COLD_AUTHORITY_GAS;
assert_eq!(expected_gas, interpreter.gas.spend());
}

#[test]
fn test_authcall_instruction_authorized_not_set() {
let mut interpreter = setup_interpreter();
let mut evm = setup_evm();

authcall_instruction(&mut interpreter, &mut evm, &InstructionsContext::default());
assert_eq!(interpreter.instruction_result, InstructionResult::Stop);

// check gas
let expected_gas = 0;
assert_eq!(expected_gas, interpreter.gas.spend());
}

#[test]
fn test_authcall_instruction_stack_underflow() {
let mut interpreter = setup_interpreter();
let mut evm = setup_evm();

let authorized = address!("cafecafecafecafecafecafecafecafecafecafe");
let ctx = InstructionsContext::default();
ctx.set(AUTHORIZED_VAR_NAME, authorized.to_vec());

authcall_instruction(&mut interpreter, &mut evm, &ctx);
assert_eq!(interpreter.instruction_result, InstructionResult::StackUnderflow);

// check gas
let expected_gas = 0;
assert_eq!(expected_gas, interpreter.gas.spend());
}

#[test]
fn test_authcall_instruction_happy_path() {
let mut interpreter = setup_interpreter();

let gas_limit = 30_000;
let to = address!("cafecafecafecafecafecafecafecafecafecafe");
let value = 100;
setup_authcall_stack(&mut interpreter.stack, gas_limit, to, value);

let mut evm = setup_evm();
let ctx = InstructionsContext::default();
let authorized = address!("beefbeefbeefbeefbeefbeefbeefbeefbeefbeef");
ctx.set(AUTHORIZED_VAR_NAME, authorized.to_vec());
authcall_instruction(&mut interpreter, &mut evm, &ctx);

assert_eq!(interpreter.instruction_result, InstructionResult::CallOrCreate);

// check gas
let expected_gas = 66612;
assert_eq!(expected_gas, interpreter.gas.spend());

// check next action
let value = U256::from(value);
match interpreter.next_action {
InterpreterAction::Call { inputs } => {
assert_eq!(inputs.contract, to);
assert_eq!(
inputs.transfer,
Transfer { source: interpreter.contract.address, target: to, value }
);
assert_eq!(inputs.input, Bytes::from(&[0_u8; 64]));
assert_eq!(inputs.gas_limit, gas_limit + gas::CALL_STIPEND);
assert_eq!(
inputs.context,
CallContext {
address: to,
// set caller to the authorized address.
caller: authorized,
code_address: to,
apparent_value: value,
scheme: CallScheme::Call,
}
);
assert_eq!(inputs.is_static, interpreter.is_static);
assert_eq!(inputs.return_memory_offset, (0..64));
}
_ => panic!("unexpected next action!"),
}
}
}
14 changes: 14 additions & 0 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,22 @@ alphanet-instructions.workspace = true
reth.workspace = true
reth-node-api.workspace = true
reth-node-optimism.workspace = true
revm.workspace = true
revm-interpreter.workspace = true
revm-precompile.workspace = true
revm-primitives.workspace = true

[features]
default = [
"alphanet-precompile/optimism",
"alphanet-instructions/optimism",
"reth/optimism",
"reth-node-optimism/optimism",
"revm/optimism",
"revm-interpreter/optimism",
"revm-precompile/optimism",
"revm-primitives/optimism",
]

[lints]
workspace = true
Loading

0 comments on commit 0d4d8f0

Please sign in to comment.