Implement ext_ hashes for contracts (issue #5258) (#5326)

* Implement ext_ hashes for contracts (issue #5258)

* load cryto hash .wat from raw string literal instead of file

* update .wat contents for testing crypto hashes

* remove unnecessary 'static

* fix bug in input (call_indirect required 1+ at least it seems)

* no longer use scratch buffer for crypto hash functions

* improve doc comments of ext_ hash functions

* remove unnecessary comment in .wat test file

* add return value (const 0) to contract test to hopefully enable result buffer

* fix bug in contract assertion

* implement proper output_len in contract

* implement proper test for crypto hashes

* bump spec_version 238 -> 239

* fix COMPLEXITY description

* remove final invalid instances of scratch buffer from docs
This commit is contained in:
Hero Bird
2020-03-20 18:46:51 +01:00
committed by GitHub
parent 46458f4082
commit 017f218926
4 changed files with 390 additions and 1 deletions
+146
View File
@@ -2736,3 +2736,149 @@ fn get_runtime_storage() {
));
});
}
const CODE_CRYPTO_HASHES: &str = r#"
(module
(import "env" "ext_scratch_size" (func $ext_scratch_size (result i32)))
(import "env" "ext_scratch_read" (func $ext_scratch_read (param i32 i32 i32)))
(import "env" "ext_scratch_write" (func $ext_scratch_write (param i32 i32)))
(import "env" "ext_hash_sha2_256" (func $ext_hash_sha2_256 (param i32 i32 i32)))
(import "env" "ext_hash_keccak_256" (func $ext_hash_keccak_256 (param i32 i32 i32)))
(import "env" "ext_hash_blake2_256" (func $ext_hash_blake2_256 (param i32 i32 i32)))
(import "env" "ext_hash_blake2_128" (func $ext_hash_blake2_128 (param i32 i32 i32)))
(import "env" "ext_hash_twox_256" (func $ext_hash_twox_256 (param i32 i32 i32)))
(import "env" "ext_hash_twox_128" (func $ext_hash_twox_128 (param i32 i32 i32)))
(import "env" "ext_hash_twox_64" (func $ext_hash_twox_64 (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
(type $hash_fn_sig (func (param i32 i32 i32)))
(table 8 funcref)
(elem (i32.const 1)
$ext_hash_sha2_256
$ext_hash_keccak_256
$ext_hash_blake2_256
$ext_hash_blake2_128
$ext_hash_twox_256
$ext_hash_twox_128
$ext_hash_twox_64
)
(data (i32.const 1) "20202010201008") ;; Output sizes of the hashes in order in hex.
;; Not in use by the tests besides instantiating the contract.
(func (export "deploy"))
;; Called by the tests.
;;
;; The `call` function expects data in a certain format in the scratch
;; buffer.
;;
;; 1. The first byte encodes an identifier for the crypto hash function
;; under test. (*)
;; 2. The rest encodes the input data that is directly fed into the
;; crypto hash function chosen in 1.
;;
;; The `deploy` function then computes the chosen crypto hash function
;; given the input and puts the result back into the scratch buffer.
;; After contract execution the test driver then asserts that the returned
;; values are equal to the expected bytes for the input and chosen hash
;; function.
;;
;; (*) The possible value for the crypto hash identifiers can be found below:
;;
;; | value | Algorithm | Bit Width |
;; |-------|-----------|-----------|
;; | 0 | SHA2 | 256 |
;; | 1 | KECCAK | 256 |
;; | 2 | BLAKE2 | 256 |
;; | 3 | BLAKE2 | 128 |
;; | 4 | TWOX | 256 |
;; | 5 | TWOX | 128 |
;; | 6 | TWOX | 64 |
;; ---------------------------------
(func (export "call") (result i32)
(local $chosen_hash_fn i32)
(local $input_ptr i32)
(local $input_len i32)
(local $output_ptr i32)
(local $output_len i32)
(local.set $input_ptr (i32.const 10))
(call $ext_scratch_read (local.get $input_ptr) (i32.const 0) (call $ext_scratch_size))
(local.set $chosen_hash_fn (i32.load8_u (local.get $input_ptr)))
(if (i32.gt_u (local.get $chosen_hash_fn) (i32.const 7))
;; We check that the chosen hash fn identifier is within bounds: [0,7]
(unreachable)
)
(local.set $input_ptr (i32.add (local.get $input_ptr) (i32.const 1)))
(local.set $input_len (i32.sub (call $ext_scratch_size) (i32.const 1)))
(local.set $output_ptr (i32.const 100))
(local.set $output_len (i32.load8_u (local.get $chosen_hash_fn)))
(call_indirect (type $hash_fn_sig)
(local.get $input_ptr)
(local.get $input_len)
(local.get $output_ptr)
(local.get $chosen_hash_fn) ;; Which crypto hash function to execute.
)
(call $ext_scratch_write
(local.get $output_ptr) ;; Linear memory location of the output buffer.
(local.get $output_len) ;; Number of output buffer bytes.
)
(i32.const 0)
)
)
"#;
#[test]
fn crypto_hashes() {
let (wasm, code_hash) = compile_module::<Test>(&CODE_CRYPTO_HASHES).unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contracts::put_code(Origin::signed(ALICE), 100_000, wasm));
// Instantiate the CRYPTO_HASHES contract.
assert_ok!(Contracts::instantiate(
Origin::signed(ALICE),
100_000,
100_000,
code_hash.into(),
vec![],
));
// Perform the call.
let input = b"_DEAD_BEEF";
use sp_io::hashing::*;
// Wraps a hash function into a more dynamic form usable for testing.
macro_rules! dyn_hash_fn {
($name:ident) => {
Box::new(|input| $name(input).as_ref().to_vec().into_boxed_slice())
};
}
// All hash functions and their associated output byte lengths.
let test_cases: &[(Box<dyn Fn(&[u8]) -> Box<[u8]>>, usize)] = &[
(dyn_hash_fn!(sha2_256), 32),
(dyn_hash_fn!(keccak_256), 32),
(dyn_hash_fn!(blake2_256), 32),
(dyn_hash_fn!(blake2_128), 16),
(dyn_hash_fn!(twox_256), 32),
(dyn_hash_fn!(twox_128), 16),
(dyn_hash_fn!(twox_64), 8),
];
// Test the given hash functions for the input: "_DEAD_BEEF"
for (n, (hash_fn, expected_size)) in test_cases.iter().enumerate() {
// We offset data in the contract tables by 1.
let mut params = vec![(n + 1) as u8];
params.extend_from_slice(input);
let result = <Module<Test>>::bare_call(
ALICE,
BOB,
0,
100_000,
params,
).unwrap();
assert_eq!(result.status, 0);
let expected = hash_fn(input.as_ref());
assert_eq!(&result.data[..*expected_size], &*expected);
}
})
}
@@ -26,6 +26,15 @@ use frame_system;
use sp_std::{prelude::*, mem, convert::TryInto};
use codec::{Decode, Encode};
use sp_runtime::traits::{Bounded, SaturatedConversion};
use sp_io::hashing::{
keccak_256,
blake2_256,
blake2_128,
twox_256,
twox_128,
twox_64,
sha2_256,
};
/// The value returned from ext_call and ext_instantiate contract external functions if the call or
/// instantiation traps. This value is chosen as if the execution does not trap, the return value
@@ -1013,8 +1022,217 @@ define_env!(Env, <E: Ext>,
}
}
},
// Computes the SHA2 256-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 32 bytes (256 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_sha2_256(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, sha2_256, input_ptr, input_len, output_ptr)
},
// Computes the KECCAK 256-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 32 bytes (256 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_keccak_256(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, keccak_256, input_ptr, input_len, output_ptr)
},
// Computes the BLAKE2 256-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 32 bytes (256 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_blake2_256(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, blake2_256, input_ptr, input_len, output_ptr)
},
// Computes the BLAKE2 128-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 16 bytes (128 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_blake2_128(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, blake2_128, input_ptr, input_len, output_ptr)
},
// Computes the TWOX 256-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 32 bytes (256 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_twox_256(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, twox_256, input_ptr, input_len, output_ptr)
},
// Computes the TWOX 128-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 16 bytes (128 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_twox_128(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, twox_128, input_ptr, input_len, output_ptr)
},
// Computes the TWOX 64-bit hash on the given input buffer.
//
// Returns the result directly into the given output buffer.
//
// # Note
//
// - The `input` and `output` buffer may overlap.
// - The output buffer is expected to hold at least 8 bytes (64 bits).
// - It is the callers responsibility to provide an output buffer that
// is large enough to hold the expected amount of bytes returned by the
// chosen hash function.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input
// data is placed.
// - `input_len`: the length of the input data in bytes.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The function will write the result
// directly into this buffer.
ext_hash_twox_64(ctx, input_ptr: u32, input_len: u32, output_ptr: u32) => {
compute_hash_on_intermediate_buffer(ctx, twox_64, input_ptr, input_len, output_ptr)
},
);
/// Computes the given hash function on the scratch buffer.
///
/// Reads from the sandboxed input buffer into an intermediate buffer.
/// Returns the result directly to the output buffer of the sandboxed memory.
///
/// It is the callers responsibility to provide an output buffer that
/// is large enough to hold the expected amount of bytes returned by the
/// chosen hash function.
///
/// # Note
///
/// The `input` and `output` buffers may overlap.
fn compute_hash_on_intermediate_buffer<E, F, R>(
ctx: &mut Runtime<E>,
hash_fn: F,
input_ptr: u32,
input_len: u32,
output_ptr: u32,
) -> Result<(), sp_sandbox::HostError>
where
E: Ext,
F: FnOnce(&[u8]) -> R,
R: AsRef<[u8]>,
{
// Copy the input buffer directly into the scratch buffer to avoid
// heap allocations.
let input = read_sandbox_memory(ctx, input_ptr, input_len)?;
// Compute the hash on the scratch buffer using the given hash function.
let hash = hash_fn(&input);
// Write the resulting hash back into the sandboxed output buffer.
write_sandbox_memory(
ctx.schedule,
&mut ctx.special_trap,
ctx.gas_meter,
&ctx.memory,
output_ptr,
hash.as_ref(),
)?;
Ok(())
}
/// Finds duplicates in a given vector.
///
/// This function has complexity of O(n log n) and no additional memory is required, although