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.
This commit is contained in:
Hamish Ivey-Law 2023-03-30 05:56:01 +11:00 committed by GitHub
parent 834522a653
commit 9480cbed99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 416 additions and 14 deletions

View File

@ -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"),

View File

@ -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:

View File

@ -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

View File

@ -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 <s y iff (x ^ sign_bit) <u (y ^ sign bit),
// where <s is signed comparison and <u is unsigned comparison.
// Reference: Hacker's Delight, 2013, 2nd edition, §2-12.
global _sys_slt:
// Stack: lhs, rhs, return_info
%add_const(0x8000000000000000000000000000000000000000000000000000000000000000)
// Stack: 2^255 + lhs, rhs, return_info
SWAP1
%add_const(0x8000000000000000000000000000000000000000000000000000000000000000)
// Stack: 2^255 + rhs, 2^255 + lhs, return_info
GT
// Stack: 2^255 + lhs < 2^255 + rhs, return_info
SWAP1
JUMP
/// These are the global entry-points for the signed system
/// calls. They just delegate to a subroutine with the same name
/// preceded by an underscore.
///
/// NB: The only reason to structure things this way is so that the
/// test suite can call the _sys_opcode versions, since the test_suite
/// uses our interpreter which doesn't handle `EXIT_KERNEL` in a way
/// that allows for easy testing. The cost is two extra JUMPs per call.
global sys_sdiv:
%charge_gas_const(@GAS_LOW)
%stack(x, y, kernel_return) -> (_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

View File

@ -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) {

View File

@ -10,6 +10,7 @@ mod hash;
mod mpt;
mod packing;
mod rlp;
mod signed_syscalls;
mod transaction_parsing;
use std::str::FromStr;

View File

@ -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<U256> {
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");
}