mirror of
https://github.com/logos-storage/plonky2.git
synced 2026-01-05 07:13:08 +00:00
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:
parent
834522a653
commit
9480cbed99
@ -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"),
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
216
evm/src/cpu/kernel/asm/signed.asm
Normal file
216
evm/src/cpu/kernel/asm/signed.asm
Normal 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
|
||||
@ -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) {
|
||||
|
||||
@ -10,6 +10,7 @@ mod hash;
|
||||
mod mpt;
|
||||
mod packing;
|
||||
mod rlp;
|
||||
mod signed_syscalls;
|
||||
mod transaction_parsing;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
166
evm/src/cpu/kernel/tests/signed_syscalls.rs
Normal file
166
evm/src/cpu/kernel/tests/signed_syscalls.rs
Normal 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");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user