From 9480cbed99a13117faec580fdd29cab7c0379ba9 Mon Sep 17 00:00:00 2001 From: Hamish Ivey-Law <426294+unzvfu@users.noreply.github.com> Date: Thu, 30 Mar 2023 05:56:01 +1100 Subject: [PATCH] Signed operations as syscalls (#933) * Implement syscalls for BYTE, SIGNEXTEND, SAR, SLT and SGT. * Implement SDIV and SMOD; minor documentation and tidying. * Implement EXP. * Add sys_byte to the syscall jumptable. * Test suite for signed syscalls. * Handle `EXIT_KERNEL` "properly". * Add gas charges; rename label. * Uppercase all opcodes. * Add test for BYTE; fix bug in BYTE. * Calculate and charge gas for calling `EXP`. * Fix gas calculation for `exponent = 0`. * Address Jacqui's comments. * Remove BYTE syscall as it will be implemented natively. * Oops, forgot to remove this bit. --- evm/src/cpu/kernel/aggregator.rs | 1 + evm/src/cpu/kernel/asm/core/syscall_stubs.asm | 12 - evm/src/cpu/kernel/asm/exp.asm | 28 ++- evm/src/cpu/kernel/asm/signed.asm | 216 ++++++++++++++++++ evm/src/cpu/kernel/interpreter.rs | 6 +- evm/src/cpu/kernel/tests/mod.rs | 1 + evm/src/cpu/kernel/tests/signed_syscalls.rs | 166 ++++++++++++++ 7 files changed, 416 insertions(+), 14 deletions(-) create mode 100644 evm/src/cpu/kernel/asm/signed.asm create mode 100644 evm/src/cpu/kernel/tests/signed_syscalls.rs diff --git a/evm/src/cpu/kernel/aggregator.rs b/evm/src/cpu/kernel/aggregator.rs index 40189b80..0b7233e0 100644 --- a/evm/src/cpu/kernel/aggregator.rs +++ b/evm/src/cpu/kernel/aggregator.rs @@ -112,6 +112,7 @@ pub(crate) fn combined_kernel() -> Kernel { include_str!("asm/rlp/num_bytes.asm"), include_str!("asm/rlp/read_to_memory.asm"), include_str!("asm/shift.asm"), + include_str!("asm/signed.asm"), include_str!("asm/transactions/common_decoding.asm"), include_str!("asm/transactions/router.asm"), include_str!("asm/transactions/type_0.asm"), diff --git a/evm/src/cpu/kernel/asm/core/syscall_stubs.asm b/evm/src/cpu/kernel/asm/core/syscall_stubs.asm index f94bcad4..95b50b0b 100644 --- a/evm/src/cpu/kernel/asm/core/syscall_stubs.asm +++ b/evm/src/cpu/kernel/asm/core/syscall_stubs.asm @@ -1,18 +1,6 @@ // Labels for unimplemented syscalls to make the kernel assemble. // Each label should be removed from this file once it is implemented. -global sys_sdiv: - PANIC -global sys_smod: - PANIC -global sys_signextend: - PANIC -global sys_slt: - PANIC -global sys_sgt: - PANIC -global sys_sar: - PANIC global sys_blockhash: PANIC global sys_prevrandao: diff --git a/evm/src/cpu/kernel/asm/exp.asm b/evm/src/cpu/kernel/asm/exp.asm index 0aa40048..a2f34b13 100644 --- a/evm/src/cpu/kernel/asm/exp.asm +++ b/evm/src/cpu/kernel/asm/exp.asm @@ -73,4 +73,30 @@ recursion_return: jump global sys_exp: - PANIC // TODO: Implement. + // stack: x, e, return_info + push 0 + // stack: shift, x, e, return_info + %jump(sys_exp_gas_loop_enter) +sys_exp_gas_loop: + %add_const(8) +sys_exp_gas_loop_enter: + dup3 + dup2 + shr + // stack: e >> shift, shift, x, e, return_info + %jumpi(sys_exp_gas_loop) + // stack: shift_bits, x, e, return_info + %div_const(8) + // stack: byte_size_of_e := shift_bits / 8, x, e, return_info + %mul_const(@GAS_EXPBYTE) + %add_const(@GAS_EXP) + // stack: gas_cost := 10 + 50 * byte_size_of_e, x, e, return_info + %stack(gas_cost, x, e, return_info) -> (gas_cost, return_info, x, e) + %charge_gas + + %stack(return_info, x, e) -> (x, e, sys_exp_return, return_info) + jump exp +sys_exp_return: + // stack: pow(x, e), return_info + swap1 + exit_kernel diff --git a/evm/src/cpu/kernel/asm/signed.asm b/evm/src/cpu/kernel/asm/signed.asm new file mode 100644 index 00000000..7dd9c482 --- /dev/null +++ b/evm/src/cpu/kernel/asm/signed.asm @@ -0,0 +1,216 @@ +// SDIV(a, b): signed division operation. +// +// If b = 0, then SDIV(a, b) = 0, +// else if a = -2^255 and b = -1, then SDIV(a, b) = -2^255 +// else SDIV(a, b) = sgn(a/b) * floor(|a/b|). +global _sys_sdiv: + // stack: num, denom, return_info + DUP1 + PUSH 0x8000000000000000000000000000000000000000000000000000000000000000 + GT + // stack: num_is_nonneg := sign_bit > num, num, denom, return_info + DUP1 + %jumpi(sys_sdiv_nonneg_num) + // stack: num_is_nonneg, num, denom, return_info + SWAP1 + PUSH 0 + SUB + SWAP1 + // stack: num_is_nonneg, num := -num, denom, return_info +sys_sdiv_nonneg_num: + SWAP2 + DUP1 + PUSH 0x8000000000000000000000000000000000000000000000000000000000000000 + GT + // stack: denom_is_nonneg := sign_bit > denom, denom, num, num_is_nonneg, return_info + DUP1 + %jumpi(sys_sdiv_nonneg_denom) + // stack: denom_is_nonneg, denom, num, num_is_nonneg, return_info + SWAP1 + PUSH 0 + SUB + // stack: denom := -denom, denom_is_nonneg, num, num_is_nonneg, return_info + SWAP1 +sys_sdiv_nonneg_denom: + // stack: denom_is_nonneg, denom, num, num_is_nonneg, return_info + SWAP2 + DIV + // stack: num / denom, denom_is_nonneg, num_is_nonneg, return_info + SWAP2 + EQ + // stack: denom_is_nonneg == num_is_nonneg, num / denom, return_info + %jumpi(sys_sdiv_same_sign) + PUSH 0 + SUB +sys_sdiv_same_sign: + SWAP1 + JUMP + + +// SMOD(a, b): signed "modulo remainder" operation. +// +// If b != 0, then SMOD(a, b) = sgn(a) * MOD(|a|, |b|), +// else SMOD(a, 0) = 0. +global _sys_smod: + // stack: x, mod, return_info + PUSH 0x8000000000000000000000000000000000000000000000000000000000000000 + // stack: sign_bit, x, mod, return_info + DUP1 + DUP4 + LT + // stack: mod < sign_bit, sign_bit, x, mod, return_info + %jumpi(sys_smod_pos_mod) + // mod is negative, so we negate it + // sign_bit, x, mod, return_info + SWAP2 + PUSH 0 + SUB + SWAP2 + // sign_bit, x, mod := 0 - mod, return_info +sys_smod_pos_mod: + // At this point, we know that mod is non-negative. + DUP2 + LT + // stack: x < sign_bit, x, mod, return_info + %jumpi(sys_smod_pos_x) + // x is negative, so let's negate it + // stack: x, mod, return_info + PUSH 0 + SUB + // stack: x := 0 - x, mod, return_info + MOD + // negate the result + PUSH 0 + SUB + SWAP1 + JUMP +sys_smod_pos_x: + // Both x and mod are non-negative + // stack: x, mod, return_info + MOD + SWAP1 + JUMP + + +// SIGNEXTEND from the Nth byte of value, where the bytes of value are +// considered in LITTLE-endian order. Just a SHL followed by a SAR. +global _sys_signextend: + // Stack: N, value, return_info + // Handle N >= 31, which is a no-op. + PUSH 31 + %min + // Stack: min(31, N), value, return_info + %increment + %mul_const(8) + // Stack: 8*(N + 1), value, return_info + PUSH 256 + SUB + // Stack: 256 - 8*(N + 1), value, return_info + %stack(bits, value, return_info) -> (bits, value, bits, return_info) + SHL + SWAP1 + // Stack: bits, value << bits, return_info + // fall through to sys_sar + + +// SAR, i.e. shift arithmetic right, shifts `value` `shift` bits to +// the right, preserving sign by filling with the most significant bit. +// +// Trick: x >>s i = (x + sign_bit >>u i) - (sign_bit >>u i), +// where >>s is arithmetic shift and >>u is logical shift. +// Reference: Hacker's Delight, 2013, 2nd edition, §2-7. +global _sys_sar: + // SAR(shift, value) is the same for all shift >= 255, so we + // replace shift with min(shift, 255) + + // Stack: shift, value, return_info + PUSH 255 + %min + // Stack: min(shift, 255), value, return_info + + // Now assume shift < 256. + // Stack: shift, value, return_info + PUSH 0x8000000000000000000000000000000000000000000000000000000000000000 + DUP2 + SHR + // Stack: 2^255 >> shift, shift, value, return_info + SWAP2 + %add_const(0x8000000000000000000000000000000000000000000000000000000000000000) + // Stack: 2^255 + value, shift, 2^255 >> shift, return_info + SWAP1 + SHR + SUB + // Stack: ((2^255 + value) >> shift) - (2^255 >> shift), return_info + SWAP1 + JUMP + + +// SGT, i.e. signed greater than, returns 1 if lhs > rhs as signed +// integers, 0 otherwise. +// +// Just swap argument order and fall through to signed less than. +global _sys_sgt: + SWAP1 + + +// SLT, i.e. signed less than, returns 1 if lhs < rhs as signed +// integers, 0 otherwise. +// +// Trick: x (_sys_sdiv, x, y, _syscall_return, kernel_return) + JUMP + +global sys_smod: + %charge_gas_const(@GAS_LOW) + %stack(x, y, kernel_return) -> (_sys_smod, x, y, _syscall_return, kernel_return) + JUMP + +global sys_signextend: + %charge_gas_const(@GAS_LOW) + %stack(x, y, kernel_return) -> (_sys_signextend, x, y, _syscall_return, kernel_return) + JUMP + +global sys_sar: + %charge_gas_const(@GAS_VERYLOW) + %stack(x, y, kernel_return) -> (_sys_sar, x, y, _syscall_return, kernel_return) + JUMP + +global sys_slt: + %charge_gas_const(@GAS_VERYLOW) + %stack(x, y, kernel_return) -> (_sys_slt, x, y, _syscall_return, kernel_return) + JUMP + +global sys_sgt: + %charge_gas_const(@GAS_VERYLOW) + %stack(x, y, kernel_return) -> (_sys_sgt, x, y, _syscall_return, kernel_return) + JUMP + +_syscall_return: + SWAP1 + EXIT_KERNEL diff --git a/evm/src/cpu/kernel/interpreter.rs b/evm/src/cpu/kernel/interpreter.rs index 8347d896..f48e6a8c 100644 --- a/evm/src/cpu/kernel/interpreter.rs +++ b/evm/src/cpu/kernel/interpreter.rs @@ -558,7 +558,11 @@ impl<'a> Interpreter<'a> { fn run_shl(&mut self) { let shift = self.pop(); let value = self.pop(); - self.push(value << shift); + self.push(if shift < U256::from(256usize) { + value << shift + } else { + U256::zero() + }); } fn run_shr(&mut self) { diff --git a/evm/src/cpu/kernel/tests/mod.rs b/evm/src/cpu/kernel/tests/mod.rs index 68695e2f..83ded791 100644 --- a/evm/src/cpu/kernel/tests/mod.rs +++ b/evm/src/cpu/kernel/tests/mod.rs @@ -10,6 +10,7 @@ mod hash; mod mpt; mod packing; mod rlp; +mod signed_syscalls; mod transaction_parsing; use std::str::FromStr; diff --git a/evm/src/cpu/kernel/tests/signed_syscalls.rs b/evm/src/cpu/kernel/tests/signed_syscalls.rs new file mode 100644 index 00000000..728d5565 --- /dev/null +++ b/evm/src/cpu/kernel/tests/signed_syscalls.rs @@ -0,0 +1,166 @@ +use ethereum_types::U256; + +use crate::cpu::kernel::aggregator::KERNEL; +use crate::cpu::kernel::interpreter::Interpreter; + +/// Generate a list of inputs suitable for testing the signed operations +/// +/// The result includes 0, ±1, ±2^(16i ± 1) for i = 0..15, and ±2^255 +/// and then each of those ±1. Little attempt has been made to avoid +/// duplicates. Total length is 279. +fn test_inputs() -> Vec { + let mut res = vec![U256::zero()]; + for i in 1..16 { + res.push(U256::one() << (16 * i)); + res.push(U256::one() << (16 * i + 1)); + res.push(U256::one() << (16 * i - 1)); + } + res.push(U256::one() << 255); + + let n = res.len(); + for i in 1..n { + // push -res[i] + res.push(res[i].overflowing_neg().0); + } + + let n = res.len(); + for i in 0..n { + res.push(res[i].overflowing_add(U256::one()).0); + res.push(res[i].overflowing_sub(U256::one()).0); + } + + res +} + +// U256_TOP_BIT == 2^255. +const U256_TOP_BIT: U256 = U256([0x0, 0x0, 0x0, 0x8000000000000000]); + +/// Given a U256 `value`, interpret as a signed 256-bit number and +/// return the arithmetic right shift of `value` by `shift` bit +/// positions, i.e. the right shift of `value` with sign extension. +fn u256_sar(shift: U256, value: U256) -> U256 { + // Reference: Hacker's Delight, 2013, 2nd edition, §2-7. + let shift = shift.min(U256::from(255)); + ((value ^ U256_TOP_BIT) >> shift) + .overflowing_sub(U256_TOP_BIT >> shift) + .0 +} + +/// Given a U256 x, interpret it as a signed 256-bit number and return +/// the pair abs(x) and sign(x), where sign(x) = 1 if x < 0, and 0 +/// otherwise. NB: abs(x) is interpreted as an unsigned value, so +/// u256_abs_sgn(-2^255) = (2^255, -1). +fn u256_abs_sgn(x: U256) -> (U256, bool) { + let is_neg = x.bit(255); + + // negate x if it's negative + let x = if is_neg { x.overflowing_neg().0 } else { x }; + (x, is_neg) +} + +fn u256_sdiv(x: U256, y: U256) -> U256 { + let (abs_x, x_is_neg) = u256_abs_sgn(x); + let (abs_y, y_is_neg) = u256_abs_sgn(y); + if y.is_zero() { + U256::zero() + } else { + let quot = abs_x / abs_y; + // negate the quotient if arguments had opposite signs + if x_is_neg != y_is_neg { + quot.overflowing_neg().0 + } else { + quot + } + } +} + +fn u256_smod(x: U256, y: U256) -> U256 { + let (abs_x, x_is_neg) = u256_abs_sgn(x); + let (abs_y, _) = u256_abs_sgn(y); + + if y.is_zero() { + U256::zero() + } else { + let rem = abs_x % abs_y; + // negate the remainder if dividend was negative + if x_is_neg { + rem.overflowing_neg().0 + } else { + rem + } + } +} + +// signextend is just a SHL followed by SAR. +fn u256_signextend(byte: U256, value: U256) -> U256 { + // byte = min(31, byte) + let byte: u32 = byte.min(U256::from(31)).try_into().unwrap(); + let bit_offset = 256 - 8 * (byte + 1); + u256_sar(U256::from(bit_offset), value << bit_offset) +} + +// Reference: Hacker's Delight, 2013, 2nd edition, §2-12. +fn u256_slt(x: U256, y: U256) -> U256 { + let top_bit: U256 = U256::one() << 255; + U256::from(((x ^ top_bit) < (y ^ top_bit)) as u32) +} + +fn u256_sgt(x: U256, y: U256) -> U256 { + u256_slt(y, x) +} + +fn run_test(fn_label: &str, expected_fn: fn(U256, U256) -> U256, opname: &str) { + let inputs = test_inputs(); + let fn_label = KERNEL.global_labels[fn_label]; + let retdest = U256::from(0xDEADBEEFu32); + + for &x in &inputs { + for &y in &inputs { + let stack = vec![retdest, y, x]; + let mut interpreter = Interpreter::new_with_kernel(fn_label, stack); + interpreter.run().unwrap(); + assert_eq!(interpreter.stack().len(), 1usize, "unexpected stack size"); + let output = interpreter.stack()[0]; + let expected_output = expected_fn(x, y); + assert_eq!( + output, expected_output, + "{opname}({x}, {y}): expected {expected_output} but got {output}" + ); + } + } +} + +#[test] +fn test_sdiv() { + // Double-check that the expected output calculation is correct in the special case. + let x = U256::one() << 255; // -2^255 + let y = U256::one().overflowing_neg().0; // -1 + assert_eq!(u256_sdiv(x, y), x); // SDIV(-2^255, -1) = -2^255. + + run_test("_sys_sdiv", u256_sdiv, "SDIV"); +} + +#[test] +fn test_smod() { + run_test("_sys_smod", u256_smod, "SMOD"); +} + +#[test] +fn test_signextend() { + run_test("_sys_signextend", u256_signextend, "SIGNEXTEND"); +} + +#[test] +fn test_sar() { + run_test("_sys_sar", u256_sar, "SAR"); +} + +#[test] +fn test_slt() { + run_test("_sys_slt", u256_slt, "SLT"); +} + +#[test] +fn test_sgt() { + run_test("_sys_sgt", u256_sgt, "SGT"); +}