Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EVM: Precompile test framework #1033

Merged
merged 7 commits into from
Jan 13, 2023
Merged
Changes from 6 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
288 changes: 201 additions & 87 deletions actors/evm/tests/precompile.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod asm;

use std::fmt::Debug;

use evm::interpreter::{address::EthAddress, U256};
use fil_actor_evm as evm;
use fil_actors_runtime::test_utils::{
new_bls_addr, MockRuntime, ACCOUNT_ACTOR_CODE_ID, EAM_ACTOR_CODE_ID, EVM_ACTOR_CODE_ID,
MINER_ACTOR_CODE_ID, MULTISIG_ACTOR_CODE_ID, PLACEHOLDER_ACTOR_CODE_ID,
MARKET_ACTOR_CODE_ID, MINER_ACTOR_CODE_ID, MULTISIG_ACTOR_CODE_ID, PLACEHOLDER_ACTOR_CODE_ID,
};
use fvm_shared::address::Address as FILAddress;

Expand Down Expand Up @@ -63,66 +65,159 @@ fn test_precompile_hash() {
);
}

#[test]
fn test_native_actor_type() {
let bytecode = {
fn precompile_address(prefix: u8, index: u8) -> EthAddress {
let mut buf = [0u8; 20];
buf[0] = prefix;
buf[19] = index;
EthAddress(buf)
}

#[repr(u8)]
#[derive(Debug, PartialEq, Eq)]
enum PrecompileExit {
Reverted = 0,
Success = 1,
}

#[repr(u8)]
#[derive(Debug)]
pub enum NativePrecompile {
ResolveAddress = 1,
LookupDelegatedAddress = 2,
CallActor = 3,
GetActorType = 4,
}

impl NativePrecompile {
fn as_address(&self) -> EthAddress {
precompile_address(0xfe, *self as u8)
}
}

struct PrecompileTest {
pub expected_return: Vec<u8>,
pub expected_exit_code: PrecompileExit,
pub precompile_address: EthAddress,
pub output_size: u32,
pub input: Vec<u8>,
pub gas_avaliable: u64,
}

impl Debug for PrecompileTest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PrecompileTest")
.field("expected_exit_code", &self.expected_exit_code)
.field("precompile_address", &self.precompile_address)
.field("input", &hex::encode(&self.input))
.field("expected_return", &hex::encode(&self.expected_return))
.field("output_size", &self.output_size)
.field("gas_avaliable", &self.gas_avaliable)
.finish()
}
}

impl PrecompileTest {
fn run_test(&self, rt: &mut MockRuntime) {
rt.expect_gas_available(self.gas_avaliable);
log::trace!("{:#?}", &self);
// first byte is precompile number, second is output buffer size, rest is input to precompile
let result = util::invoke_contract(
rt,
&[
self.precompile_address.as_evm_word().to_bytes().to_vec(),
U256::from(self.output_size).to_bytes().to_vec(),
self.input.clone(),
]
.concat(),
);
log::trace!("returned: {:?}", hex::encode(&result));
rt.verify();

let returned_exit = match result[0] {
0 => PrecompileExit::Reverted,
1 => PrecompileExit::Success,
_ => panic!("Expected call to give either 1 or 0, this is a bug!"),
};
assert_eq!(self.expected_exit_code, returned_exit);
assert_eq!(&self.expected_return, &result[1..]);
rt.reset();
}

fn test_runner_bytecode() -> Vec<u8> {
let init = "";
let body = r#"

# get call payload size
# store entire input to mem 0x00
calldatasize
# store payload to mem 0x00
push1 0x00
push1 0x00
push1 0x00 # input offset
push1 0x00 # dst offset
calldatacopy

# out size
push1 0x20 # second word of input
mload

# out off
push1 0x20
push1 0xA0
push2 0xA000

# in size
# in off
push1 0x40 # two words
calldatasize
push1 0x00
sub
# in off
push1 0x40 # two words

# value
push1 0x00

# dst (get_actor_type precompile)
push20 0xfe00000000000000000000000000000000000004
# precompile address
push1 0x00 # first word of input is precompile
mload

# gas
push1 0x00

call

# copy result to mem 0x00 (overwrites input data)
# write exit code first byte of memory
push1 0x00 # offset
mstore8

# write precompile return to memory
returndatasize
push1 0x00
push1 0x00
push1 0x00 # input offset
push1 0x01 # dst offset (plus 1 to accommodate exit code)
returndatacopy

# return
# size
returndatasize
push1 0x01
add
# offset
push1 0x00
return
"#;

asm::new_contract("native_actor_type", init, body).unwrap()
};
asm::new_contract("precompile_tester", init, body).unwrap()
}
}

