diff --git a/unittests/svnn_ibc_tests.cpp b/unittests/svnn_ibc_tests.cpp new file mode 100644 index 0000000000..f1d1fe998a --- /dev/null +++ b/unittests/svnn_ibc_tests.cpp @@ -0,0 +1,136 @@ +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include "fork_test_utilities.hpp" + +using namespace eosio::chain; +using namespace eosio::testing; + +#include +using mvo = mutable_variant_object; + + +BOOST_AUTO_TEST_SUITE(svnn_ibc) + +// Extending the default chain tester +// ( libraries/testing/include/eosio/testing/tester.hpp ) +class ibc_tester : public tester { +public: + const account_name _bridge = "bridge"_n; + + // This is mostly for creating accounts and loading contracts. + void setup(){ + // load bridge contract + create_account( _bridge ); + set_code( _bridge, eosio::testing::test_contracts::svnn_ibc_wasm()); + set_abi( _bridge, eosio::testing::test_contracts::svnn_ibc_abi()); + } + + //set a finalizer policy + void set_policy(){ + auto cr = push_action( _bridge, "setfpolicy"_n, _bridge, mutable_variant_object() + ("from_block_num", 1) + ("policy", mutable_variant_object() + ("generation", 1) + ("fthreshold", 3) + ("last_block_num", 0) + ("finalizers", fc::variants({ + mutable_variant_object() + ("description","finalizer1") + ("fweight", 1) + ("public_key", "b12eba13063c6cdc7bbe40e7de62a1c0f861a9ad55e924cdd5049be9b58e205053968179cede5be79afdcbbb90322406aefb7a5ce64edc2a4482d8656daed1eeacfb4286f661c0f9117dcd83fad451d301b2310946e5cd58808f7b441b280a02") + , + mutable_variant_object() + ("description","finalizer2") + ("fweight", 1) + ("public_key", "0728121cffe7b8ddac41817c3a6faca76ae9de762d9c26602f936ac3e283da756002d3671a2858f54c355f67b31b430b23b957dba426d757eb422db617be4cc13daf41691aa059b0f198fa290014d3c3e4fa1def2abc6a3328adfa7705c75508") + , + mutable_variant_object() + ("description","finalizer3") + ("fweight", 1) + ("public_key", "e06c31c83f70b4fe9507877563bfff49235774d94c98dbf9673d61d082ef589f7dd4865281f37d60d1bb433514d4ef0b787424fb5e53472b1d45d28d90614fad29a4e5e0fe70ea387f7845e22c843f6061f9be20a7af21d8b72d02f4ca494a0a") + , + mutable_variant_object() + ("description","finalizer4") + ("fweight", 1) + ("public_key", "08c9bd408bac02747e493d918e4b3e6bd1a2ffaf9bfca4f2e79dd22e12556bf46e911f25613c24d9f6403996c5246c19ef94aff48094868425eda1e46bcd059c59f3b060521be797f5cc2e6debe2180efa12c0814618a38836a64c3d7440740f") + })) + ) + ); + } + + //verify a proof + void check_proof(){ + auto cr = push_action( _bridge, "checkproof"_n, _bridge, mutable_variant_object() + ("proof", mutable_variant_object() + ("finality_proof", mutable_variant_object() + ("qc_block", mutable_variant_object() + ("major_version", 1) + ("minor_version", 0) + ("finalizer_policy_generation", 1) + ("witness_hash", "888ceeb757ea240d1c1ae2f4f717e67b73dcd592b2ba097f63b4c3e3ca4350e1") + ("finality_mroot", "1d2ab7379301370d3fa1b27a9f4ac077f6ea445a1aa3dbf7e18e9cc2c25b140c") + ) + ("qc", mutable_variant_object() + ("signature", "") + ("finalizers", fc::variants({})) + ) + ) + ("target_block_proof_of_inclusion", mutable_variant_object() + ("target_node_index", 7) + ("last_node_index", 7) + ("target", fc::variants({"block_data", mutable_variant_object() + ("finality_data", mutable_variant_object() + ("major_version", 1) + ("minor_version", 0) + ("finalizer_policy_generation", 1) + ("witness_hash", "dff620c1c4d31cade95ed609269a86d4ecb2357f9302d17675c0665c75786508") + ("finality_mroot", "1397eb7c86719f160188fa740fc3610ccb5a6681ad56807dc99a17fe73a7b7fd") + ) + ("dynamic_data", mutable_variant_object() + ("block_num", 28) + ("action_proofs", fc::variants()) + ("action_mroot", "4e890ef0e014f93bd1b31fabf1041ecc9fb1c44e957c2f7b1682333ee426677a") + ) + })) + ("merkle_branches", fc::variants({ + mutable_variant_object() + ("direction", 1) + ("hash", "4e17da018040c80339f2714828d1927d5b616f9af7aa4768c1876df6f05e5602") + , + mutable_variant_object() + ("direction", 1) + ("hash", "7ee0e16f1941fb5a98d80d20ca92e0c689e9284285d5f90ecd4f8f1ea2ffb53c") + , + mutable_variant_object() + ("direction", 1) + ("hash", "401526ba03ec4a955c83cda131dacd3e89becaad2cf04107170e436dd90a553f") + })) + ) + ) + ); + } +}; + +BOOST_AUTO_TEST_CASE( first_test ) try { + + ibc_tester chain_a; + + chain_a.setup(); + + chain_a.set_policy(); + chain_a.check_proof(); + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/unittests/test-contracts/CMakeLists.txt b/unittests/test-contracts/CMakeLists.txt index a62d82809c..accc16bf7b 100644 --- a/unittests/test-contracts/CMakeLists.txt +++ b/unittests/test-contracts/CMakeLists.txt @@ -45,3 +45,4 @@ add_subdirectory( crypto_primitives_test ) add_subdirectory( bls_primitives_test ) add_subdirectory( get_block_num_test ) add_subdirectory( nested_container_multi_index ) +add_subdirectory( svnn_ibc ) diff --git a/unittests/test-contracts/svnn_ibc/CMakeLists.txt b/unittests/test-contracts/svnn_ibc/CMakeLists.txt new file mode 100644 index 0000000000..31dfbd568d --- /dev/null +++ b/unittests/test-contracts/svnn_ibc/CMakeLists.txt @@ -0,0 +1,8 @@ +set( IBC_CONTRACT "svnn_ibc" ) + +if( EOSIO_COMPILE_TEST_CONTRACTS ) + add_contract( ${IBC_CONTRACT} ${IBC_CONTRACT} ${IBC_CONTRACT}.cpp ) +else() + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/${IBC_CONTRACT}.wasm ${CMAKE_CURRENT_BINARY_DIR}/${IBC_CONTRACT}.wasm COPYONLY ) + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/${IBC_CONTRACT}.abi ${CMAKE_CURRENT_BINARY_DIR}/${IBC_CONTRACT}.abi COPYONLY ) +endif() diff --git a/unittests/test-contracts/svnn_ibc/bitset.hpp b/unittests/test-contracts/svnn_ibc/bitset.hpp new file mode 100644 index 0000000000..711e089fad --- /dev/null +++ b/unittests/test-contracts/svnn_ibc/bitset.hpp @@ -0,0 +1,45 @@ +using namespace eosio; + +class bitset { +public: + bitset(size_t size) + : num_bits(size), data((size + 63) / 64) {} + + bitset(size_t size, const std::vector& raw_bitset) + : num_bits(size), data(raw_bitset) { + check(raw_bitset.size() == (size + 63) / 64, "invalid raw bitset size"); + + } + + // Set a bit to 1 + void set(size_t index) { + check_bounds(index); + data[index / 64] |= (1ULL << (index % 64)); + } + + // Clear a bit (set to 0) + void clear(size_t index) { + check_bounds(index); + data[index / 64] &= ~(1ULL << (index % 64)); + } + + // Check if a bit is set + bool test(size_t index) const { + check_bounds(index); + return (data[index / 64] & (1ULL << (index % 64))) != 0; + } + + // Size of the bitset + size_t size() const { + return num_bits; + } + +private: + size_t num_bits; + std::vector data; + + // Check if the index is within bounds + void check_bounds(size_t index) const { + check(index < num_bits, "bitset index out of bounds"); + } +}; diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.abi b/unittests/test-contracts/svnn_ibc/svnn_ibc.abi new file mode 100644 index 0000000000..a6737df603 --- /dev/null +++ b/unittests/test-contracts/svnn_ibc/svnn_ibc.abi @@ -0,0 +1,366 @@ +{ + "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", + "version": "eosio::abi/1.2", + "types": [ + { + "new_type_name": "bls_public_key", + "type": "bytes" + }, + { + "new_type_name": "bls_signature", + "type": "bytes" + }, + { + "new_type_name": "target_data", + "type": "variant_block_data_action_data" + } + ], + "structs": [ + { + "name": "action_data", + "base": "", + "fields": [ + { + "name": "action", + "type": "r_action" + }, + { + "name": "action_receipt_digest", + "type": "checksum256" + }, + { + "name": "return_value", + "type": "bytes" + } + ] + }, + { + "name": "block_data", + "base": "", + "fields": [ + { + "name": "finality_data", + "type": "block_finality_data" + }, + { + "name": "dynamic_data", + "type": "dynamic_data_v0" + } + ] + }, + { + "name": "block_finality_data", + "base": "", + "fields": [ + { + "name": "major_version", + "type": "uint32" + }, + { + "name": "minor_version", + "type": "uint32" + }, + { + "name": "finalizer_policy_generation", + "type": "uint32" + }, + { + "name": "active_finalizer_policy", + "type": "fpolicy?" + }, + { + "name": "witness_hash", + "type": "checksum256" + }, + { + "name": "finality_mroot", + "type": "checksum256" + } + ] + }, + { + "name": "checkproof", + "base": "", + "fields": [ + { + "name": "proof", + "type": "proof" + } + ] + }, + { + "name": "clear", + "base": "", + "fields": [] + }, + { + "name": "dynamic_data_v0", + "base": "", + "fields": [ + { + "name": "block_num", + "type": "uint32" + }, + { + "name": "action_proofs", + "type": "proof_of_inclusion[]" + }, + { + "name": "action_mroot", + "type": "checksum256?" + } + ] + }, + { + "name": "finality_proof", + "base": "", + "fields": [ + { + "name": "qc_block", + "type": "block_finality_data" + }, + { + "name": "qc", + "type": "quorum_certificate" + } + ] + }, + { + "name": "finalizer_authority", + "base": "", + "fields": [ + { + "name": "description", + "type": "string" + }, + { + "name": "fweight", + "type": "uint64" + }, + { + "name": "public_key", + "type": "bls_public_key" + } + ] + }, + { + "name": "fpolicy", + "base": "", + "fields": [ + { + "name": "generation", + "type": "uint32" + }, + { + "name": "fthreshold", + "type": "uint64" + }, + { + "name": "finalizers", + "type": "finalizer_authority[]" + } + ] + }, + { + "name": "lastproof", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "block_num", + "type": "uint32" + }, + { + "name": "finality_mroot", + "type": "checksum256" + }, + { + "name": "cache_expiry", + "type": "time_point" + } + ] + }, + { + "name": "merkle_branch", + "base": "", + "fields": [ + { + "name": "direction", + "type": "uint8" + }, + { + "name": "hash", + "type": "checksum256" + } + ] + }, + { + "name": "permission_level", + "base": "", + "fields": [ + { + "name": "actor", + "type": "name" + }, + { + "name": "permission", + "type": "name" + } + ] + }, + { + "name": "proof", + "base": "", + "fields": [ + { + "name": "finality_proof", + "type": "finality_proof?" + }, + { + "name": "target_block_proof_of_inclusion", + "type": "proof_of_inclusion" + } + ] + }, + { + "name": "proof_of_inclusion", + "base": "", + "fields": [ + { + "name": "target_node_index", + "type": "uint64" + }, + { + "name": "last_node_index", + "type": "uint64" + }, + { + "name": "target", + "type": "target_data" + }, + { + "name": "merkle_branches", + "type": "merkle_branch[]" + } + ] + }, + { + "name": "quorum_certificate", + "base": "", + "fields": [ + { + "name": "finalizers", + "type": "uint64[]" + }, + { + "name": "signature", + "type": "bls_signature" + } + ] + }, + { + "name": "r_action", + "base": "r_action_base", + "fields": [ + { + "name": "data", + "type": "bytes" + }, + { + "name": "returnvalue", + "type": "bytes" + } + ] + }, + { + "name": "r_action_base", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "name", + "type": "name" + }, + { + "name": "authorization", + "type": "permission_level[]" + } + ] + }, + { + "name": "setfpolicy", + "base": "", + "fields": [ + { + "name": "policy", + "type": "fpolicy" + }, + { + "name": "from_block_num", + "type": "uint32" + } + ] + }, + { + "name": "storedpolicy", + "base": "fpolicy", + "fields": [ + { + "name": "last_block_num", + "type": "uint32" + }, + { + "name": "cache_expiry", + "type": "time_point" + } + ] + } + ], + "actions": [ + { + "name": "checkproof", + "type": "checkproof", + "ricardian_contract": "" + }, + { + "name": "clear", + "type": "clear", + "ricardian_contract": "" + }, + { + "name": "setfpolicy", + "type": "setfpolicy", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "lastproofs", + "type": "lastproof", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "policies", + "type": "storedpolicy", + "index_type": "i64", + "key_names": [], + "key_types": [] + } + ], + "ricardian_clauses": [], + "variants": [ + { + "name": "variant_block_data_action_data", + "types": ["block_data","action_data"] + } + ], + "action_results": [] +} \ No newline at end of file diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp b/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp new file mode 100644 index 0000000000..a5ec47f869 --- /dev/null +++ b/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp @@ -0,0 +1,209 @@ +#include "svnn_ibc.hpp" + +//add two numbers from the g1 group (aggregation) +std::vector svnn_ibc::_g1add(const std::vector& op1, const std::vector& op2) { + check(op1.size() == std::tuple_size::value, "wrong op1 size passed"); + check(op2.size() == std::tuple_size::value, "wrong op2 size passed"); + bls_g1 r; + bls_g1_add(*reinterpret_cast(op1.data()), *reinterpret_cast(op2.data()), r); + std::vector v(r.begin(), r.end()); + return v; +} + +void svnn_ibc::_maybe_set_finalizer_policy(const fpolicy& policy, const uint32_t from_block_num){ + policies_table _policies_table(get_self(), get_self().value); + auto itr = _policies_table.rbegin(); + //if the new policy is more recent than the most recent we are aware of, we record the new one + if (itr==_policies_table.rend() || itr->generation < policy.generation){ + + //if a previous policy was in force, it is now superseded by the newer one for any future proof verification + if (itr!=_policies_table.rend()){ + auto fwd_itr = itr.base(); + fwd_itr--; + _policies_table.modify( fwd_itr, same_payer, [&]( auto& sfp ) { + sfp.last_block_num = from_block_num; + }); + } + svnn_ibc::storedpolicy spolicy; + spolicy.generation = policy.generation; + spolicy.fthreshold = policy.fthreshold; + spolicy.finalizers = policy.finalizers; + + //policy is in force until a newer policy is proven + spolicy.last_block_num = std::numeric_limits::max(); + //set cache expiry + spolicy.cache_expiry = add_time(current_time_point(), POLICY_CACHE_EXPIRY); + _policies_table.emplace( get_self(), [&]( auto& p ) { + p = spolicy; + }); + } +} + +//adds the newly proven root if necessary +void svnn_ibc::_maybe_add_proven_root(const uint32_t block_num, const checksum256& finality_mroot){ + proofs_table _proofs_table(get_self(), get_self().value); + auto block_num_index = _proofs_table.get_index<"blocknum"_n>(); + auto merkle_index = _proofs_table.get_index<"merkleroot"_n>(); + auto last_itr = block_num_index.rbegin(); + + //if first proven root or newer than the last proven root, we store it + if (last_itr == block_num_index.rend() || last_itr->block_num& pk, const std::vector& sig, std::vector& msg){ + check(pk.size() == std::tuple_size::value, "wrong pk size passed"); + check(sig.size() == std::tuple_size::value, "wrong sig size passed"); + bls_g1 g1_points[2]; + bls_g2 g2_points[2]; + + memcpy(g1_points[0].data(), eosio::detail::G1_ONE_NEG.data(), sizeof(bls_g1)); + memcpy(g2_points[0].data(), sig.data(), sizeof(bls_g2)); + + bls_g2 p_msg; + eosio::detail::g2_fromMessage(msg, eosio::detail::CIPHERSUITE_ID, p_msg); + memcpy(g1_points[1].data(), pk.data(), sizeof(bls_g1)); + memcpy(g2_points[1].data(), p_msg.data(), sizeof(bls_g2)); + + bls_gt r; + bls_pairing(g1_points, g2_points, 2, r); + check(0 == memcmp(r.data(), eosio::detail::GT_ONE.data(), sizeof(bls_gt)), "bls signature verify failed"); +} + +//verify that the quorum certificate over the finality digest is valid +void svnn_ibc::_check_qc(const quorum_certificate& qc, const checksum256& finality_digest, const uint64_t finalizer_policy_generation){ + + policies_table _policies_table(get_self(), get_self().value); + check(_policies_table.begin() != _policies_table.end(), "must set a finalizer policy before checking proofs"); + + //fetch the finalizer policy where generation num is equal prepare.input.finalizer_policy_generation, and that it is still in force + auto itr = _policies_table.find(finalizer_policy_generation); + check(itr!=_policies_table.end(), "finalizer policy not found"); + storedpolicy target_policy = *itr; + auto fa_itr = target_policy.finalizers.begin(); + auto fa_end_itr = target_policy.finalizers.end(); + size_t finalizer_count = std::distance(fa_itr, fa_end_itr); + std::vector bitset_data(qc.finalizers); + bitset b(finalizer_count, bitset_data); + + bool first = true; + + size_t index = 0; + uint64_t weight = 0; + + bls_public_key agg_pub_key; + + while (fa_itr != fa_end_itr){ + if (b.test(index)){ + if (first){ + first=false; + agg_pub_key = fa_itr->public_key; + } + else agg_pub_key = _g1add(agg_pub_key, fa_itr->public_key); + weight+=fa_itr->fweight; + + } + index++; + fa_itr++; + } + + //verify that we have enough vote weight to meet the quorum threshold of the target policy + check(weight>=target_policy.fthreshold, "insufficient signatures to reach quorum"); + std::array data = finality_digest.extract_as_byte_array(); + std::vector v_data(data.begin(), data.end()); + //verify signature validity + _verify(agg_pub_key, qc.signature, v_data); +} + +void svnn_ibc::_check_target_block_proof_of_inclusion(const proof_of_inclusion& proof, const std::optional reference_root){ + + //verify that the proof of inclusion is over a target block + check(std::holds_alternative(proof.target), "must supply proof of inclusion over block data"); + + //resolve the proof to its merkle root + checksum256 finality_mroot = proof.root(); + if (reference_root.has_value()){ + check(reference_root.value() == finality_mroot, "cannot link proof to proven merkle root"); + } + else { + proofs_table _proofs_table(get_self(), get_self().value); + auto merkle_index = _proofs_table.get_index<"merkleroot"_n>(); + auto itr = merkle_index.find(finality_mroot); + check(itr!= merkle_index.end(), "cannot link proof to proven merkle root"); + } + block_data target_block = std::get(proof.target); + if (target_block.finality_data.active_finalizer_policy.has_value()){ + _maybe_set_finalizer_policy(target_block.finality_data.active_finalizer_policy.value(), target_block.dynamic_data.block_num); + } +} + +void svnn_ibc::_check_finality_proof(const finality_proof& finality_proof, const proof_of_inclusion& target_block_proof_of_inclusion){ + //temporarilly disabled : skip qc verification + //if QC is valid, it means that we have reaced finality on the block referenced by the finality_mroot + //_check_qc(finality_proof.qc, finality_proof.qc_block.finality_digest(), finality_proof.qc_block.finalizer_policy_generation); + + //check if the target proof of inclusion correctly resolves to the root of the finality proof + _check_target_block_proof_of_inclusion(target_block_proof_of_inclusion, finality_proof.qc_block.finality_mroot); + + //if proof of inclusion was successful, the target block and its dynamic data have been validated as final and correct + block_data target_block = std::get(target_block_proof_of_inclusion.target); + + //if the finality_mroot we just proven is more recent than the last root we have stored, store it + _maybe_add_proven_root(target_block.dynamic_data.block_num, finality_proof.qc_block.finality_mroot); +} + +ACTION svnn_ibc::setfpolicy(const fpolicy& policy, const uint32_t from_block_num){ + + //can only be called with account authority + require_auth(get_self()); + + policies_table _policies_table(get_self(), get_self().value); + + //can only be used once for the initilization of the contract + check(_policies_table.begin() == _policies_table.end(), "can only set finalizer policy manually for initialization"); + + _maybe_set_finalizer_policy(policy, from_block_num); +} + +ACTION svnn_ibc::checkproof(const proof& proof){ + + //if we have a finality proof, we execute the "heavy" code path + if (proof.finality_proof.has_value()){ + _check_finality_proof(proof.finality_proof.value(), proof.target_block_proof_of_inclusion); + } + else { + //if we only have a proof of inclusion of the target block, we execute the "light" code path + _check_target_block_proof_of_inclusion(proof.target_block_proof_of_inclusion, std::nullopt); + } +} + +//temporary : reset the state +ACTION svnn_ibc::clear(){ + require_auth(get_self()); + proofs_table _proofs_table(get_self(), get_self().value); + policies_table _policies_table(get_self(), get_self().value); + auto fp_itr = _proofs_table.begin(); + while (fp_itr!= _proofs_table.end()){ + fp_itr = _proofs_table.erase(fp_itr); + } + auto p_itr = _policies_table.begin(); + while (p_itr!= _policies_table.end()){ + p_itr = _policies_table.erase(p_itr); + } +} \ No newline at end of file diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp b/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp new file mode 100644 index 0000000000..6ac6d8597b --- /dev/null +++ b/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp @@ -0,0 +1,414 @@ +#include +#include +#include +#include +#include + +#include "bitset.hpp" + +#include +#include + +using namespace eosio; + +CONTRACT svnn_ibc : public contract { + public: + using contract::contract; + + using bls_public_key = std::vector; + using bls_signature = std::vector; + + const uint32_t POLICY_CACHE_EXPIRY = 600; //10 minutes for testing + const uint32_t PROOF_CACHE_EXPIRY = 600; //10 minutes for testing + + //Compute the maximum number of layers of a merkle tree for a given number of leaves + uint64_t calculate_max_depth(uint64_t node_count) { + if(node_count <= 1) + return node_count; + return 64 - __builtin_clzll(2 << (64 - 1 - __builtin_clzll ((node_count - 1)))); + } + + static uint32_t reverse_bytes(const uint32_t input){ + uint32_t output = (input>>24 & 0xff)|(input>>8 & 0xff00)|(input<<8 & 0xff0000)|(input<<24 & 0xff000000); + return output; + } + + static checksum256 hash_pair(const std::pair p){ + std::array arr1 = p.first.extract_as_byte_array(); + std::array arr2 = p.second.extract_as_byte_array(); + std::array result; + std::copy (arr1.cbegin(), arr1.cend(), result.begin()); + std::copy (arr2.cbegin(), arr2.cend(), result.begin() + 32); + checksum256 hash = sha256(reinterpret_cast(result.data()), 64); + return hash; + } + + static time_point add_time(const time_point& time, const uint32_t seconds ){ + int64_t total_seconds = (static_cast(time.sec_since_epoch()) + static_cast(seconds)); + microseconds ms = microseconds(total_seconds * 1000000); + time_point tp = time_point(ms); + return tp; + } + + /* + + //discuss : compute merkle branch direction vs providing them as part of the proof + + static std::vector _get_directions(const uint64_t index, const uint64_t last_node_index){ + + std::vector proof; + + uint64_t c_index = index; + uint64_t layers_depth = calculate_max_depth(last_node_index) -1; + uint64_t c_last_node_index = last_node_index; + + for (uint64_t i = 0; i < layers_depth; i++) { + if (c_last_node_index % 2) c_last_node_index+=1; + bool isLeft = c_index % 2 == 0 ? 0 : 1; + uint64_t pairIndex = isLeft ? c_index - 1 : + c_index == last_node_index - 1 && i < layers_depth - 1 ? c_index : + c_index + 1; + c_last_node_index/=2; + if (pairIndex < last_node_index) proof.push_back(isLeft); + c_index = c_index / 2; + } + return proof; + } + + */ + + struct merkle_branch { + uint8_t direction; + checksum256 hash; + }; + + + //compute the merkle root of target node and vector of merkle branches + static checksum256 _compute_root(const std::vector proof_nodes, const checksum256& target){ + checksum256 hash = target; + for (int i = 0 ; i < proof_nodes.size() ; i++){ + const checksum256 node = proof_nodes[i].hash; + std::array arr = node.extract_as_byte_array(); + if (proof_nodes[i].direction == 0){ + hash = hash_pair(std::make_pair(hash, node)); + } + else { + hash = hash_pair(std::make_pair(node, hash)); + } + } + return hash; + } + + struct quorum_certificate { + std::vector finalizers; + bls_signature signature; + }; + + struct finalizer_authority { + std::string description; + uint64_t fweight = 0; + bls_public_key public_key; + }; + + struct fpolicy { + + uint32_t generation = 0; ///< sequentially incrementing version number + uint64_t fthreshold = 0; ///< vote fweight threshold to finalize blocks + std::vector finalizers; ///< Instant Finality voter set + + checksum256 digest() const { + std::vector serialized = pack(*this); + return sha256(serialized.data(), serialized.size()); + } + + }; + + //finalizer policy augmented with contextually-relevant data + TABLE storedpolicy : fpolicy { + + uint32_t last_block_num = 0; //last block number where this policy is in force + + time_point cache_expiry; //cache expiry + + uint64_t primary_key() const {return generation;} + uint64_t by_cache_expiry()const { return cache_expiry.sec_since_epoch(); } + + EOSLIB_SERIALIZE( storedpolicy, (generation)(fthreshold)(finalizers)(last_block_num)(cache_expiry)) + + }; + + TABLE lastproof { + + uint64_t id; + + uint32_t block_num; + + checksum256 finality_mroot; + + time_point cache_expiry; + + uint64_t primary_key()const { return id; } + uint64_t by_block_num()const { return block_num; } + uint64_t by_cache_expiry()const { return cache_expiry.sec_since_epoch(); } + checksum256 by_merkle_root()const { return finality_mroot; } + + }; + + struct authseq { + name account; + uint64_t sequence; + + EOSLIB_SERIALIZE( authseq, (account)(sequence) ) + + }; + + struct r_action_base { + name account; + name name; + std::vector authorization; + + }; + + struct r_action : r_action_base { + std::vector data; + std::vector returnvalue; + + checksum256 digest() const { + checksum256 hashes[2]; + const r_action_base* base = this; + const auto action_input_size = pack_size(data); + const auto return_value_size = pack_size(returnvalue); + const auto rhs_size = action_input_size + return_value_size; + const auto serialized_base = pack(*base); + const auto serialized_data = pack(data); + const auto serialized_output = pack(returnvalue); + hashes[0] = sha256(serialized_base.data(), serialized_base.size()); + std::vector data_digest(action_input_size); + std::vector output_digest(return_value_size); + std::vector h1_result(rhs_size); + std::copy (serialized_data.cbegin(), serialized_data.cend(), h1_result.begin()); + std::copy (serialized_output.cbegin(), serialized_output.cend(), h1_result.begin() + action_input_size); + hashes[1] = sha256(reinterpret_cast(h1_result.data()), rhs_size); + std::array arr1 = hashes[0].extract_as_byte_array(); + std::array arr2 = hashes[1].extract_as_byte_array(); + std::array result; + std::copy (arr1.cbegin(), arr1.cend(), result.begin()); + std::copy (arr2.cbegin(), arr2.cend(), result.begin() + 32); + checksum256 final_hash = sha256(reinterpret_cast(result.data()), 64); + return final_hash; + } + + EOSLIB_SERIALIZE( r_action, (account)(name)(authorization)(data)(returnvalue)) + + }; + + struct action_receipt { + + name receiver; + + //act_digest is provided instead by obtaining the action digest. Implementation depends on the activation of action_return_value feature + //checksum256 act_digest; + + uint64_t global_sequence = 0; + uint64_t recv_sequence = 0; + + std::vector auth_sequence; + unsigned_int code_sequence = 0; + unsigned_int abi_sequence = 0; + + EOSLIB_SERIALIZE( action_receipt, (receiver)(global_sequence)(recv_sequence)(auth_sequence)(code_sequence)(abi_sequence) ) + + }; + + struct proof_of_inclusion; + + struct dynamic_data_v0 { + + //block_num is always present + uint32_t block_num; + + //can include any number of action_proofs and / or state_proofs pertaining to a given block + //all action_proofs must resolve to the same action_mroot + std::vector action_proofs; + + //can be used instead of providing action_proofs. Useful for proving finalizer policy changes + std::optional action_mroot; + + checksum256 get_action_mroot() const { + if (action_mroot.has_value()) return action_mroot.value(); + else { + check(action_proofs.size()>0, "must have at least one action proof"); + checksum256 root = checksum256(); + for (auto ap : action_proofs){ + if (root == checksum256()) root = ap.root(); + else check(ap.root() == root, "all action proofs must resolve to the same merkle root"); + } + return root; + } + }; + }; + + struct block_finality_data { + + //major_version for this block + uint32_t major_version; + + //minor_version for this block + uint32_t minor_version; + + //finalizer_policy_generation for this block + uint32_t finalizer_policy_generation; + + //if the block to prove contains a finalizer policy change, it can be provided + std::optional active_finalizer_policy; + + //if a finalizer policy is present, witness_hash should be the base_digest. Otherwise, witness_hash should be the static_data_digest + checksum256 witness_hash; + + //final_on_qc for this block + checksum256 finality_mroot; + + //returns hash of digest of active_finalizer_policy + witness_hash if active_finalizer_policy is present, otherwise returns witness_hash + checksum256 resolve_witness() const { + if (active_finalizer_policy.has_value()){ + std::vector serialized_policy = pack(active_finalizer_policy.value()); + checksum256 policy_digest = sha256(serialized_policy.data(), serialized_policy.size()); + checksum256 base_fpolicy_digest = hash_pair( std::make_pair( policy_digest, witness_hash) ); + return base_fpolicy_digest; + } + else { + return witness_hash; + } + }; + + //returns hash of major_version + minor_version + finalizer_policy_generation + resolve_witness() + finality_mroot + checksum256 finality_digest() const { + std::array result; + memcpy(&result[0], (uint8_t *)&major_version, 4); + memcpy(&result[4], (uint8_t *)&minor_version, 4); + memcpy(&result[8], (uint8_t *)&finalizer_policy_generation, 4); + std::array arr1 = finality_mroot.extract_as_byte_array(); + std::array arr2 = resolve_witness().extract_as_byte_array(); + std::copy (arr1.cbegin(), arr1.cend(), result.begin() + 12); + std::copy (arr2.cbegin(), arr2.cend(), result.begin() + 44); + checksum256 hash = sha256(reinterpret_cast(result.data()), 76); + return hash; + }; + + }; + + struct block_data { + + //finality data + block_finality_data finality_data; + + //dynamic_data to be verified + dynamic_data_v0 dynamic_data; + + //returns hash of finality_digest() and dynamic_data_digest() + checksum256 digest() const { + checksum256 finality_digest = finality_data.finality_digest(); + checksum256 action_mroot = dynamic_data.get_action_mroot(); + std::array result; + memcpy(&result[0], (uint8_t *)&finality_data.major_version, 4); + memcpy(&result[4], (uint8_t *)&finality_data.minor_version, 4); + memcpy(&result[8], (uint8_t *)&dynamic_data.block_num, 4); + std::array arr1 = finality_digest.extract_as_byte_array(); + std::array arr2 = action_mroot.extract_as_byte_array(); + std::copy (arr1.cbegin(), arr1.cend(), result.begin() + 12); + std::copy (arr2.cbegin(), arr2.cend(), result.begin() + 44); + checksum256 hash = sha256(reinterpret_cast(result.data()), 76); + return hash; + }; + }; + + struct action_data { + + r_action action; //antelope action + checksum256 action_receipt_digest; //required witness hash, actual action_receipt is irrelevant to IBC + + std::vector return_value; //empty if no return value + + //returns the action digest + checksum256 action_digest() const { + return action.digest(); + }; + + //returns the receipt digest, composed of the action_digest() and action_receipt_digest witness hash + checksum256 digest() const { + checksum256 action_receipt_digest = hash_pair( std::make_pair( action_digest(), action_receipt_digest) ); + return action_receipt_digest; + }; + + }; + + using target_data = std::variant; + + + struct proof_of_inclusion { + + uint64_t target_node_index; + uint64_t last_node_index; + + target_data target; + + std::vector merkle_branches; + + //returns the merkle root obtained by hashing target.digest() with merkle_branches + checksum256 root() const { + auto call_digest = [](const auto& var) -> checksum256 { return var.digest(); }; + checksum256 digest = std::visit(call_digest, target); + checksum256 root = _compute_root(merkle_branches, digest); + return root; + }; + + }; + + struct finality_proof { + + //block finality data over which we validate a QC + block_finality_data qc_block; + + //signature over finality_digest() of qc_block. + quorum_certificate qc; + + }; + + struct proof { + + //valid configurations : + //1) finality_proof for a QC block, and proof_of_inclusion of a target block within the final_on_strong_qc block represented by the finality_mroot present in header + //2) only a proof_of_inclusion of a target block, which must be included in a merkle tree represented by a root stored in the contract's RAM + std::optional finality_proof; + proof_of_inclusion target_block_proof_of_inclusion; + + }; + + typedef eosio::multi_index< "policies"_n, storedpolicy, + indexed_by<"expiry"_n, const_mem_fun>> policies_table; + + typedef eosio::multi_index< "lastproofs"_n, lastproof, + indexed_by<"blocknum"_n, const_mem_fun>, + indexed_by<"merkleroot"_n, const_mem_fun>, + indexed_by<"expiry"_n, const_mem_fun>> proofs_table; + + std::vector _g1add(const std::vector& op1, const std::vector& op2); + + void _maybe_set_finalizer_policy(const fpolicy& policy, const uint32_t from_block_num); + void _maybe_add_proven_root(const uint32_t block_num, const checksum256& finality_mroot); + + void _garbage_collection(); + + void _verify(const std::vector& pk, const std::vector& sig, std::vector& msg); + void _check_qc(const quorum_certificate& qc, const checksum256& finality_mroot, const uint64_t finalizer_policy_generation); + + void _check_finality_proof(const finality_proof& finality_proof, const proof_of_inclusion& target_block_proof_of_inclusion); + void _check_target_block_proof_of_inclusion(const proof_of_inclusion& proof, const std::optional reference_root); + + ACTION setfpolicy(const fpolicy& policy, const uint32_t from_block_num); //set finality policy + ACTION checkproof(const proof& proof); + + //clearing function, to be removed for production version + ACTION clear(); + +}; \ No newline at end of file diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm b/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm new file mode 100755 index 0000000000..8f5d1276fd Binary files /dev/null and b/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm differ diff --git a/unittests/test_contracts.hpp.in b/unittests/test_contracts.hpp.in index 7523a54f2b..ae110b75ca 100644 --- a/unittests/test_contracts.hpp.in +++ b/unittests/test_contracts.hpp.in @@ -50,6 +50,7 @@ namespace eosio { MAKE_READ_WASM_ABI(bls_primitives_test, bls_primitives_test, test-contracts) MAKE_READ_WASM_ABI(get_block_num_test, get_block_num_test, test-contracts) MAKE_READ_WASM_ABI(nested_container_multi_index, nested_container_multi_index, test-contracts) + MAKE_READ_WASM_ABI(svnn_ibc, svnn_ibc, test-contracts) }; } /// eosio::testing