Integrate benchmarks and differential tests against an EVM interpreter (#7)

This commit is contained in:
Cyrill Leutwiler
2024-04-24 18:51:19 +02:00
parent bd10742ef8
commit df8ebb61ec
27 changed files with 1567 additions and 383 deletions
+119
View File
@@ -0,0 +1,119 @@
use alloy_primitives::U256;
use alloy_sol_types::{sol, SolCall};
#[derive(Clone)]
pub struct Contract {
pub evm_runtime: Vec<u8>,
pub pvm_runtime: Vec<u8>,
pub calldata: Vec<u8>,
}
sol!(contract Baseline { function baseline() public payable; });
sol!(contract Computation {
function odd_product(int32 n) public pure returns (int64);
function triangle_number(int64 n) public pure returns (int64 sum);
});
sol!(
contract FibonacciRecursive {
function fib3(uint n) public pure returns (uint);
}
);
sol!(
contract FibonacciIterative {
function fib3(uint n) external pure returns (uint b);
}
);
sol!(
contract FibonacciBinet {
function fib3(uint n) external pure returns (uint a);
}
);
sol!(
contract SHA1 {
function sha1(bytes memory data) public pure returns (bytes20 ret);
}
);
impl Contract {
pub fn baseline() -> Self {
let code = include_str!("../contracts/Baseline.sol");
let name = "Baseline";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: Baseline::baselineCall::new(()).abi_encode(),
}
}
pub fn odd_product(n: i32) -> Self {
let code = include_str!("../contracts/Computation.sol");
let name = "Computation";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: Computation::odd_productCall::new((n,)).abi_encode(),
}
}
pub fn triangle_number(n: i64) -> Self {
let code = include_str!("../contracts/Computation.sol");
let name = "Computation";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: Computation::triangle_numberCall::new((n,)).abi_encode(),
}
}
pub fn fib_recursive(n: u32) -> Self {
let code = include_str!("../contracts/Fibonacci.sol");
let name = "FibonacciRecursive";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: FibonacciRecursive::fib3Call::new((U256::from(n),)).abi_encode(),
}
}
pub fn fib_iterative(n: u32) -> Self {
let code = include_str!("../contracts/Fibonacci.sol");
let name = "FibonacciIterative";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: FibonacciIterative::fib3Call::new((U256::from(n),)).abi_encode(),
}
}
pub fn fib_binet(n: u32) -> Self {
let code = include_str!("../contracts/Fibonacci.sol");
let name = "FibonacciBinet";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: FibonacciBinet::fib3Call::new((U256::from(n),)).abi_encode(),
}
}
pub fn sha1(pre: Vec<u8>) -> Self {
let code = include_str!("../contracts/SHA1.sol");
let name = "SHA1";
Self {
evm_runtime: crate::compile_evm_bin_runtime(name, code),
pvm_runtime: crate::compile_blob(name, code),
calldata: SHA1::sha1Call::new((pre,)).abi_encode(),
}
}
}
+44 -318
View File
@@ -1,9 +1,42 @@
use cases::Contract;
use mock_runtime::State;
pub mod cases;
pub mod mock_runtime;
#[cfg(test)]
mod tests;
/// Compile the blob of `contract_name` found in given `source_code`.
/// The `solc` optimizer will be enabled
pub fn compile_blob(contract_name: &str, source_code: &str) -> Vec<u8> {
compile_blob_with_options(contract_name, source_code, true)
compile_blob_with_options(
contract_name,
source_code,
true,
revive_solidity::SolcPipeline::Yul,
)
}
/// Compile the EVM bin-runtime of `contract_name` found in given `source_code`.
/// The `solc` optimizer will be enabled
pub fn compile_evm_bin_runtime(contract_name: &str, source_code: &str) -> Vec<u8> {
let file_name = "contract.sol";
let contracts = revive_solidity::test_utils::build_solidity_with_options_evm(
[(file_name.into(), source_code.into())].into(),
Default::default(),
None,
revive_solidity::SolcPipeline::Yul,
true,
)
.expect("source should compile");
let bin_runtime = &contracts
.get(contract_name)
.unwrap_or_else(|| panic!("contract '{}' didn't produce bin-runtime", contract_name))
.object;
hex::decode(bin_runtime).expect("bin-runtime shold be hex encoded")
}
/// Compile the blob of `contract_name` found in given `source_code`.
@@ -11,6 +44,7 @@ pub fn compile_blob_with_options(
contract_name: &str,
source_code: &str,
solc_optimizer_enabled: bool,
pipeline: revive_solidity::SolcPipeline,
) -> Vec<u8> {
let file_name = "contract.sol";
@@ -18,7 +52,7 @@ pub fn compile_blob_with_options(
[(file_name.into(), source_code.into())].into(),
Default::default(),
None,
revive_solidity::SolcPipeline::Yul,
pipeline,
era_compiler_llvm_context::OptimizerSettings::cycles(),
solc_optimizer_enabled,
)
@@ -37,323 +71,15 @@ pub fn compile_blob_with_options(
hex::decode(bytecode).expect("hex encoding should always be valid")
}
#[cfg(test)]
mod tests {
use alloy_primitives::{FixedBytes, Keccak256, I256, U256};
use alloy_sol_types::{sol, SolCall};
use sha1::Digest;
pub fn assert_success(contract: Contract, differential: bool) -> State {
let (mut instance, export) = mock_runtime::prepare(&contract.pvm_runtime, None);
let state = mock_runtime::call(State::new(contract.calldata.clone()), &mut instance, export);
assert_eq!(state.output.flags, 0);
use crate::mock_runtime::{self, State};
#[test]
fn fibonacci() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract Fibonacci {
function fib3(uint n) public pure returns (uint);
}
);
for contract in ["FibonacciIterative", "FibonacciRecursive", "FibonacciBinet"] {
let code = crate::compile_blob(contract, include_str!("../contracts/Fibonacci.sol"));
let parameter = U256::from(6);
let input = Fibonacci::fib3Call::new((parameter,)).abi_encode();
let state = State::new(input);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
let expected = U256::from(8);
assert_eq!(received, expected);
}
if differential {
let evm = revive_differential::prepare(contract.evm_runtime, contract.calldata);
assert_eq!(state.output.data.clone(), revive_differential::execute(evm));
}
#[test]
fn flipper() {
let code = crate::compile_blob("Flipper", include_str!("../contracts/flipper.sol"));
let state = State::new(0xcde4efa9u32.to_be_bytes().to_vec());
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
assert_eq!(state.storage[&U256::ZERO], U256::try_from(1).unwrap());
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
assert_eq!(state.storage[&U256::ZERO], U256::ZERO);
}
#[test]
fn hash_keccak_256() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract TestSha3 {
function test(string memory _pre) external payable returns (bytes32);
}
);
let source = r#"contract TestSha3 {
function test(string memory _pre) external payable returns (bytes32 hash) {
hash = keccak256(bytes(_pre));
}
}"#;
let code = crate::compile_blob("TestSha3", source);
let param = "hello";
let input = TestSha3::testCall::new((param.to_string(),)).abi_encode();
let state = State::new(input);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let mut hasher = Keccak256::new();
hasher.update(param);
let expected = hasher.finalize();
let received = FixedBytes::<32>::from_slice(&state.output.data);
assert_eq!(received, expected);
}
#[test]
fn erc20() {
let _ = crate::compile_blob("ERC20", include_str!("../contracts/ERC20.sol"));
}
#[test]
fn triangle_number() {
let code = crate::compile_blob("Computation", include_str!("../contracts/Computation.sol"));
let param = U256::try_from(13).unwrap();
let expected = U256::try_from(91).unwrap();
// function triangle_number(int64)
let mut input = 0x0f760610u32.to_be_bytes().to_vec();
input.extend_from_slice(&param.to_be_bytes::<32>());
let state = State::new(input);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn odd_product() {
let code = crate::compile_blob("Computation", include_str!("../contracts/Computation.sol"));
let param = I256::try_from(5i32).unwrap();
let expected = I256::try_from(945i64).unwrap();
// function odd_product(int32)
let mut input = 0x00261b66u32.to_be_bytes().to_vec();
input.extend_from_slice(&param.to_be_bytes::<32>());
let state = State::new(input);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let received = I256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn msize_plain() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract MSize {
function mSize() public pure returns (uint);
}
);
let code = crate::compile_blob_with_options(
"MSize",
include_str!("../contracts/MSize.sol"),
false,
);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let input = MSize::mSizeCall::new(()).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
// Solidity always stores the "free memory pointer" (32 byte int) at offset 64.
let expected = U256::try_from(64 + 32).unwrap();
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn transferred_value() {
sol!(
contract Value {
function value() public payable returns (uint);
}
);
let code = crate::compile_blob("Value", include_str!("../contracts/Value.sol"));
let mut state = State::new(Value::valueCall::SELECTOR.to_vec());
state.value = 0x1;
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let expected = I256::try_from(state.value).unwrap();
let received = I256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn msize_non_word_sized_access() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract MSize {
function mStore100() public pure returns (uint);
}
);
let code = crate::compile_blob_with_options(
"MSize",
include_str!("../contracts/MSize.sol"),
false,
);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let input = MSize::mStore100Call::new(()).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
// https://docs.zksync.io/build/developer-reference/differences-with-ethereum.html#mstore-mload
// "Unlike EVM, where the memory growth is in words, on zkEVM the memory growth is counted in bytes."
// "For example, if you write mstore(100, 0) the msize on zkEVM will be 132, but on the EVM it will be 160."
let expected = U256::try_from(132).unwrap();
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn mstore8() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract MStore8 {
function mStore8(uint value) public pure returns (uint256 word);
}
);
let code = crate::compile_blob_with_options(
"MStore8",
include_str!("../contracts/mStore8.sol"),
false,
);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let mut assert = |parameter, expected| {
let input = MStore8::mStore8Call::new((parameter,)).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
};
for (parameter, expected) in [
(U256::MIN, U256::MIN),
(
U256::from(1),
U256::from_str_radix(
"452312848583266388373324160190187140051835877600158453279131187530910662656",
10,
)
.unwrap(),
),
(
U256::from(2),
U256::from_str_radix(
"904625697166532776746648320380374280103671755200316906558262375061821325312",
10,
)
.unwrap(),
),
(
U256::from(255),
U256::from_str_radix(
"115339776388732929035197660848497720713218148788040405586178452820382218977280",
10,
)
.unwrap(),
),
(
U256::from(256),
U256::from(0),
),
(
U256::from(257),
U256::from_str_radix(
"452312848583266388373324160190187140051835877600158453279131187530910662656",
10,
)
.unwrap(),
),
(
U256::from(258),
U256::from_str_radix(
"904625697166532776746648320380374280103671755200316906558262375061821325312",
10,
)
.unwrap(),
),
(
U256::from(123456789),
U256::from_str_radix(
"9498569820248594155839807363993929941088553429603327518861754938149123915776",
10,
)
.unwrap(),
),
(
U256::MAX,
U256::from_str_radix(
"115339776388732929035197660848497720713218148788040405586178452820382218977280",
10,
)
.unwrap(),
),
] {
assert(parameter, expected);
}
}
#[test]
fn sha1() {
sol!(
contract SHA1 {
function sha1(bytes memory data) public pure returns (bytes20);
}
);
let code =
crate::compile_blob_with_options("SHA1", include_str!("../contracts/sha1.sol"), false);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let pre = vec![0xffu8; 512];
let mut hasher = sha1::Sha1::new();
hasher.update(&pre);
let hash = hasher.finalize();
let input = SHA1::sha1Call::new((pre,)).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
let expected = FixedBytes::<20>::from_slice(&hash[..]);
let received = FixedBytes::<20>::from_slice(&state.output.data[..20]);
assert_eq!(received, expected);
}
state
}
+4 -7
View File
@@ -3,7 +3,6 @@
use std::collections::HashMap;
use alloy_primitives::{Keccak256, U256};
use parity_scale_codec::Encode;
use polkavm::{
Caller, Config, Engine, ExportIndex, GasMeteringKind, Instance, Linker, Module, ModuleConfig,
ProgramBlob, Trap,
@@ -61,7 +60,7 @@ fn link_host_functions(engine: &Engine) -> Linker<State> {
assert!(state.input.len() <= caller.read_u32(out_len_ptr).unwrap() as usize);
caller.write_memory(out_ptr, &state.input)?;
caller.write_memory(out_len_ptr, &(state.input.len() as u32).encode())?;
caller.write_memory(out_len_ptr, &(state.input.len() as u32).to_le_bytes())?;
Ok(())
},
@@ -91,7 +90,7 @@ fn link_host_functions(engine: &Engine) -> Linker<State> {
let value = state.value.to_le_bytes();
caller.write_memory(out_ptr, &value)?;
caller.write_memory(out_len_ptr, &(value.len() as u32).encode())?;
caller.write_memory(out_len_ptr, &(value.len() as u32).to_le_bytes())?;
Ok(())
},
@@ -200,14 +199,12 @@ pub fn recompile_code(code: &[u8], engine: &Engine) -> Module {
let mut module_config = ModuleConfig::new();
module_config.set_gas_metering(Some(GasMeteringKind::Sync));
Module::new(&engine, &module_config, code).unwrap()
Module::new(engine, &module_config, code).unwrap()
}
pub fn instantiate_module(module: &Module, engine: &Engine) -> (Instance<State>, ExportIndex) {
let export = module.lookup_export("call").unwrap();
let func = link_host_functions(&engine)
.instantiate_pre(module)
.unwrap();
let func = link_host_functions(engine).instantiate_pre(module).unwrap();
let instance = func.instantiate().unwrap();
(instance, export)
+267
View File
@@ -0,0 +1,267 @@
use alloy_primitives::{FixedBytes, Keccak256, I256, U256};
use alloy_sol_types::{sol, SolCall};
use sha1::Digest;
use crate::{
assert_success,
cases::Contract,
mock_runtime::{self, State},
};
#[test]
fn fibonacci() {
let parameter = 6;
for contract in [
Contract::fib_recursive(parameter),
Contract::fib_iterative(parameter),
Contract::fib_binet(parameter),
] {
let state = assert_success(contract, true);
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
let expected = U256::from(8);
assert_eq!(received, expected);
}
}
#[test]
fn flipper() {
let code = crate::compile_blob("Flipper", include_str!("../contracts/flipper.sol"));
let state = State::new(0xcde4efa9u32.to_be_bytes().to_vec());
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
assert_eq!(state.storage[&U256::ZERO], U256::try_from(1).unwrap());
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
assert_eq!(state.storage[&U256::ZERO], U256::ZERO);
}
#[test]
fn hash_keccak_256() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract TestSha3 {
function test(string memory _pre) external payable returns (bytes32);
}
);
let source = r#"contract TestSha3 {
function test(string memory _pre) external payable returns (bytes32 hash) {
hash = keccak256(bytes(_pre));
}
}"#;
let code = crate::compile_blob("TestSha3", source);
let param = "hello";
let input = TestSha3::testCall::new((param.to_string(),)).abi_encode();
let state = State::new(input);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let mut hasher = Keccak256::new();
hasher.update(param);
let expected = hasher.finalize();
let received = FixedBytes::<32>::from_slice(&state.output.data);
assert_eq!(received, expected);
}
#[test]
fn erc20() {
let _ = crate::compile_blob("ERC20", include_str!("../contracts/ERC20.sol"));
}
#[test]
fn triangle_number() {
let state = assert_success(Contract::triangle_number(13), true);
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
let expected = U256::try_from(91).unwrap();
assert_eq!(received, expected);
}
#[test]
fn odd_product() {
let state = assert_success(Contract::odd_product(5), true);
let received = I256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
let expected = I256::try_from(945i64).unwrap();
assert_eq!(received, expected);
}
#[test]
fn msize_plain() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract MSize {
function mSize() public pure returns (uint);
}
);
let code = crate::compile_blob_with_options(
"MSize",
include_str!("../contracts/MSize.sol"),
false,
revive_solidity::SolcPipeline::EVMLA,
);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let input = MSize::mSizeCall::new(()).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
// Solidity always stores the "free memory pointer" (32 byte int) at offset 64.
let expected = U256::try_from(64 + 32).unwrap();
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn transferred_value() {
sol!(
contract Value {
function value() public payable returns (uint);
}
);
let code = crate::compile_blob("Value", include_str!("../contracts/Value.sol"));
let mut state = State::new(Value::valueCall::SELECTOR.to_vec());
state.value = 0x1;
let (mut instance, export) = mock_runtime::prepare(&code, None);
let state = crate::mock_runtime::call(state, &mut instance, export);
assert_eq!(state.output.flags, 0);
let expected = I256::try_from(state.value).unwrap();
let received = I256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn msize_non_word_sized_access() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract MSize {
function mStore100() public pure returns (uint);
}
);
let code = crate::compile_blob_with_options(
"MSize",
include_str!("../contracts/MSize.sol"),
false,
revive_solidity::SolcPipeline::Yul,
);
let (mut instance, export) = mock_runtime::prepare(&code, None);
let input = MSize::mStore100Call::new(()).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
// https://docs.zksync.io/build/developer-reference/differences-with-ethereum.html#mstore-mload
// "Unlike EVM, where the memory growth is in words, on zkEVM the memory growth is counted in bytes."
// "For example, if you write mstore(100, 0) the msize on zkEVM will be 132, but on the EVM it will be 160."
let expected = U256::try_from(132).unwrap();
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
}
#[test]
fn mstore8() {
sol!(
#[derive(Debug, PartialEq, Eq)]
contract MStore8 {
function mStore8(uint value) public pure returns (uint256 word);
}
);
let code = crate::compile_blob("MStore8", include_str!("../contracts/mStore8.sol"));
let (mut instance, export) = mock_runtime::prepare(&code, None);
let mut assert = |parameter, expected| {
let input = MStore8::mStore8Call::new((parameter,)).abi_encode();
let state = crate::mock_runtime::call(State::new(input), &mut instance, export);
assert_eq!(state.output.flags, 0);
let received = U256::from_be_bytes::<32>(state.output.data.try_into().unwrap());
assert_eq!(received, expected);
};
for (parameter, expected) in [
(U256::MIN, U256::MIN),
(
U256::from(1),
U256::from_str_radix(
"452312848583266388373324160190187140051835877600158453279131187530910662656",
10,
)
.unwrap(),
),
(
U256::from(2),
U256::from_str_radix(
"904625697166532776746648320380374280103671755200316906558262375061821325312",
10,
)
.unwrap(),
),
(
U256::from(255),
U256::from_str_radix(
"115339776388732929035197660848497720713218148788040405586178452820382218977280",
10,
)
.unwrap(),
),
(U256::from(256), U256::from(0)),
(
U256::from(257),
U256::from_str_radix(
"452312848583266388373324160190187140051835877600158453279131187530910662656",
10,
)
.unwrap(),
),
(
U256::from(258),
U256::from_str_radix(
"904625697166532776746648320380374280103671755200316906558262375061821325312",
10,
)
.unwrap(),
),
(
U256::from(123456789),
U256::from_str_radix(
"9498569820248594155839807363993929941088553429603327518861754938149123915776",
10,
)
.unwrap(),
),
(
U256::MAX,
U256::from_str_radix(
"115339776388732929035197660848497720713218148788040405586178452820382218977280",
10,
)
.unwrap(),
),
] {
assert(parameter, expected);
}
}
#[test]
fn sha1() {
let pre = vec![0xffu8; 512];
let mut hasher = sha1::Sha1::new();
hasher.update(&pre);
let hash = hasher.finalize();
let state = assert_success(Contract::sha1(pre), true);
let expected = FixedBytes::<20>::from_slice(&hash[..]);
let received = FixedBytes::<20>::from_slice(&state.output.data[..20]);
assert_eq!(received, expected);
}