#[test]
fn test_native_actor_type() {
use evm::interpreter::precompiles::NativeType;

let mut rt = util::construct_and_verify(bytecode);
let mut rt = util::construct_and_verify(PrecompileTest::test_runner_bytecode());

// 0x88 is an EVM actor
let evm_target = FILAddress::new_id(0x88);
rt.set_address_actor_type(evm_target, *EVM_ACTOR_CODE_ID);

// f0 31 is a system actor
let system_target = FILAddress::new_id(10);
rt.set_address_actor_type(system_target, *EAM_ACTOR_CODE_ID);
// f0 10 is the EAM actor (System)
let eam_target = FILAddress::new_id(10);
rt.set_address_actor_type(eam_target, *EAM_ACTOR_CODE_ID);

// f0 7 is the Market actor (System)
let market_target = FILAddress::new_id(7);
rt.set_address_actor_type(market_target, *MARKET_ACTOR_CODE_ID);

// f0 101 is an account
let account_target = FILAddress::new_id(101);
Expand All @@ -141,27 +236,56 @@ return
rt.set_address_actor_type(other_target, *MULTISIG_ACTOR_CODE_ID);

fn test_type(rt: &mut MockRuntime, id: FILAddress, expected: NativeType) {
rt.expect_gas_available(10_000_000_000u64);
let result = util::invoke_contract(rt, &id_to_vec(&id));
rt.verify();
assert_eq!(&U256::from(expected as u32).to_bytes(), result.as_slice());
rt.reset();
let test = PrecompileTest {
precompile_address: NativePrecompile::GetActorType.as_address(),
input: id_to_vec(&id),
output_size: 32,
expected_exit_code: PrecompileExit::Success,
expected_return: U256::from(expected as u32).to_bytes().to_vec(),
gas_avaliable: 10_000_000_000,
};
test.run_test(rt);
}

test_type(&mut rt, evm_target, NativeType::EVMContract);
test_type(&mut rt, system_target, NativeType::System);
test_type(&mut rt, eam_target, NativeType::System);
test_type(&mut rt, market_target, NativeType::System);
test_type(&mut rt, account_target, NativeType::Account);
test_type(&mut rt, placeholder_target, NativeType::Placeholder);
test_type(&mut rt, miner_target, NativeType::StorageProvider);
test_type(&mut rt, other_target, NativeType::OtherTypes);
test_type(&mut rt, FILAddress::new_id(10101), NativeType::NonExistent);

// invalid format address
rt.expect_gas_available(10_000_000_000u64);
let result = util::invoke_contract(&mut rt, &[0xff; 64]);
rt.verify();
assert!(result.is_empty());
rt.reset();
// invalid id parameter (over)
fn test_type_invalid(rt: &mut MockRuntime, input: Vec<u8>) {
let test = PrecompileTest {
precompile_address: NativePrecompile::GetActorType.as_address(),
input,
output_size: 32,
expected_exit_code: PrecompileExit::Reverted,
expected_return: vec![],
gas_avaliable: 10_000_000_000,
};
test.run_test(rt);
}

// extra bytes
test_type_invalid(&mut rt, vec![0xff; 64]);
// single byte get padded and is invalid
test_type_invalid(&mut rt, vec![0xff]);

// VERY weird and NOBODY should depend on this, but this is expected behavior soo
// ¯\_(ツ)_/¯
{
// f0 (0xff00)
let padded_target = FILAddress::new_id(0xff00);
rt.set_address_actor_type(padded_target, *EVM_ACTOR_CODE_ID);

// not enough bytes (but still valid id when padded)
let mut input = vec![0u8; 31];
input[30] = 0xff; // will get padded to 0xff00
test_type(&mut rt, evm_target, NativeType::EVMContract);
}
}

