diff --git a/run_contests.sh b/run_contests.sh index d72ee1c02..9a2b90370 100755 --- a/run_contests.sh +++ b/run_contests.sh @@ -17,3 +17,4 @@ bash ./contest.sh test/examples/dispatch/slices.json bash ./contest.sh test/examples/dispatch/fallback.json bash ./contest.sh test/examples/dispatch/ecrecover.json bash ./contest.sh test/examples/dispatch/memory.json +bash ./contest.sh test/examples/dispatch/deposit.json diff --git a/test/Cases.hs b/test/Cases.hs index 5da75eba0..c36710f14 100644 --- a/test/Cases.hs +++ b/test/Cases.hs @@ -117,7 +117,8 @@ dispatches = runDispatchTest "Revert.solc", runDispatchTest "hashes.solc", runDispatchTest "empty.solc", - runDispatchTest "empty_no_constructor.solc" + runDispatchTest "empty_no_constructor.solc", + runDispatchTest "deposit.solc" ] where runDispatchTest file = runTestForFileWith (emptyOption mempty) file "./test/examples/dispatch" diff --git a/test/examples/dispatch/deposit.json b/test/examples/dispatch/deposit.json new file mode 100644 index 000000000..137c5e2aa --- /dev/null +++ b/test/examples/dispatch/deposit.json @@ -0,0 +1,214 @@ +{ + "depositcontract": { + "bytecode": "_CODE", + "contract": "DepositContract", + "tests": [ + { + "input": { + "comment": "constructor()", + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + + { + "input": { + "comment": "get_zero_hash(0)", + "calldata": "43ca34020000000000000000000000000000000000000000000000000000000000000000", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + + { + "input": { + "comment": "get_zero_hash(1)", + "calldata": "43ca34020000000000000000000000000000000000000000000000000000000000000001", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "status": "success" + } + }, + + + { + "input": { + "comment": "get_zero_hash(2)", + "calldata": "43ca34020000000000000000000000000000000000000000000000000000000000000002", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "status": "success" + } + }, + + + { + "input": { + "comment": "get_zero_hash(3)", + "calldata": "43ca34020000000000000000000000000000000000000000000000000000000000000003", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "c78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "status": "success" + } + }, + + + { + "input": { + "comment": "get_zero_hash(4)", + "calldata": "43ca34020000000000000000000000000000000000000000000000000000000000000004", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "status": "success" + } + }, + + { + "input": { + "comment": "get_zero_hash(15)", + "calldata": "43ca3402000000000000000000000000000000000000000000000000000000000000000f", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "d49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "status": "success" + } + }, + + { + "input": { + "comment": "get_zero_hash(31)", + "calldata": "43ca3402000000000000000000000000000000000000000000000000000000000000001f", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "status": "success" + } + }, + + { + "input": { + "comment": "get_deposit_root()", + "calldata": "c5f2892f", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "d70a234731285c6804c2a4f56711ddb8c82c99740f207854891028af34e27e5e", + "status": "success" + } + }, + + { + "input": { + "comment": "get_deposit_count()", + "calldata": "621fd130", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + + { + "input": { + "comment": "deposit(bytes,bytes,bytes,bytes32)", + "calldata": "22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120d2b73e995b4c94bf83bcbf58cb9dede2c5e0211ec61eeb09c43aeaad97ce02620000000000000000000000000000000000000000000000000000000000000030abe405f4ca553d02da780b187e15c767fc89d4f9a707bd54c98a8d8e401124dedc6bc14dcdf0ed690e9ec424deae49a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d0500000000000000000000000000000000000000000000000000000000000000608927fa063b70c19a28c3c78aa19bbb2e7b971fc1b8de2033930952a11ef17709348cc2cc14ae810ab249a1fe39e959470b850608afc299811c5cce190f92dd79529ba11846c138073ae200782476c8e5e4c6ea3b5938a16398835e4964c6bac0", + "value": "32000000000000000000" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + + { + "input": { + "comment": "get_deposit_root()", + "calldata": "c5f2892f", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "d3014e96ac4ef15ccbac8047549cac09ad687f6169cba901b92c8875a62ca917", + "status": "success" + } + }, + + { + "input": { + "comment": "get_deposit_count()", + "calldata": "621fd130", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080100000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + + { + "input": { + "comment": "deposit(bytes,bytes,bytes,bytes32)", + "calldata": "22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120d2b73e995b4c94bf83bcbf58cb9dede2c5e0211ec61eeb09c43aeaad97ce02620000000000000000000000000000000000000000000000000000000000000030abe405f4ca553d02da780b187e15c767fc89d4f9a707bd54c98a8d8e401124dedc6bc14dcdf0ed690e9ec424deae49a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d0500000000000000000000000000000000000000000000000000000000000000608927fa063b70c19a28c3c78aa19bbb2e7b971fc1b8de2033930952a11ef17709348cc2cc14ae810ab249a1fe39e959470b850608afc299811c5cce190f92dd79529ba11846c138073ae200782476c8e5e4c6ea3b5938a16398835e4964c6bac0", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "bdfc7472", + "status": "failure" + } + }, + + { + "input": { + "comment": "get_deposit_root()", + "calldata": "c5f2892f", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "d3014e96ac4ef15ccbac8047549cac09ad687f6169cba901b92c8875a62ca917", + "status": "success" + } + }, + + { + "input": { + "comment": "get_deposit_count()", + "calldata": "621fd130", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080100000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + } + ] + } +} diff --git a/test/examples/dispatch/deposit.solc b/test/examples/dispatch/deposit.solc new file mode 100644 index 000000000..c05455817 --- /dev/null +++ b/test/examples/dispatch/deposit.solc @@ -0,0 +1,163 @@ +import std.{*}; +import std.dispatch.{*}; +import std.opcodes.{callvalue}; + +// TODO: Should use uint64. +// Assumes 64-bit input. +function to_little_endian_64(v: uint256) -> memory(bytes) { + let res: word = allocate_memory(32 + 8); + let value: word = Typedef.rep(v); + assembly { + mstore(res, 8) + mstore8(add(res, 32), and(value, 0xff)) + mstore8(add(res, 33), and(shr(8, value), 0xff)) + mstore8(add(res, 34), and(shr(16, value), 0xff)) + mstore8(add(res, 35), and(shr(24, value), 0xff)) + mstore8(add(res, 36), and(shr(32, value), 0xff)) + mstore8(add(res, 37), and(shr(40, value), 0xff)) + mstore8(add(res, 38), and(shr(48, value), 0xff)) + mstore8(add(res, 39), and(shr(56, value), 0xff)) + } + return memory(res); +} + +// No constants are supported yet, using this as a workaround. +// Defining variables outside of contract/function is not supported. +function DEPOSIT_CONTRACT_TREE_DEPTH() -> uint256 { + return uint256(32); +} + +function MAX_DEPOSIT_COUNT() -> uint256 { + // uint constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1; + // Could use Bounded(uint64).maxVal() + return uint256(0xFFFFFFFF); +} + +contract DepositContract { + deposit_count : uint256; + // TODO: need storage array support + // branch: array(uint256, DEPOSIT_CONTRACT_TREE_DEPTH()); + // zero_hashes : array(uint256, DEPOSIT_CONTRACT_TREE_DEPTH()); + // misusing mappings here, the index is the key + branch : mapping(uint256, bytes32); + zero_hashes : mapping(uint256, bytes32); + + constructor() { + // Compute hashes in empty sparse Merkle tree + for (let height = uint256(0); height < (DEPOSIT_CONTRACT_TREE_DEPTH() - uint256(1)); height += uint256(1)) { + zero_hashes[height + uint256(1)] = sha256(concat(zero_hashes[height], zero_hashes[height])); + } + } + + // TODO: this is for testing only + public function get_zero_hash(index: uint256) -> bytes32 { + return zero_hashes[index]; + } + + public function get_deposit_root() -> bytes32 { + let node: bytes32; + let size = deposit_count; + for (let height = uint256(0); height < DEPOSIT_CONTRACT_TREE_DEPTH(); height += uint256(1)) { + //if ((size & 1) == 1) { + if ((size % uint256(2)) == uint256(1)) { + node = sha256(concat(branch[height], node)); + } else { + node = sha256(concat(node, zero_hashes[height])); + } + size = size / uint256(2); + } + return sha256(concat( + concat( + node, + to_little_endian_64(deposit_count) + ), + empty(24) + )); + } + + public function get_deposit_count() -> memory(bytes) { + return to_little_endian_64(deposit_count); + } + + // TODO: once string literals are properly supported, change errors to messages + // matching the deposit contract, full 100% identical behaviour. + public payable function deposit(pubkey: memory(bytes), withdrawal_credentials: memory(bytes), signature: memory(bytes), deposit_data_root: bytes32) -> () { + // Extended ABI length checks since dynamic types are used. + require(MemorySize.len(pubkey) == 48, Error(0x9ca717ed)); // InvalidPubkeyLength() + require(MemorySize.len(withdrawal_credentials) == 32, Error(0x3debbf1e)); // InvalidWithdrawalCredentialsLength() + require(MemorySize.len(signature) == 96, Error(0x4be6321b)); // InvalidSignatureLength() + + // Check deposit amount + // >= 1ether + require(callvalue() >= 1000000000000000000, Error(0xbdfc7472)); // DepositValueTooLow() + // % 1 gwei == 0 + require((callvalue() % 1000000000) == 0, Error(0x9c7417e9)); // DepositValueNotMultipleOfOneGwei() + + let deposit_amount = callvalue() / 1000000000; // 1 gwei + // <= type(uint64).max + require(deposit_amount <= 0xffffffffffffffff, Error(0x2aa66734)); // DepositValueTooHigh() + + let amount: memory(bytes) = to_little_endian_64(uint256(deposit_amount)); + // TODO: emit DepositEvent + /* + event DepositEvent( + bytes pubkey, + bytes withdrawal_credentials, + bytes amount, + bytes signature, + bytes index + ); + + emit DepositEvent( + pubkey, + withdrawal_credentials, + amount, + signature, + to_little_endian_64(uint64(deposit_count)) + ); + */ + assembly { + // TODO: need to ABI-encode all the arguments + log1(0, 0, 0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5) + } + + // Compute deposit data root (`DepositData` hash tree root) + let pubkey_root = sha256(concat(pubkey, empty(16))); + let signature_root = sha256(concat( + sha256(truncate(signature, 64)), // slice + sha256(concat(slice_(signature, 64), empty(32))) + )); + let node = sha256(concat( + sha256(concat(pubkey_root, withdrawal_credentials)), + sha256(concat(concat(amount, empty(24)), signature_root)) + )); + + // Verify computed and expected deposit data roots match + require(node == deposit_data_root, Error(0x2ec2f183)); // ReconstructedDepositDataMismatch() + + // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`) + require(deposit_count < MAX_DEPOSIT_COUNT(), Error(0xef5ccf66)); // MerkleTreeFull() + + // Add deposit data root to Merkle tree (update a single `branch` node) + deposit_count += uint256(1); + let size = deposit_count; + for (let height = uint256(0); height < DEPOSIT_CONTRACT_TREE_DEPTH(); height += uint256(1)) { + //if ((size & 1) == 1) { + if ((size % uint256(2)) == uint256(1)) { + branch[height] = node; + return (); + } + node = sha256(concat(branch[height], node)); + size = size / uint256(2); + } + + // As the loop should always end prematurely with the `return` statement, + // this code should be unreachable. We assert `false` just to be safe. + assert(false); + } + + // TODO: use bytes4 + public function supportsInterface(interfaceId: uint256) -> bool { + return false; + } +} \ No newline at end of file diff --git a/test/testrunner/testrunner.cpp b/test/testrunner/testrunner.cpp index 8e26fd869..2c46815b0 100644 --- a/test/testrunner/testrunner.cpp +++ b/test/testrunner/testrunner.cpp @@ -138,6 +138,8 @@ int main(int argc, char** argv) } bool status = result.status_code == EVMC_SUCCESS; + + std::cout << "Status: " << result.status_code << " Output: " << toHex(output) << std::endl; if (kind == "constructor") {