-
Notifications
You must be signed in to change notification settings - Fork 546
Fix padding and gas cost computation issues in precompiles #961
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
Changes from all commits
57974fb
b9ca007
4d2ac73
fb5d3d9
d57800b
922fda0
1ef1d6d
634812a
b9703c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,8 +20,8 @@ | |
|
|
||
| extern crate alloc; | ||
|
|
||
| use alloc::vec::Vec; | ||
| use core::{cmp::max, ops::BitAnd}; | ||
| use alloc::{vec, vec::Vec}; | ||
| use core::cmp::max; | ||
|
|
||
| use num::{BigUint, FromPrimitive, One, ToPrimitive, Zero}; | ||
|
|
||
|
|
@@ -38,9 +38,9 @@ const MIN_GAS_COST: u64 = 200; | |
| // https://eips.ethereum.org/EIPS/eip-2565 | ||
| fn calculate_gas_cost( | ||
| base_length: u64, | ||
| exp_length: u64, | ||
| mod_length: u64, | ||
| exponent: &BigUint, | ||
| exponent_bytes: &[u8], | ||
| ) -> u64 { | ||
| fn calculate_multiplication_complexity(base_length: u64, mod_length: u64) -> u64 { | ||
| let max_length = max(base_length, mod_length); | ||
|
|
@@ -56,18 +56,15 @@ fn calculate_gas_cost( | |
| words * words | ||
| } | ||
|
|
||
| fn calculate_iteration_count(exp_length: u64, exponent: &BigUint) -> u64 { | ||
| fn calculate_iteration_count(exponent: &BigUint, exponent_bytes: &[u8]) -> u64 { | ||
| let mut iteration_count: u64 = 0; | ||
| let exp_length = exponent_bytes.len() as u64; | ||
|
|
||
| if exp_length <= 32 && exponent.is_zero() { | ||
| iteration_count = 0; | ||
| } else if exp_length <= 32 { | ||
| iteration_count = exponent.bits() - 1; | ||
| } else if exp_length > 32 { | ||
| // construct BigUint to represent (2^256) - 1 | ||
| let bytes: [u8; 32] = [0xFF; 32]; | ||
| let max_256_bit_uint = BigUint::from_bytes_be(&bytes); | ||
|
|
||
| // from the EIP spec: | ||
| // (8 * (exp_length - 32)) + ((exponent & (2**256 - 1)).bit_length() - 1) | ||
| // | ||
|
|
@@ -77,21 +74,42 @@ fn calculate_gas_cost( | |
| // must be > 0) | ||
| // * the addition can't overflow because the terms are both capped at roughly | ||
| // 8 * max size of exp_length (1024) | ||
| iteration_count = | ||
| (8 * (exp_length - 32)) + exponent.bitand(max_256_bit_uint).bits() - 1; | ||
| // * the EIP spec is written in python, in which (exponent & (2**256 - 1)) takes the | ||
| // FIRST 32 bytes. However this `BigUint` `&` operator takes the LAST 32 bytes. | ||
| // We thus instead take the bytes manually. | ||
| let exponent_head = BigUint::from_bytes_be(&exponent_bytes[..32]); | ||
|
|
||
| iteration_count = (8 * (exp_length - 32)) + exponent_head.bits() - 1; | ||
| } | ||
|
|
||
| max(iteration_count, 1) | ||
| } | ||
|
|
||
| let multiplication_complexity = calculate_multiplication_complexity(base_length, mod_length); | ||
| let iteration_count = calculate_iteration_count(exp_length, exponent); | ||
| let iteration_count = calculate_iteration_count(exponent, exponent_bytes); | ||
| max( | ||
| MIN_GAS_COST, | ||
| multiplication_complexity * iteration_count / 3, | ||
| ) | ||
| } | ||
|
|
||
| /// Copy bytes from input to target. | ||
| fn read_input(source: &[u8], target: &mut [u8], source_offset: &mut usize) { | ||
| // We move the offset by the len of the target, regardless of what we | ||
| // actually copy. | ||
| let offset = *source_offset; | ||
| *source_offset += target.len(); | ||
|
|
||
| // Out of bounds, nothing to copy. | ||
| if source.len() <= offset { | ||
| return; | ||
| } | ||
|
|
||
| // Find len to copy up to target len, but not out of bounds. | ||
| let len = core::cmp::min(target.len(), source.len() - offset); | ||
| target[..len].copy_from_slice(&source[offset..][..len]); | ||
| } | ||
|
|
||
| // ModExp expects the following as inputs: | ||
| // 1) 32 bytes expressing the length of base | ||
| // 2) 32 bytes expressing the length of exponent | ||
|
|
@@ -111,35 +129,35 @@ fn calculate_gas_cost( | |
| impl Precompile for Modexp { | ||
| fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult { | ||
| let input = handle.input(); | ||
| let mut input_offset = 0; | ||
|
|
||
| if input.len() < 96 { | ||
| return Err(PrecompileFailure::Error { | ||
| exit_status: ExitError::Other("input must contain at least 96 bytes".into()), | ||
| }); | ||
| }; | ||
| // Yellowpaper: whenever the input is too short, the missing bytes are | ||
| // considered to be zero. | ||
| let mut base_len_buf = [0u8; 32]; | ||
| read_input(input, &mut base_len_buf, &mut input_offset); | ||
| let mut exp_len_buf = [0u8; 32]; | ||
| read_input(input, &mut exp_len_buf, &mut input_offset); | ||
| let mut mod_len_buf = [0u8; 32]; | ||
| read_input(input, &mut mod_len_buf, &mut input_offset); | ||
|
|
||
| // reasonable assumption: this must fit within the Ethereum EVM's max stack size | ||
| let max_size_big = BigUint::from_u32(1024).expect("can't create BigUint"); | ||
|
|
||
| let mut buf = [0; 32]; | ||
| buf.copy_from_slice(&input[0..32]); | ||
| let base_len_big = BigUint::from_bytes_be(&buf); | ||
| let base_len_big = BigUint::from_bytes_be(&base_len_buf); | ||
| if base_len_big > max_size_big { | ||
| return Err(PrecompileFailure::Error { | ||
| exit_status: ExitError::Other("unreasonably large base length".into()), | ||
| }); | ||
| } | ||
|
|
||
| buf.copy_from_slice(&input[32..64]); | ||
| let exp_len_big = BigUint::from_bytes_be(&buf); | ||
| let exp_len_big = BigUint::from_bytes_be(&exp_len_buf); | ||
| if exp_len_big > max_size_big { | ||
| return Err(PrecompileFailure::Error { | ||
| exit_status: ExitError::Other("unreasonably large exponent length".into()), | ||
| }); | ||
| } | ||
|
|
||
| buf.copy_from_slice(&input[64..96]); | ||
| let mod_len_big = BigUint::from_bytes_be(&buf); | ||
| let mod_len_big = BigUint::from_bytes_be(&mod_len_buf); | ||
| if mod_len_big > max_size_big { | ||
| return Err(PrecompileFailure::Error { | ||
| exit_status: ExitError::Other("unreasonably large modulus length".into()), | ||
|
|
@@ -151,11 +169,11 @@ impl Precompile for Modexp { | |
| let exp_len = exp_len_big.to_usize().expect("exp_len out of bounds"); | ||
| let mod_len = mod_len_big.to_usize().expect("mod_len out of bounds"); | ||
|
|
||
| // input length should be at least 96 + user-specified length of base + exp + mod | ||
| let total_len = base_len + exp_len + mod_len + 96; | ||
| if input.len() < total_len { | ||
| return Err(PrecompileFailure::Error { | ||
| exit_status: ExitError::Other("insufficient input size".into()), | ||
| // if mod_len is 0 output must be empty | ||
| if mod_len == 0 { | ||
| return Ok(PrecompileOutput { | ||
| exit_status: ExitSucceed::Returned, | ||
| output: vec![], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually right? EIP-198 states:
So if I'm to understand the spec, this should be a number of zeros, instead of an empty array. Also is this special case actually needed? L166 already has:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Base len is encoded before mod len, so a truncated input could have non-zero base len but zero mod len. As the EIP states that it returns a byte array with the same length as the modulus, then if mod len is 0 the output should be zero len too. I'm not too familiar with Go but this seems to return zero len array: |
||
| }); | ||
| } | ||
|
|
||
|
|
@@ -165,21 +183,22 @@ impl Precompile for Modexp { | |
| BigUint::zero() | ||
| } else { | ||
| // read the numbers themselves. | ||
| let base_start = 96; // previous 3 32-byte fields | ||
| let base = BigUint::from_bytes_be(&input[base_start..base_start + base_len]); | ||
| let mut base_buf = vec![0u8; base_len]; | ||
| read_input(input, &mut base_buf, &mut input_offset); | ||
| let base = BigUint::from_bytes_be(&base_buf); | ||
|
|
||
| let exp_start = base_start + base_len; | ||
| let exponent = BigUint::from_bytes_be(&input[exp_start..exp_start + exp_len]); | ||
| let mut exp_buf = vec![0u8; exp_len]; | ||
| read_input(input, &mut exp_buf, &mut input_offset); | ||
| let exponent = BigUint::from_bytes_be(&exp_buf); | ||
|
|
||
| let mut mod_buf = vec![0u8; mod_len]; | ||
| read_input(input, &mut mod_buf, &mut input_offset); | ||
| let modulus = BigUint::from_bytes_be(&mod_buf); | ||
|
|
||
| // do our gas accounting | ||
| let gas_cost = | ||
| calculate_gas_cost(base_len as u64, exp_len as u64, mod_len as u64, &exponent); | ||
| let gas_cost = calculate_gas_cost(base_len as u64, mod_len as u64, &exponent, &exp_buf); | ||
|
|
||
| handle.record_cost(gas_cost)?; | ||
| let input = handle.input(); | ||
|
|
||
| let mod_start = exp_start + exp_len; | ||
| let modulus = BigUint::from_bytes_be(&input[mod_start..mod_start + mod_len]); | ||
|
|
||
| if modulus.is_zero() || modulus.is_one() { | ||
| BigUint::zero() | ||
|
|
@@ -228,7 +247,7 @@ mod tests { | |
| } | ||
|
|
||
| #[test] | ||
| fn test_empty_input() -> Result<(), PrecompileFailure> { | ||
| fn test_empty_input() { | ||
| let input = Vec::new(); | ||
|
|
||
| let cost: u64 = 1; | ||
|
|
@@ -242,25 +261,17 @@ mod tests { | |
| let mut handle = MockHandle::new(input, Some(cost), context); | ||
|
|
||
| match Modexp::execute(&mut handle) { | ||
| Ok(_) => { | ||
| panic!("Test not expected to pass"); | ||
| Ok(precompile_result) => { | ||
| assert_eq!(precompile_result.output.len(), 0); | ||
| } | ||
| Err(e) => { | ||
| assert_eq!( | ||
| e, | ||
| PrecompileFailure::Error { | ||
| exit_status: ExitError::Other( | ||
| "input must contain at least 96 bytes".into() | ||
| ) | ||
| } | ||
| ); | ||
| Ok(()) | ||
| Err(_) => { | ||
| panic!("Modexp::execute() returned error"); // TODO: how to pass error on? | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_insufficient_input() -> Result<(), PrecompileFailure> { | ||
| fn test_insufficient_input() { | ||
| let input = hex::decode( | ||
| "0000000000000000000000000000000000000000000000000000000000000001\ | ||
| 0000000000000000000000000000000000000000000000000000000000000001\ | ||
|
|
@@ -279,17 +290,12 @@ mod tests { | |
| let mut handle = MockHandle::new(input, Some(cost), context); | ||
|
|
||
| match Modexp::execute(&mut handle) { | ||
| Ok(_) => { | ||
| panic!("Test not expected to pass"); | ||
| Ok(precompile_result) => { | ||
| assert_eq!(precompile_result.output.len(), 1); | ||
| assert_eq!(precompile_result.output, vec![0x00]); | ||
| } | ||
| Err(e) => { | ||
| assert_eq!( | ||
| e, | ||
| PrecompileFailure::Error { | ||
| exit_status: ExitError::Other("insufficient input size".into()) | ||
| } | ||
| ); | ||
| Ok(()) | ||
| Err(_) => { | ||
| panic!("Modexp::execute() returned error"); // TODO: how to pass error on? | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -474,4 +480,36 @@ mod tests { | |
| let expected = BigUint::parse_bytes(b"0", 10).unwrap(); | ||
| assert_eq!(result, expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_long_exp_gas_cost_matches_specs() { | ||
| let input = vec![ | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 16, 0, 0, 0, 255, 255, 255, 2, 0, 0, 179, 0, 0, 2, 0, 0, 122, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 255, 251, 0, 0, 0, 0, 4, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 255, 255, 255, 2, 0, 0, 179, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, | ||
| 255, 255, 255, 249, | ||
| ]; | ||
|
|
||
| let context: Context = Context { | ||
| address: Default::default(), | ||
| caller: Default::default(), | ||
| apparent_value: From::from(0), | ||
| }; | ||
|
|
||
| let mut handle = MockHandle::new(input, Some(100_000), context); | ||
|
|
||
| let _ = Modexp::execute(&mut handle).expect("Modexp::execute() returned error"); | ||
|
|
||
| assert_eq!(handle.gas_used, 7104); // gas used when ran in geth | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.