fn resolve_address_contract() -> Vec<u8> {
Expand Down Expand Up @@ -216,41 +340,7 @@ return

#[test]
fn test_native_lookup_delegated_address() {
let bytecode = {
let init = "";
let body = r#"

# get call payload size
calldatasize
# store payload to mem 0x00
push1 0x00
push1 0x00
calldatacopy

push1 0x20 # out size
push1 0xA0 # out off
calldatasize # in size
push1 0x00 # in off
push1 0x00 # value
# dst (lookup_delegated_address precompile)
push20 0xfe00000000000000000000000000000000000002
push1 0x00 # gas
call

# copy result to mem 0x00
returndatasize
push1 0x00
push1 0x00
returndatacopy
# return
returndatasize
push1 0x00
return
"#;

asm::new_contract("native_lookup_delegated_address", init, body).unwrap()
};
let mut rt = util::construct_and_verify(bytecode);
let mut rt = util::construct_and_verify(PrecompileTest::test_runner_bytecode());

// f0 10101 is an EVM actor
let evm_target = FILAddress::new_id(10101);
Expand All @@ -262,24 +352,24 @@ return
let unknown_del = FILAddress::new_delegated(1234, "foobarboxy".as_bytes()).unwrap();
rt.add_delegated_address(unknown_target, unknown_del);

fn test_reslove(rt: &mut MockRuntime, id: FILAddress, expected: Vec<u8>) {
rt.expect_gas_available(10_000_000_000u64);
let result = util::invoke_contract(rt, &id_to_vec(&id));
rt.verify();
assert_eq!(expected, result.as_slice());
rt.reset();
fn test_lookup_address(rt: &mut MockRuntime, id: FILAddress, expected: Vec<u8>) {
let test = PrecompileTest {
precompile_address: NativePrecompile::LookupDelegatedAddress.as_address(),
input: id_to_vec(&id),
output_size: 32,
expected_exit_code: PrecompileExit::Success,
expected_return: expected,
gas_avaliable: 10_000_000_000,
};

test.run_test(rt);
}

test_reslove(&mut rt, evm_target, evm_del.to_bytes());
test_reslove(&mut rt, unknown_target, unknown_del.to_bytes());
test_reslove(&mut rt, FILAddress::new_id(11111), Vec::new());
println!("{}", "0000000000000000000000000000000000000000000000000000000000002775".len());

// invalid input
rt.expect_gas_available(10_000_000_000u64);
let result = util::invoke_contract(&mut rt, &[0xff; 42]);
rt.verify();
assert_eq!(Vec::<u8>::new(), result);
rt.reset();
test_lookup_address(&mut rt, evm_target, evm_del.to_bytes());
test_lookup_address(&mut rt, unknown_target, unknown_del.to_bytes());
test_lookup_address(&mut rt, FILAddress::new_id(11111), Vec::new());
}

#[test]
Expand Down Expand Up @@ -399,6 +489,30 @@ fn test_resolve_delegated() {

#[test]
fn test_precompile_failure() {
let mut rt = util::construct_and_verify(PrecompileTest::test_runner_bytecode());

// test invalid precompile address
fn invalid_address(rt: &mut MockRuntime, prefix: u8, index: u8) {
let test = PrecompileTest {
precompile_address: precompile_address(prefix, index), // precompile does not exist
input: vec![0xff; 32], // garbage input should change nothing
output_size: 32,
expected_exit_code: PrecompileExit::Reverted,
expected_return: vec![],
gas_avaliable: 10_000_000_000,
};
test.run_test(rt);
}

// invalid evm precompile
invalid_address(&mut rt, 0x00, 0xff);
// invalid fvm precompile
invalid_address(&mut rt, 0xfe, 0xff);

// TODO above test can be used to test CALL normally

// TODO: refactor these to be more clear

let bytecode = resolve_address_contract();
let mut rt = util::construct_and_verify(bytecode);

Expand Down