diff --git a/Makefile.am b/Makefile.am index c0a574d5c0..9e165eff97 100755 --- a/Makefile.am +++ b/Makefile.am @@ -269,6 +269,7 @@ test_libbitcoin_system_test_SOURCES = \ test/chain/stripper.cpp \ test/chain/taproot.cpp \ test/chain/tapscript.cpp \ + test/chain/threshold.cpp \ test/chain/transaction.cpp \ test/chain/witness.cpp \ test/chain/enums/opcode.cpp \ @@ -537,6 +538,7 @@ include_bitcoin_system_chain_HEADERS = \ include/bitcoin/system/chain/stripper.hpp \ include/bitcoin/system/chain/taproot.hpp \ include/bitcoin/system/chain/tapscript.hpp \ + include/bitcoin/system/chain/threshold.hpp \ include/bitcoin/system/chain/transaction.hpp \ include/bitcoin/system/chain/witness.hpp diff --git a/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj b/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj index 3cb7bd465a..6676c31e42 100644 --- a/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj @@ -196,6 +196,7 @@ + $(IntDir)test_chain_transaction.obj diff --git a/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters index a1188b9722..633198b031 100644 --- a/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters @@ -225,6 +225,9 @@ src\chain + + src\chain + src\chain diff --git a/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj b/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj index 9738bbae57..7a4ace68a2 100644 --- a/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj @@ -402,6 +402,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj.filters index 78e9381c56..1951a4c0c3 100644 --- a/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-system/libbitcoin-system.vcxproj.filters @@ -869,6 +869,9 @@ include\bitcoin\system\chain + + include\bitcoin\system\chain + include\bitcoin\system\chain diff --git a/builds/msvc/vs2026/libbitcoin-system-examples/libbitcoin-system-examples.vcxproj b/builds/msvc/vs2026/libbitcoin-system-examples/libbitcoin-system-examples.vcxproj index c55056f4c7..617502c57f 100644 --- a/builds/msvc/vs2026/libbitcoin-system-examples/libbitcoin-system-examples.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-system-examples/libbitcoin-system-examples.vcxproj @@ -141,7 +141,7 @@ - + @@ -156,7 +156,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-system-examples/packages.config b/builds/msvc/vs2026/libbitcoin-system-examples/packages.config index aaa2275581..0fb78a9c39 100644 --- a/builds/msvc/vs2026/libbitcoin-system-examples/packages.config +++ b/builds/msvc/vs2026/libbitcoin-system-examples/packages.config @@ -6,7 +6,7 @@ | --> - + diff --git a/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj b/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj index 520692e083..b40177c87d 100644 --- a/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj @@ -196,6 +196,7 @@ + $(IntDir)test_chain_transaction.obj @@ -483,7 +484,7 @@ - + @@ -499,7 +500,7 @@ - + @@ -509,4 +510,4 @@ - + \ No newline at end of file diff --git a/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters index a1188b9722..633198b031 100644 --- a/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-system-test/libbitcoin-system-test.vcxproj.filters @@ -225,6 +225,9 @@ src\chain + + src\chain + src\chain diff --git a/builds/msvc/vs2026/libbitcoin-system-test/packages.config b/builds/msvc/vs2026/libbitcoin-system-test/packages.config index a2158b7ddf..f72a09697a 100644 --- a/builds/msvc/vs2026/libbitcoin-system-test/packages.config +++ b/builds/msvc/vs2026/libbitcoin-system-test/packages.config @@ -6,7 +6,7 @@ | --> - + diff --git a/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj b/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj index f349cd6d40..17f4d53604 100644 --- a/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj @@ -402,6 +402,7 @@ + @@ -773,7 +774,7 @@ - + @@ -797,9 +798,9 @@ - + - + \ No newline at end of file diff --git a/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj.filters index 78e9381c56..1951a4c0c3 100644 --- a/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-system/libbitcoin-system.vcxproj.filters @@ -869,6 +869,9 @@ include\bitcoin\system\chain + + include\bitcoin\system\chain + include\bitcoin\system\chain diff --git a/builds/msvc/vs2026/libbitcoin-system/packages.config b/builds/msvc/vs2026/libbitcoin-system/packages.config index aaa2275581..0fb78a9c39 100644 --- a/builds/msvc/vs2026/libbitcoin-system/packages.config +++ b/builds/msvc/vs2026/libbitcoin-system/packages.config @@ -6,7 +6,7 @@ | --> - + diff --git a/include/bitcoin/system.hpp b/include/bitcoin/system.hpp index 0ac1f42ced..9690246c8c 100755 --- a/include/bitcoin/system.hpp +++ b/include/bitcoin/system.hpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/system/chain/chain.hpp b/include/bitcoin/system/chain/chain.hpp index b1bf52159c..f977bb7033 100644 --- a/include/bitcoin/system/chain/chain.hpp +++ b/include/bitcoin/system/chain/chain.hpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/system/chain/enums/magic_numbers.hpp b/include/bitcoin/system/chain/enums/magic_numbers.hpp index 89d9fa03b5..5223d5d90f 100644 --- a/include/bitcoin/system/chain/enums/magic_numbers.hpp +++ b/include/bitcoin/system/chain/enums/magic_numbers.hpp @@ -42,7 +42,7 @@ constexpr size_t max_unified_stack_size = 1'000; constexpr size_t max_unified_script_size = 20'000; constexpr size_t max_script_size = to_half(max_unified_script_size); constexpr size_t max_push_data_size = 520; -constexpr size_t max_script_public_keys = 20; +constexpr size_t max_multisig_public_keys = 20; constexpr size_t multisig_default_sigops = 20; constexpr size_t max_number_size_four = 4; constexpr size_t max_number_size_five = 5; diff --git a/include/bitcoin/system/chain/signatures.hpp b/include/bitcoin/system/chain/signatures.hpp index 535ab00cee..30dcd0b54f 100644 --- a/include/bitcoin/system/chain/signatures.hpp +++ b/include/bitcoin/system/chain/signatures.hpp @@ -20,10 +20,9 @@ #define LIBBITCOIN_SYSTEM_CHAIN_SIGNATURES_HPP #include -#include #include +#include #include -#include #include #include @@ -52,51 +51,6 @@ struct BC_API signatures schnorr }; - /// Threshold category. - enum class category - { - single, - equal, - inequal, - lesser, - greater, - not_lesser, - not_greater, - between - }; - - struct threshold_group - { - struct entry - { - /// Digest is created in the sigop (sigop scope - must copy). - hash_digest digest; - - /// Point is a stack element (script scope - use reference). - cref point; - - /// Signature is a stack element (script scope - use reference). - cref sig; - }; - - /// Scoping requires that capture_.threshold(threshold_group) does not - /// retain a reference to point or sig (must copy or dispose the refs). - inline bool push_entry(const hash_digest& digest, - const cref& point, - const cref& sig) NOEXCEPT - { - BC_ASSERT(entries.size() == expected); - entries.emplace_back(digest, point, sig); - return entries.size() == expected; - } - - std::vector entries{}; - opcode condition{}; - uint16_t minimum{}; - uint16_t maximum{}; - uint16_t expected{}; - }; - /// Reporting handlers. using log_handler = std::function; using fire_handler = std::function; @@ -108,7 +62,7 @@ struct BC_API signatures const ec_xonly, const ec_signature&)>; using multisig_handler = std::function; - using threshold_handler = std::function; + using threshold_handler = std::function; /// Default construction disables batching. const bool enabled{}; @@ -151,7 +105,7 @@ struct BC_API signatures }; const threshold_handler threshold { - [] (const threshold_group&) NOEXCEPT + [] (const chain::threshold&) NOEXCEPT { return false; } diff --git a/include/bitcoin/system/chain/threshold.hpp b/include/bitcoin/system/chain/threshold.hpp new file mode 100644 index 0000000000..95a23950ed --- /dev/null +++ b/include/bitcoin/system/chain/threshold.hpp @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SYSTEM_CHAIN_THRESHOLD_HPP +#define LIBBITCOIN_SYSTEM_CHAIN_THRESHOLD_HPP + +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace system { +namespace chain { + +/// Script helper for threshold signature accumulation. +struct threshold +{ + enum category_t : uint8_t + { + unknown, + single, + equal, + inequal, + lesser, + greater, + not_lesser, + not_greater, + between + }; + + struct tuple + { + /// Digest is created in the sigop (sigop scope - must copy). + hash_digest digest; + + /// Point is a stack element (script scope - use reference). + cref point; + + /// Signature is a stack element (script scope - use reference). + cref sig; + }; + + /// Convert opcode to category. + static constexpr category_t to_category(opcode code) NOEXCEPT + { + switch (code) + { + // checksig is not a threshold script code but is recorded with + // single signatures archived in the multisig table. + case opcode::checksig: + return category_t::single; + case opcode::numequal: + case opcode::numequalverify: + return category_t::equal; + case opcode::numnotequal: + return category_t::inequal; + case opcode::lessthan: + return category_t::lesser; + case opcode::greaterthan: + return category_t::greater; + case opcode::lessthanorequal: + return category_t::not_lesser; + case opcode::greaterthanorequal: + return category_t::not_greater; + case opcode::within: + return category_t::between; + default: + return category_t::unknown; + } + } + + /// Scoping requires that capture_.threshold(threshold) does not + /// retain a reference to point or sig (must copy or dispose the refs). + inline bool push_tuple(const hash_digest& digest, + const cref& point, const cref& sig) NOEXCEPT + { + tuples.emplace_back(digest, point, sig); + return tuples.size() == expected; + } + + std::vector tuples{}; + category_t category{}; + uint16_t minimum{}; + uint16_t maximum{}; + uint16_t expected{}; +}; + +} // namespace chain +} // namespace system +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/system/crypto/secp256k1_batch.hpp b/include/bitcoin/system/crypto/secp256k1_batch.hpp index b656024e4f..4a1be8ee77 100644 --- a/include/bitcoin/system/crypto/secp256k1_batch.hpp +++ b/include/bitcoin/system/crypto/secp256k1_batch.hpp @@ -29,61 +29,12 @@ namespace libbitcoin { namespace system { -namespace batchy { + +namespace batched { using link = data_array<3>; using link_t = unsigned_type; using links_t = std::vector; -using pairing_t = uint8_t; -using group_t = uint16_t; -} // namespace batchy - -namespace schnorr { - -/// Span matches serialized buffer. -struct BC_API batch -{ - using span = std::span; - static batchy::links_t verify(const stopper& cancel, - const span& batch) NOEXCEPT; - - hash_digest digest; - ec_xonly point; - ec_signature signature; - batchy::link id; - -protected: - static data_chunk evaluate(const stopper& cancel, - const span& batch) NOEXCEPT; - static batchy::links_t correlate(const stopper& cancel, - const data_chunk& out, const span& batch) NOEXCEPT; -}; - -} // namespace schnorr - -namespace threshold { - -/// Span matches serialized buffer. -struct BC_API batch -{ - using span = std::span; - static batchy::links_t verify(const stopper& cancel, - const span& batch) NOEXCEPT; - - hash_digest digest; - ec_xonly point; - ec_signature signature; - batchy::pairing_t required; - batchy::group_t group; - batchy::link id; - -protected: - static data_chunk evaluate(const stopper& cancel, - const span& batch) NOEXCEPT; - static batchy::links_t correlate(const stopper& cancel, - const data_chunk& out, const span& batch) NOEXCEPT; -}; - -} // namespace threshold +} // namespace batched namespace ecdsa { @@ -91,47 +42,59 @@ namespace ecdsa { struct BC_API batch { using span = std::span; - static batchy::links_t verify(const stopper& cancel, + static batched::links_t verify(const stopper& cancel, const span& batch) NOEXCEPT; hash_digest digest; ec_compressed point; ec_signature signature; - batchy::link id; + uint8_t pair; + uint16_t group; + batched::link id; protected: + using multisig_matrix = std::array>; + static bool meets_threshold(uint8_t signatures, uint8_t keys, + const multisig_matrix& successes) NOEXCEPT; + static batched::links_t get_failures(const stopper& cancel, + const data_chunk& out, const span& in) NOEXCEPT; static data_chunk evaluate(const stopper& cancel, const span& batch) NOEXCEPT; - static batchy::links_t correlate(const stopper& cancel, + static batched::links_t correlate(const stopper& cancel, const data_chunk& out, const span& batch) NOEXCEPT; }; } // namespace ecdsa -namespace multisig { +namespace schnorr { /// Span matches serialized buffer. struct BC_API batch { using span = std::span; - static batchy::links_t verify(const stopper& cancel, + static batched::links_t verify(const stopper& cancel, const span& batch) NOEXCEPT; hash_digest digest; - ec_compressed point; + ec_xonly point; ec_signature signature; - batchy::pairing_t pair; - batchy::group_t group; - batchy::link id; + uint8_t category; + uint16_t pair; + uint16_t group; + batched::link id; protected: + static bool meets_threshold(uint8_t category, size_t successes, + size_t minimum, size_t maximum) NOEXCEPT; + static batched::links_t get_failures(const stopper& cancel, + const data_chunk& out, const span& in) NOEXCEPT; static data_chunk evaluate(const stopper& cancel, const span& batch) NOEXCEPT; - static batchy::links_t correlate(const stopper& cancel, + static batched::links_t correlate(const stopper& cancel, const data_chunk& out, const span& batch) NOEXCEPT; }; -} // namespace multisig +} // namespace schnorr } // namespace system } // namespace libbitcoin diff --git a/include/bitcoin/system/impl/chain/script_patterns.ipp b/include/bitcoin/system/impl/chain/script_patterns.ipp index f2f9c19d60..aca5624c51 100644 --- a/include/bitcoin/system/impl/chain/script_patterns.ipp +++ b/include/bitcoin/system/impl/chain/script_patterns.ipp @@ -307,8 +307,9 @@ constexpr bool script::is_pay_multisig_standard_pattern( return true; } +// This does not match 0 of n (no signatures to verify). // Allows non-minimal number encoding, and invalid size public keys. -// op_check_multisig is limited to 20 signatures (max_script_public_keys). +// op_check_multisig is limited to 20 signatures (max_multisig_public_keys). constexpr bool script::is_pay_multisig_pattern(const operations& ops) NOEXCEPT { if (ops.size() < 4u || @@ -319,7 +320,7 @@ constexpr bool script::is_pay_multisig_pattern(const operations& ops) NOEXCEPT { uint32_t count{}; if (!op.as_unsigned32(count) || - is_limited(count, one, max_script_public_keys)) + is_limited(count, one, max_multisig_public_keys)) return 0_u8; return narrow_cast(count); diff --git a/include/bitcoin/system/impl/machine/interpreter.ipp b/include/bitcoin/system/impl/machine/interpreter.ipp index 62fcae6b36..b7f65caca3 100644 --- a/include/bitcoin/system/impl/machine/interpreter.ipp +++ b/include/bitcoin/system/impl/machine/interpreter.ipp @@ -1126,7 +1126,7 @@ op_check_multisig_verify() NOEXCEPT if (!this->pop_index32(count)) return error::op_check_multisig_verify1; - if (count > chain::max_script_public_keys) + if (count > chain::max_multisig_public_keys) return error::op_check_multisig_verify2; if (!this->ops_increment(count)) diff --git a/include/bitcoin/system/impl/machine/program_batch.ipp b/include/bitcoin/system/impl/machine/program_batch.ipp index 93d156b92a..62eb52cce3 100644 --- a/include/bitcoin/system/impl/machine/program_batch.ipp +++ b/include/bitcoin/system/impl/machine/program_batch.ipp @@ -143,7 +143,7 @@ verify_schnorr_signature(const data_chunk& point, const hash_digest& hash, { if (is_threshold_batchable()) { - if (threshold_.push_entry(hash, std::cref(as_xonly(point)), + if (threshold_.push_tuple(hash, std::cref(as_xonly(point)), std::cref(signature))) { // With a capture fault block success is disregarded. @@ -180,12 +180,13 @@ is_threshold_batchable() const NOEXCEPT return false; // Already batching. - if (!threshold_.entries.empty()) + if (!threshold_.tuples.empty()) return true; size_t min{}, max{}; - const auto condition = script_->extract_tapscript_threshold(min, max); - if (operation::is_invalid(condition)) + const auto op = script_->extract_tapscript_threshold(min, max); + const auto category = chain::threshold::to_category(op); + if (category == chain::threshold::category_t::unknown) return false; // All non-empty elements (sigs) on the stack (plus self) must be evaluated @@ -198,11 +199,11 @@ is_threshold_batchable() const NOEXCEPT is_limited(expected)) return false; - threshold_.entries.reserve(expected); + threshold_.tuples.reserve(expected); threshold_.minimum = narrow_cast(min); threshold_.maximum = narrow_cast(max); threshold_.expected = narrow_cast(expected); - threshold_.condition = condition; + threshold_.category = category; return true; } diff --git a/include/bitcoin/system/machine/program.hpp b/include/bitcoin/system/machine/program.hpp index 36a7eb9f0f..ba630a8042 100644 --- a/include/bitcoin/system/machine/program.hpp +++ b/include/bitcoin/system/machine/program.hpp @@ -216,7 +216,6 @@ class program private: static constexpr auto relaxed = std::memory_order_relaxed; static constexpr auto bip342_mask = bit_not(flags::bip342_rule); - using threshold_cache = chain::signatures::threshold_group; using primary_stack = stack; struct multisig_cache { @@ -272,7 +271,7 @@ class program // Caches. mutable multisig_cache multisig_{}; - mutable threshold_cache threshold_{}; + mutable chain::threshold threshold_{}; // Stacks. primary_stack primary_; diff --git a/src/crypto/secp256k1/batch.cpp b/src/crypto/secp256k1/batch.cpp index c18c446029..2b057219cb 100644 --- a/src/crypto/secp256k1/batch.cpp +++ b/src/crypto/secp256k1/batch.cpp @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -25,6 +26,7 @@ #if defined(HAVE_ULTRAFAST) #include #endif +#include #include #include #include @@ -36,53 +38,55 @@ namespace libbitcoin { namespace system { -using namespace batchy; +using namespace chain; +using namespace batched; + +// Array indexing required for c++20 span. +BC_PUSH_WARNING(NO_ARRAY_INDEXING) // polymorphic namespace selectors (template support) // ---------------------------------------------------------------------------- // local -inline bool verify_signature(const schnorr::batch& single) NOEXCEPT -{ - return schnorr::verify_signature(single.point, single.digest, - single.signature); -} - -inline bool verify_signature(const threshold::batch& single) NOEXCEPT -{ - return schnorr::verify_signature(single.point, single.digest, - single.signature); -} - inline bool verify_signature(const ecdsa::batch& single) NOEXCEPT { return ecdsa::verify_signature(single.point, single.digest, single.signature); } -inline bool verify_signature(const multisig::batch& single) NOEXCEPT +inline bool verify_signature(const schnorr::batch& single) NOEXCEPT { - return ecdsa::verify_signature(single.point, single.digest, + return schnorr::verify_signature(single.point, single.digest, single.signature); } -// Array indexing required for c++20 span. -BC_PUSH_WARNING(NO_ARRAY_INDEXING) - // batch_verify // ---------------------------------------------------------------------------- #if defined(HAVE_ULTRAFAST) -// TODO: pass cancel. template -data_chunk batch_verify(const stopper& /* cancel */, +data_chunk batch_verify(const stopper& cancel, const std::span& batch) NOEXCEPT { // OOM is unrecoverable. - static thread_local ufsecp::lbtc::Controller context{ UFSECP_LBTC_AUTO }; + static thread_local ufsecp::lbtc::Controller context{}; if (!context.ok()) std::abort(); + // set up cancellation callback. + const ufsecp_cancel_fn callback = [](void* atomic) NOEXCEPT + { + constexpr auto relaxed = std::memory_order_relaxed; + return to_int(pointer_cast(atomic)->load(relaxed)); + }; + + // TODO: should be modified to accept `const void*`. + BC_PUSH_WARNING(NO_CONST_CAST) + constexpr auto interval = 0; + const auto user = const_cast(&cancel); + const ufsecp_cancel_token token{ callback, user, interval }; + BC_POP_WARNING() + const auto count = batch.size(); data_chunk results(count); const auto out = results.data(); @@ -90,13 +94,17 @@ data_chunk batch_verify(const stopper& /* cancel */, if constexpr (is_same_type) { - constexpr auto extra_size = sizeof(Batch) - (sizeof(hash_digest) + sizeof(ec_xonly) + sizeof(ec_signature)); - ufsecp_lbtc_verify_schnorr(context.get(), in, count, extra_size, out, nullptr, 0, nullptr); + constexpr auto extra_size = sizeof(Batch) - (sizeof(hash_digest) + + sizeof(ec_xonly) + sizeof(ec_signature)); + ufsecp_lbtc_verify_schnorr(context.get(), in, count, extra_size, + out, nullptr, 0, nullptr, &token); } else { - constexpr auto extra_size = sizeof(Batch) - (sizeof(hash_digest) + sizeof(ec_compressed) + sizeof(ec_signature)); - ufsecp_lbtc_verify_ecdsa_opaque(context.get(), in, count, extra_size, out, nullptr, 0, nullptr); + constexpr auto extra_size = sizeof(Batch) - (sizeof(hash_digest) + + sizeof(ec_compressed) + sizeof(ec_signature)); + ufsecp_lbtc_verify_ecdsa_opaque(context.get(), in, count, extra_size, + out, nullptr, 0, nullptr, &token); } return results; @@ -127,103 +135,86 @@ data_chunk batch_verify(const stopper& cancel, #endif -// utility -// ---------------------------------------------------------------------------- // local - -using multisig_matrix = std::array>; - -// O(1) as m and n are bounded at 16. -inline bool has_valid_path(uint8_t m_sigs, uint8_t n_keys, - const multisig_matrix& success) NOEXCEPT +// ---------------------------------------------------------------------------- +inline void push_fail(links_t& fails, const batched::link& id) NOEXCEPT { - multisig_matrix matrix{}; - - for (size_t key{}; key < n_keys; ++key) - { - for (size_t sig{}; sig < m_sigs; ++sig) - { - if (get_right(success.at(sig), key)) - { - uint16_t length{}; - for (size_t subkey{}; subkey < key; ++subkey) - length = greater(matrix.at(subkey), length); - - if (++length >= m_sigs) return true; - matrix.at(key) = greater(length, matrix.at(key)); - } - } - } - - return false; + fails.push_back(from_little_array(id)); } - -// to_links + +// get_failures (ecdsa) // ---------------------------------------------------------------------------- -// Trivial single signature correlation. -template -links_t to_links(const stopper& cancel, const data_chunk& out, - const std::span& in) NOEXCEPT +// O(1) as m and n are bounded at 16. +bool ecdsa::batch::meets_threshold(uint8_t signatures, uint8_t keys, + const multisig_matrix& successes) NOEXCEPT { - BC_ASSERT(out.size() == in.size()); - - // Used only to produce order for concurrency. - std::vector it(in.size()); - std::iota(it.begin(), it.end(), zero); + BC_ASSERT(!is_limited(signatures, 1, 16)); + BC_ASSERT(!is_limited(keys, 1, 16)); + if (signatures > keys) + return false; - links_t fails(zero); - std::shared_mutex mutex{}; - - std::for_each(poolstl::execution::par, it.cbegin(), it.cend(), - [&](size_t row) NOEXCEPT - { - if (cancel) return; + uint8_t key{}; + for (uint8_t signature{}; signature < signatures; ++signature) + { + bool matched{}; + while (key < keys && !matched) + if (get_right(successes.at(signature), key++)) + matched = true; - // Failure is *extremely* rare, so this is very efficient. - if (!to_bool(out.at(row))) - { - std::unique_lock lock{ mutex }; - fails.push_back(from_little_array(in[row].id)); - } - }); + if (!matched) + return false; + } - return fails; + return true; } -// Specialized threshold correlation. +// Ecdsa (single sig and standard multisig) correlation. // O(n) over the sig set, ~100 bytes of stack, no heap. -template <> -links_t to_links(const stopper& cancel, - const data_chunk& out, const threshold::batch::span& in) NOEXCEPT +links_t ecdsa::batch::get_failures(const stopper& cancel, + const data_chunk& out, const span& in) NOEXCEPT { BC_ASSERT(out.size() == in.size()); - if (in.empty()) - return {}; - size_t group{}; links_t fails{}; - for (auto index = one; index <= in.size(); ++index) + for (auto index = one; index <= in.size() && !cancel; ++index) { - if (cancel) - return {}; - // Find the start of the next group (or end). if ((index != in.size()) && (in[index].id == in[group].id) && (in[index].group == in[group].group)) continue; - // Count successes in this group. - size_t successes{}; + // Short-circuit single signature. + const auto second = add1(group); + const auto single = (index == second); + if (single) + { + if (!to_bool(out.at(group))) + push_fail(fails, in[group].id); + + group = index; + continue; + } + + // Build matrix and determine effective m/n from (sig, key) pairs. + multisig_matrix successes{}; + uint8_t max_sig{}, max_key{}; + for (auto row = group; row < index; ++row) + { + const auto [sig, key] = unpack_word(in[row].pair); if (to_bool(out.at(row))) - ++successes; + set_right_into(successes.at(sig), key); + + max_sig = greater(sig, max_sig); + max_key = greater(key, max_key); + } - // Fail if group successes < required. - if (successes < in[group].required) - fails.push_back(from_little_array(in[group].id)); + // Evaluate op_checkmultisig success. + if (!meets_threshold(add1(max_sig), add1(max_key), successes)) + push_fail(fails, in[group].id); group = index; } @@ -231,45 +222,75 @@ links_t to_links(const stopper& cancel, return fails; } -// Specialized multisig correlation. +// get_failures (schnorr) +// ---------------------------------------------------------------------------- + +bool schnorr::batch::meets_threshold(uint8_t category, size_t successes, + size_t minimum, size_t maximum) NOEXCEPT +{ + switch (static_cast(category)) + { + case threshold::category_t::equal: + return (successes == minimum); + case threshold::category_t::inequal: + return (successes != minimum); + case threshold::category_t::lesser: + return is_lesser(successes, minimum); + case threshold::category_t::greater: + return is_greater(successes, minimum); + case threshold::category_t::not_greater: + return !is_greater(successes, minimum); + case threshold::category_t::not_lesser: + return !is_lesser(successes, minimum); + case threshold::category_t::between: + return !is_limited(successes, minimum, maximum); + default: + return false; + } +} + +// Schnorr (single sig and threshold sigs) correlation. // O(n) over the sig set, ~100 bytes of stack, no heap. -template <> -links_t to_links(const stopper& cancel, - const data_chunk& out, const multisig::batch::span& in) NOEXCEPT +links_t schnorr::batch::get_failures(const stopper& cancel, + const data_chunk& out, const span& in) NOEXCEPT { BC_ASSERT(out.size() == in.size()); - if (in.empty()) - return {}; - size_t group{}; links_t fails{}; - for (auto index = one; index <= in.size(); ++index) + for (auto index = one; index <= in.size() && !cancel; ++index) { - if (cancel) - return {}; - // Find the start of the next group (or end). if ((index != in.size()) && (in[index].id == in[group].id) && (in[index].group == in[group].group)) continue; - // Process the previous group. - multisig_matrix matrix{}; - uint8_t max_sig{}, max_key{}; - for (auto row = group; row < index; ++row) + // Short-circuit single signature. + const auto first = group; + const auto second = add1(group); + const auto single = (index == second); + if (single) { - static_assert(is_same_type); - const auto [sig, key] = unpack_word(in[row].pair); - if (to_bool(out.at(row))) set_right_into(matrix.at(sig), key); - max_sig = greater(sig, max_sig); - max_key = greater(key, max_key); + if (!to_bool(out.at(group))) + push_fail(fails, in[group].id); + + group = index; + continue; } - // Evaluate matrix for valid solution. - if (!has_valid_path(++max_sig, ++max_key, matrix)) - fails.push_back(from_little_array(in[group].id)); + // Count successes in the group. + size_t successes{}; + for (auto row = group; row < index; ++row) + if (to_bool(out.at(row))) + ++successes; + + // Get min and max from first two rows (max only for op_within). + const auto minimum = in[first].pair; + const auto maximum = in[second].pair; + const auto category = in[first].category; + if (!meets_threshold(category, successes, minimum, maximum)) + push_fail(fails, in[group].id); group = index; } @@ -277,32 +298,18 @@ links_t to_links(const stopper& cancel, return fails; } -BC_POP_WARNING() - // evaluate // ---------------------------------------------------------------------------- // static/protected -data_chunk schnorr::batch::evaluate(const stopper& cancel, - const schnorr::batch::span& batch) NOEXCEPT -{ - return batch_verify(cancel, batch); -} - -data_chunk threshold::batch::evaluate(const stopper& cancel, - const threshold::batch::span& batch) NOEXCEPT -{ - return batch_verify(cancel, batch); -} - data_chunk ecdsa::batch::evaluate(const stopper& cancel, - const ecdsa::batch::span& batch) NOEXCEPT + const span& batch) NOEXCEPT { return batch_verify(cancel, batch); } -data_chunk multisig::batch::evaluate(const stopper& cancel, - const multisig::batch::span& batch) NOEXCEPT +data_chunk schnorr::batch::evaluate(const stopper& cancel, + const span& batch) NOEXCEPT { return batch_verify(cancel, batch); } @@ -311,57 +318,35 @@ data_chunk multisig::batch::evaluate(const stopper& cancel, // ---------------------------------------------------------------------------- // static/protected -links_t schnorr::batch::correlate(const stopper& cancel, - const data_chunk& out, const schnorr::batch::span& in) NOEXCEPT -{ - return to_links(cancel, out, in); -} - -links_t threshold::batch::correlate(const stopper& cancel, - const data_chunk& out, const threshold::batch::span& in) NOEXCEPT -{ - return to_links(cancel, out, in); -} - links_t ecdsa::batch::correlate(const stopper& cancel, - const data_chunk& out, const ecdsa::batch::span& in) NOEXCEPT + const data_chunk& out, const span& in) NOEXCEPT { - return to_links(cancel, out, in); + return get_failures(cancel, out, in); } -links_t multisig::batch::correlate(const stopper& cancel, - const data_chunk& out, const multisig::batch::span& in) NOEXCEPT +links_t schnorr::batch::correlate(const stopper& cancel, + const data_chunk& out, const span& in) NOEXCEPT { - return to_links(cancel, out, in); + return get_failures(cancel, out, in); } // verify // ---------------------------------------------------------------------------- // static/public -links_t schnorr::batch::verify(const stopper& cancel, - const schnorr::batch::span& batch) NOEXCEPT -{ - return correlate(cancel, evaluate(cancel, batch), batch); -} - -links_t threshold::batch::verify(const stopper& cancel, - const threshold::batch::span& batch) NOEXCEPT -{ - return correlate(cancel, evaluate(cancel, batch), batch); -} - links_t ecdsa::batch::verify(const stopper& cancel, - const ecdsa::batch::span& batch) NOEXCEPT + const span& batch) NOEXCEPT { return correlate(cancel, evaluate(cancel, batch), batch); } -links_t multisig::batch::verify(const stopper& cancel, - const multisig::batch::span& batch) NOEXCEPT +links_t schnorr::batch::verify(const stopper& cancel, + const span& batch) NOEXCEPT { return correlate(cancel, evaluate(cancel, batch), batch); } +BC_POP_WARNING() + } // namespace system } // namespace libbitcoin diff --git a/test/chain/threshold.cpp b/test/chain/threshold.cpp new file mode 100644 index 0000000000..9f77a2bb41 --- /dev/null +++ b/test/chain/threshold.cpp @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" + +BOOST_AUTO_TEST_SUITE(threshold_tests) + +using namespace system; +using namespace system::chain; + +BOOST_AUTO_TEST_CASE(threshold__construction__default__unknown_empty) +{ + threshold group{}; + BOOST_CHECK(group.tuples.empty()); + BOOST_CHECK_EQUAL(group.category, threshold::category_t::unknown); + BOOST_CHECK_EQUAL(group.minimum, 0u); + BOOST_CHECK_EQUAL(group.maximum, 0u); + BOOST_CHECK_EQUAL(group.expected, 0u); +} + +BOOST_AUTO_TEST_CASE(threshold__push_tuple__under_expected__false) +{ + threshold group{}; + group.expected = 2; + hash_digest digest{}; + ec_signature sig{}; + ec_xonly point{}; + + BOOST_CHECK(!group.push_tuple(digest, cref(point), cref(sig))); + BOOST_CHECK_EQUAL(group.tuples.size(), 1u); +} + +BOOST_AUTO_TEST_CASE(threshold__push_tuple__exactly_expected__true) +{ + threshold group{}; + group.expected = 1; + hash_digest digest{}; + ec_signature sig{}; + ec_xonly point{}; + + BOOST_CHECK(group.push_tuple(digest, cref(point), cref(sig))); + BOOST_CHECK_EQUAL(group.tuples.size(), 1u); +} + +// to_category + +BOOST_AUTO_TEST_CASE(threshold__to_category__checksig__single) +{ + // checksig is not a threshold script code but is recorded with + // single signatures archived in the multisig table. + BOOST_CHECK(threshold::to_category(opcode::checksig) == threshold::category_t::single); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__numequal__equal) +{ + BOOST_CHECK(threshold::to_category(opcode::numequal) == threshold::category_t::equal); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__numequalverify__equal) +{ + BOOST_CHECK(threshold::to_category(opcode::numequalverify) == threshold::category_t::equal); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__numnotequal__inequal) +{ + BOOST_CHECK(threshold::to_category(opcode::numnotequal) == threshold::category_t::inequal); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__lessthan__lesser) +{ + BOOST_CHECK(threshold::to_category(opcode::lessthan) == threshold::category_t::lesser); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__greaterthan__greater) +{ + BOOST_CHECK(threshold::to_category(opcode::greaterthan) == threshold::category_t::greater); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__lessthanorequal__not_lesser) +{ + BOOST_CHECK(threshold::to_category(opcode::lessthanorequal) == threshold::category_t::not_lesser); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__greaterthanorequal__not_greater) +{ + BOOST_CHECK(threshold::to_category(opcode::greaterthanorequal) == threshold::category_t::not_greater); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__within__between) +{ + BOOST_CHECK(threshold::to_category(opcode::within) == threshold::category_t::between); +} + +BOOST_AUTO_TEST_CASE(threshold__to_category__non_threashold__unknown) +{ + BOOST_CHECK(threshold::to_category(opcode::add) == threshold::category_t::unknown); + BOOST_CHECK(threshold::to_category(opcode::boolor) == threshold::category_t::unknown); + BOOST_CHECK(threshold::to_category(opcode::op_xor) == threshold::category_t::unknown); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/crypto/secp256k1/batch.cpp b/test/crypto/secp256k1/batch.cpp index 2c76adece4..3ec6d94135 100644 --- a/test/crypto/secp256k1/batch.cpp +++ b/test/crypto/secp256k1/batch.cpp @@ -20,45 +20,37 @@ BOOST_AUTO_TEST_SUITE(secp256k1_tests) -// These tests use sign() to produce ECDSA signature vectors, however sign() -// produces the private format `secp256k1_ecdsa_signature` type, not canonical. - const ec_secret one = base16_array( "0000000000000000000000000000000000000000000000000000000000000001"); -const ec_secret secret1 = base16_array( +const ec_secret secret0 = base16_array( "8010b1bb119ad37d4b65a1022a314897b1b3614b345974332cb1b9582cf03536"); -const ec_secret secret3 = base16_array( +const ec_secret secret1 = base16_array( "33436393f770d9b3f5d11c20be561837300f89515284008965d2fd3f714b8fce"); -// batch schnorr signature verification +// batch ecdsa +// ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(secp256k1__schnorr_batch_verify__all_valid__expected) +BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__singles_all_valid__expected) { using namespace system; - using namespace system::schnorr; - const auto hash = bitcoin_hash(to_chunk("batch-schnorr")); - - ec_compressed pub1{}, pub2{}, pub3{}; - BOOST_REQUIRE(secret_to_public(pub1, secret1)); - BOOST_REQUIRE(secret_to_public(pub2, secret3)); - BOOST_REQUIRE(secret_to_public(pub3, one)); + using namespace system::ecdsa; + const auto hash = bitcoin_hash(to_chunk("batch-ecdsa")); - // TODO: add system utils: ec_secret -> ec_compressed -> ec_xonly. - const auto& point1 = array_cast(pub1); - const auto& point2 = array_cast(pub2); - const auto& point3 = array_cast(pub3); + ec_compressed point0{}, point1{}, point2{}; + BOOST_REQUIRE(secret_to_public(point0, secret0)); + BOOST_REQUIRE(secret_to_public(point1, secret1)); + BOOST_REQUIRE(secret_to_public(point2, one)); - ec_signature sig1{}, sig2{}, sig3{}; - constexpr hash_digest auxiliary{}; - BOOST_REQUIRE(sign(sig1, secret1, hash, auxiliary)); - BOOST_REQUIRE(sign(sig2, secret3, hash, auxiliary)); - BOOST_REQUIRE(sign(sig3, one, hash, auxiliary)); + ec_signature sig0{}, sig1{}, sig2{}; + BOOST_REQUIRE(sign(sig0, secret0, hash)); + BOOST_REQUIRE(sign(sig1, secret1, hash)); + BOOST_REQUIRE(sign(sig2, one, hash)); const std::array triples { - batch{ hash, point1, sig1, { 0, 0, 0 } }, - batch{ hash, point2, sig2, { 1, 0, 0 } }, - batch{ hash, point3, sig3, { 2, 0, 0 } } + batch{ hash, point0, sig0, 0, 0, { 0, 0, 0 } }, + batch{ hash, point1, sig1, 0, 0, { 1, 0, 0 } }, + batch{ hash, point2, sig2, 0, 0, { 2, 0, 0 } } }; stopper cancel{}; @@ -66,66 +58,60 @@ BOOST_AUTO_TEST_CASE(secp256k1__schnorr_batch_verify__all_valid__expected) BOOST_REQUIRE(tokens.empty()); } -BOOST_AUTO_TEST_CASE(secp256k1__schnorr_batch_verify__one_invalid__expected) +BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__singles_one_invalid__expected) { using namespace system; - using namespace system::schnorr; - const auto hash = bitcoin_hash(to_chunk("batch-schnorr-neg")); - - ec_compressed pub1{}, pub2{}, pub3{}; - BOOST_REQUIRE(secret_to_public(pub1, secret1)); - BOOST_REQUIRE(secret_to_public(pub2, secret3)); - BOOST_REQUIRE(secret_to_public(pub3, one)); + using namespace system::ecdsa; + const auto hash = bitcoin_hash(to_chunk("batch-ecdsa-invalid")); - // TODO: add system utils: ec_secret -> ec_compressed -> ec_xonly. - const auto& point1 = array_cast(pub1); - const auto& point2 = array_cast(pub2); - const auto& point3 = array_cast(pub3); + ec_compressed point0{}, point1{}, point2{}; + BOOST_REQUIRE(secret_to_public(point0, secret0)); + BOOST_REQUIRE(secret_to_public(point1, secret1)); + BOOST_REQUIRE(secret_to_public(point2, one)); - ec_signature sig1{}, sig2{}, sig3{}; - constexpr hash_digest auxiliary{}; - BOOST_REQUIRE(sign(sig1, secret1, hash, auxiliary)); - BOOST_REQUIRE(sign(sig2, secret3, hash, auxiliary)); - BOOST_REQUIRE(sign(sig3, one, hash, auxiliary)); + ec_signature sig0{}, sig1{}, sig2{}; + BOOST_REQUIRE(sign(sig0, secret0, hash)); + BOOST_REQUIRE(sign(sig1, secret1, hash)); + BOOST_REQUIRE(sign(sig2, one, hash)); - // corrupt second signature + // corrupt third signature sig2[10] ^= 0xff; const std::array triples { - batch{ hash, point1, sig1, { 0, 0, 0 } }, - batch{ hash, point2, sig2, { 1, 0, 0 } }, - batch{ hash, point3, sig3, { 2, 0, 0 } } + batch{ hash, point0, sig0, 0, 0, { 0, 0, 0 } }, + batch{ hash, point1, sig1, 0, 0, { 1, 0, 0 } }, + batch{ hash, point2, sig2, 0, 0, { 2, 0, 0 } } }; stopper cancel{}; const auto tokens = batch::verify(cancel, { triples.data(), triples.size() }); BOOST_REQUIRE_EQUAL(tokens.size(), 1u); - BOOST_REQUIRE_EQUAL(tokens.front(), from_little_array(triples.at(1).id)); + BOOST_REQUIRE_EQUAL(tokens.front(), from_little_array(triples.at(2).id)); } -// batch ecdsa signature verification - -BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__all_valid__expected) +BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__multisig_all_valid__expected) { using namespace system; using namespace system::ecdsa; - const auto hash = bitcoin_hash(to_chunk("batch-ecdsa")); - ec_compressed point1{}, point2{}, point3{}; + const auto hash = bitcoin_hash(to_chunk("batch-multisig")); + + ec_compressed point0{}, point1{}, point2{}; + BOOST_REQUIRE(secret_to_public(point0, secret0)); BOOST_REQUIRE(secret_to_public(point1, secret1)); - BOOST_REQUIRE(secret_to_public(point2, secret3)); - BOOST_REQUIRE(secret_to_public(point3, one)); + BOOST_REQUIRE(secret_to_public(point2, one)); - ec_signature sig1{}, sig2{}, sig3{}; + ec_signature sig0{}, sig1{}; + BOOST_REQUIRE(sign(sig0, secret0, hash)); BOOST_REQUIRE(sign(sig1, secret1, hash)); - BOOST_REQUIRE(sign(sig2, secret3, hash)); - BOOST_REQUIRE(sign(sig3, one, hash)); - const std::array triples + // 2-of-3 group. + const std::array triples { - batch{ hash, point1, sig1, { 0, 0, 0 } }, - batch{ hash, point2, sig2, { 1, 0, 0 } }, - batch{ hash, point3, sig3, { 2, 0, 0 } } + batch{ hash, point0, sig0, 0b0000'0000, 5, { 0, 0, 0 } }, // invalid + batch{ hash, point1, sig0, 0b0000'0001, 5, { 0, 0, 0 } }, // valid + batch{ hash, point1, sig1, 0b0001'0001, 5, { 0, 0, 0 } }, // invalid + batch{ hash, point2, sig1, 0b0001'0010, 5, { 0, 0, 0 } } // valid }; stopper cancel{}; @@ -133,62 +119,142 @@ BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__all_valid__expected) BOOST_REQUIRE(tokens.empty()); } -BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__one_invalid__expected) +BOOST_AUTO_TEST_CASE(secp256k1__ecdsa_batch_verify__multisig_one_invalid__expected) { using namespace system; using namespace system::ecdsa; - const auto hash = bitcoin_hash(to_chunk("batch-ecdsa-invalid")); + const auto hash = bitcoin_hash(to_chunk("batch-multisig-invalid")); - ec_compressed point1{}, point2{}, point3{}; + ec_compressed point0{}, point1{}, point2{}; + BOOST_REQUIRE(secret_to_public(point0, secret0)); BOOST_REQUIRE(secret_to_public(point1, secret1)); - BOOST_REQUIRE(secret_to_public(point2, secret3)); - BOOST_REQUIRE(secret_to_public(point3, one)); + BOOST_REQUIRE(secret_to_public(point2, one)); - ec_signature sig1{}, sig2{}, sig3{}; + ec_signature sig0{}, sig1{}, sig2{}; + BOOST_REQUIRE(sign(sig0, secret0, hash)); BOOST_REQUIRE(sign(sig1, secret1, hash)); - BOOST_REQUIRE(sign(sig2, secret3, hash)); - BOOST_REQUIRE(sign(sig3, one, hash)); + BOOST_REQUIRE(sign(sig2, one, hash)); - // corrupt third signature - sig3[10] ^= 0xff; + // corrupt second signature + sig1[10] ^= 0xff; + + // 2-of-3 group (same id + group) const std::array triples { - batch{ hash, point1, sig1, { 0, 0, 0 } }, - batch{ hash, point2, sig2, { 1, 0, 0 } }, - batch{ hash, point3, sig3, { 2, 0, 0 } } + batch{ hash, point0, sig0, 0b0000'0000, 7, { 0, 0, 0 } }, // valid + batch{ hash, point1, sig1, 0b0000'0001, 7, { 0, 0, 0 } }, // invalid + batch{ hash, point2, sig2, 0b0001'0000, 7, { 0, 0, 0 } } // valid }; stopper cancel{}; const auto tokens = batch::verify(cancel, { triples.data(), triples.size() }); BOOST_REQUIRE_EQUAL(tokens.size(), 1u); - BOOST_REQUIRE_EQUAL(tokens.front(), from_little_array(triples.at(2).id)); + BOOST_REQUIRE_EQUAL(tokens.front(), from_little_array(triples.at(0).id)); +} + +// meets_threshold + +struct ecdsa_accessor + : public ecdsa::batch +{ + using multisig_matrix = multisig_matrix; + using ecdsa::batch::meets_threshold; +}; + +using multisig_matrix = ecdsa_accessor::multisig_matrix; + +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__1_of_1__success) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + BOOST_REQUIRE(ecdsa_accessor::meets_threshold(1, 1, matrix)); } -// batch multisig (ecdsa) verification +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__1_of_1__failure) +{ + multisig_matrix matrix{}; + BOOST_REQUIRE(!ecdsa_accessor::meets_threshold(1, 1, matrix)); +} -BOOST_AUTO_TEST_CASE(secp256k1__multisig_batch_verify__all_valid__expected) +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__2_of_3__success) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + set_right_into(matrix.at(1), 1); + BOOST_REQUIRE(ecdsa_accessor::meets_threshold(2, 3, matrix)); +} + +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__2_of_3__failure) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + set_right_into(matrix.at(0), 1); // both successes on same key + BOOST_REQUIRE(!ecdsa_accessor::meets_threshold(2, 3, matrix)); +} + +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__3_of_3__success) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + set_right_into(matrix.at(1), 1); + set_right_into(matrix.at(2), 2); + BOOST_REQUIRE(ecdsa_accessor::meets_threshold(3, 3, matrix)); +} + +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__3_of_3__failure) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + set_right_into(matrix.at(1), 0); + set_right_into(matrix.at(2), 0); + BOOST_REQUIRE(!ecdsa_accessor::meets_threshold(3, 3, matrix)); +} + +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__2_of_2__success) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + set_right_into(matrix.at(1), 1); + BOOST_REQUIRE(ecdsa_accessor::meets_threshold(2, 2, matrix)); +} + +BOOST_AUTO_TEST_CASE(ecdsa_batch__meets_threshold__2_of_2__failure) +{ + multisig_matrix matrix{}; + set_right_into(matrix.at(0), 0); + set_right_into(matrix.at(0), 1); + BOOST_REQUIRE(!ecdsa_accessor::meets_threshold(2, 2, matrix)); +} + +// batch schnorr +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(secp256k1__schnorr_batch_verify__single_all_valid__expected) { using namespace system; - using namespace system::multisig; + using namespace system::schnorr; + const auto hash = bitcoin_hash(to_chunk("batch-schnorr")); - const auto hash = bitcoin_hash(to_chunk("batch-multisig")); + ec_compressed pub0{}, pub1{}, pub2{}; + BOOST_REQUIRE(secret_to_public(pub0, secret0)); + BOOST_REQUIRE(secret_to_public(pub1, secret1)); + BOOST_REQUIRE(secret_to_public(pub2, one)); - ec_compressed point1{}, point2{}, point3{}; - BOOST_REQUIRE(secret_to_public(point1, secret1)); - BOOST_REQUIRE(secret_to_public(point2, secret3)); - BOOST_REQUIRE(secret_to_public(point3, one)); + const auto& point0 = array_cast(pub0); + const auto& point1 = array_cast(pub1); + const auto& point2 = array_cast(pub2); - ec_signature sig1{}, sig2{}, sig3{}; - BOOST_REQUIRE(ecdsa::sign(sig1, secret1, hash)); - BOOST_REQUIRE(ecdsa::sign(sig2, secret3, hash)); - BOOST_REQUIRE(ecdsa::sign(sig3, one, hash)); + ec_signature sig0{}, sig1{}, sig2{}; + constexpr hash_digest auxiliary{}; + BOOST_REQUIRE(sign(sig0, secret0, hash, auxiliary)); + BOOST_REQUIRE(sign(sig1, secret1, hash, auxiliary)); + BOOST_REQUIRE(sign(sig2, one, hash, auxiliary)); - // One 2-of-3 multisig group (same id + set) const std::array triples { - batch{ hash, point1, sig1, 0x12, 5, { 0, 0, 0 } }, // (sig 1, key 2) - batch{ hash, point2, sig2, 0x01, 5, { 0, 0, 0 } }, // (sig 0, key 1) - batch{ hash, point3, sig3, 0x20, 5, { 0, 0, 0 } } // (sig 2, key 0) + batch{ hash, point0, sig0, 0, 0, 0, { 0, 0, 0 } }, + batch{ hash, point1, sig1, 0, 0, 0, { 1, 0, 0 } }, + batch{ hash, point2, sig2, 0, 0, 0, { 2, 0, 0 } } }; stopper cancel{}; @@ -196,37 +262,132 @@ BOOST_AUTO_TEST_CASE(secp256k1__multisig_batch_verify__all_valid__expected) BOOST_REQUIRE(tokens.empty()); } -BOOST_AUTO_TEST_CASE(secp256k1__multisig_batch_verify__one_invalid__expected) +BOOST_AUTO_TEST_CASE(secp256k1__schnorr_batch_verify__single_one_invalid__expected) { using namespace system; - using namespace system::multisig; - const auto hash = bitcoin_hash(to_chunk("batch-multisig-invalid")); + using namespace system::schnorr; + const auto hash = bitcoin_hash(to_chunk("batch-schnorr-neg")); - ec_compressed point1{}, point2{}, point3{}; - BOOST_REQUIRE(secret_to_public(point1, secret1)); - BOOST_REQUIRE(secret_to_public(point2, secret3)); - BOOST_REQUIRE(secret_to_public(point3, one)); + ec_compressed pub0{}, pub1{}, pub2{}; + BOOST_REQUIRE(secret_to_public(pub0, secret0)); + BOOST_REQUIRE(secret_to_public(pub1, secret1)); + BOOST_REQUIRE(secret_to_public(pub2, one)); - ec_signature sig1{}, sig2{}, sig3{}; - BOOST_REQUIRE(ecdsa::sign(sig1, secret1, hash)); - BOOST_REQUIRE(ecdsa::sign(sig2, secret3, hash)); - BOOST_REQUIRE(ecdsa::sign(sig3, one, hash)); + const auto& point0 = array_cast(pub0); + const auto& point1 = array_cast(pub1); + const auto& point2 = array_cast(pub2); - // corrupt second signature - sig2[10] ^= 0xff; + ec_signature sig0{}, sig1{}, sig2{}; + constexpr hash_digest auxiliary{}; + BOOST_REQUIRE(sign(sig0, secret0, hash, auxiliary)); + BOOST_REQUIRE(sign(sig1, secret1, hash, auxiliary)); + BOOST_REQUIRE(sign(sig2, one, hash, auxiliary)); - // Same group (id + set) + // corrupt second signature + sig1[10] ^= 0xff; const std::array triples { - batch{ hash, point1, sig1, 0x12, 7, { 0, 0, 0 } }, - batch{ hash, point2, sig2, 0x01, 7, { 0, 0, 0 } }, - batch{ hash, point3, sig3, 0x20, 7, { 0, 0, 0 } } + batch{ hash, point0, sig0, 0, 0, 0, { 0, 0, 0 } }, + batch{ hash, point1, sig1, 0, 0, 0, { 1, 0, 0 } }, + batch{ hash, point2, sig2, 0, 0, 0, { 2, 0, 0 } } }; stopper cancel{}; const auto tokens = batch::verify(cancel, { triples.data(), triples.size() }); BOOST_REQUIRE_EQUAL(tokens.size(), 1u); - BOOST_REQUIRE_EQUAL(tokens.front(), from_little_array(triples.at(0).id)); + BOOST_REQUIRE_EQUAL(tokens.front(), from_little_array(triples.at(1).id)); +} + +// meets_threshold + +struct schnorr_accessor + : public schnorr::batch +{ + using schnorr::batch::meets_threshold; +}; + +using category_t = system::chain::threshold::category_t; + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__equal__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::equal), 5, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__equal__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::equal), 4, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__inequal__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::inequal), 4, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__inequal__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::inequal), 5, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__lesser__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::lesser), 3, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__lesser__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::lesser), 5, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__greater__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::greater), 7, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__greater__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::greater), 5, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__not_lesser__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::not_lesser), 5, 5, 0)); + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::not_lesser), 7, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__not_lesser__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::not_lesser), 3, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__not_greater__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::not_greater), 5, 5, 0)); + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::not_greater), 3, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__not_greater__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::not_greater), 7, 5, 0)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__between__success) +{ + BOOST_REQUIRE(schnorr_accessor::meets_threshold(to_value(category_t::between), 5, 3, 7)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__between__failure_below) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::between), 2, 3, 7)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__between__failure_above) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::between), 8, 3, 7)); +} + +BOOST_AUTO_TEST_CASE(schnorr_batch__meets_threshold__unknown_category__failure) +{ + BOOST_REQUIRE(!schnorr_accessor::meets_threshold(to_value(category_t::unknown), 5, 5, 5)); } BOOST_AUTO_TEST_SUITE_END()