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()