mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-11-10 22:18:54 +01:00
Merge bitcoin/bitcoin#24149: Signing support for Miniscript Descriptors
6c7a17a8e0psbt: support externally provided preimages for Miniscript satisfaction (Antoine Poinsot)840a396029qa: add a "smart" Miniscript fuzz target (Antoine Poinsot)17e3547241qa: add a fuzz target generating random nodes from a binary encoding (Antoine Poinsot)611e12502aqa: functional test Miniscript signing with key and timelocks (Antoine Poinsot)d57b7f2021refactor: make descriptors in Miniscript functional test more readable (Antoine Poinsot)0a8fc9e200wallet: check solvability using descriptor in AvailableCoins (Antoine Poinsot)560e62b1e2script/sign: signing support for Miniscripts with hash preimage challenges (Antoine Poinsot)a2f81b6a8fscript/sign: signing support for Miniscript with timelocks (Antoine Poinsot)61c6d1a844script/sign: basic signing support for Miniscript descriptors (Antoine Poinsot)4242c1c521Align 'e' property of or_d and andor with website spec (Pieter Wuille)f5deb41780Various additional explanations of the satisfaction logic from Pieter (Pieter Wuille)22c5b00345miniscript: satisfaction support (Antoine Poinsot) Pull request description: This makes the Miniscript descriptors solvable. Note this introduces signing support for much more complex scripts than the wallet was previously able to solve, and the whole tooling isn't provided for a complete Miniscript integration in the wallet. Particularly, the PSBT<->Miniscript integration isn't entirely covered in this PR. ACKs for top commit: achow101: ACK6c7a17a8e0sipa: utACK6c7a17a8e0(to the extent that it's not my own code). Tree-SHA512: a71ec002aaf66bd429012caa338fc58384067bcd2f453a46e21d381ed1bacc8e57afb9db57c0fb4bf40de43b30808815e9ebc0ae1fbd9e61df0e7b91a17771cc
This commit is contained in:
@@ -2,18 +2,23 @@
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <test/util/setup_common.h>
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <core_io.h>
|
||||
#include <hash.h>
|
||||
#include <pubkey.h>
|
||||
#include <uint256.h>
|
||||
#include <crypto/ripemd160.h>
|
||||
#include <crypto/sha256.h>
|
||||
#include <script/interpreter.h>
|
||||
#include <script/miniscript.h>
|
||||
#include <script/standard.h>
|
||||
#include <script/script_error.h>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -24,15 +29,22 @@ struct TestData {
|
||||
//! A map from the public keys to their CKeyIDs (faster than hashing every time).
|
||||
std::map<CPubKey, CKeyID> pkhashes;
|
||||
std::map<CKeyID, CPubKey> pkmap;
|
||||
std::map<CPubKey, std::vector<unsigned char>> signatures;
|
||||
|
||||
// Various precomputed hashes
|
||||
std::vector<std::vector<unsigned char>> sha256;
|
||||
std::vector<std::vector<unsigned char>> ripemd160;
|
||||
std::vector<std::vector<unsigned char>> hash256;
|
||||
std::vector<std::vector<unsigned char>> hash160;
|
||||
std::map<std::vector<unsigned char>, std::vector<unsigned char>> sha256_preimages;
|
||||
std::map<std::vector<unsigned char>, std::vector<unsigned char>> ripemd160_preimages;
|
||||
std::map<std::vector<unsigned char>, std::vector<unsigned char>> hash256_preimages;
|
||||
std::map<std::vector<unsigned char>, std::vector<unsigned char>> hash160_preimages;
|
||||
|
||||
TestData()
|
||||
{
|
||||
// All our signatures sign (and are required to sign) this constant message.
|
||||
auto const MESSAGE_HASH = uint256S("f5cd94e18b6fe77dd7aca9e35c2b0c9cbd86356c80a71065");
|
||||
// We generate 255 public keys and 255 hashes of each type.
|
||||
for (int i = 1; i <= 255; ++i) {
|
||||
// This 32-byte array functions as both private key data and hash preimage (31 zero bytes plus any nonzero byte).
|
||||
@@ -48,18 +60,28 @@ struct TestData {
|
||||
pkhashes.emplace(pubkey, keyid);
|
||||
pkmap.emplace(keyid, pubkey);
|
||||
|
||||
// Compute ECDSA signatures on MESSAGE_HASH with the private keys.
|
||||
std::vector<unsigned char> sig;
|
||||
BOOST_CHECK(key.Sign(MESSAGE_HASH, sig));
|
||||
sig.push_back(1); // sighash byte
|
||||
signatures.emplace(pubkey, sig);
|
||||
|
||||
// Compute various hashes
|
||||
std::vector<unsigned char> hash;
|
||||
hash.resize(32);
|
||||
CSHA256().Write(keydata, 32).Finalize(hash.data());
|
||||
sha256.push_back(hash);
|
||||
sha256_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
|
||||
CHash256().Write(keydata).Finalize(hash);
|
||||
hash256.push_back(hash);
|
||||
hash256_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
|
||||
hash.resize(20);
|
||||
CRIPEMD160().Write(keydata, 32).Finalize(hash.data());
|
||||
ripemd160.push_back(hash);
|
||||
ripemd160_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
|
||||
CHash160().Write(keydata).Finalize(hash);
|
||||
hash160.push_back(hash);
|
||||
hash160_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -67,7 +89,27 @@ struct TestData {
|
||||
//! Global TestData object
|
||||
std::unique_ptr<const TestData> g_testdata;
|
||||
|
||||
/** A class encapsulating conversion routing for CPubKey. */
|
||||
//! A classification of leaf conditions in miniscripts (excluding true/false).
|
||||
enum class ChallengeType {
|
||||
SHA256,
|
||||
RIPEMD160,
|
||||
HASH256,
|
||||
HASH160,
|
||||
OLDER,
|
||||
AFTER,
|
||||
PK
|
||||
};
|
||||
|
||||
/* With each leaf condition we associate a challenge number.
|
||||
* For hashes it's just the first 4 bytes of the hash. For pubkeys, it's the last 4 bytes.
|
||||
*/
|
||||
uint32_t ChallengeNumber(const CPubKey& pubkey) { return ReadLE32(pubkey.data() + 29); }
|
||||
uint32_t ChallengeNumber(const std::vector<unsigned char>& hash) { return ReadLE32(hash.data()); }
|
||||
|
||||
//! A Challenge is a combination of type of leaf condition and its challenge number.
|
||||
typedef std::pair<ChallengeType, uint32_t> Challenge;
|
||||
|
||||
/** A class encapulating conversion routing for CPubKey. */
|
||||
struct KeyConverter {
|
||||
typedef CPubKey Key;
|
||||
|
||||
@@ -117,12 +159,197 @@ struct KeyConverter {
|
||||
}
|
||||
};
|
||||
|
||||
/** A class that encapsulates all signing/hash revealing operations. */
|
||||
struct Satisfier : public KeyConverter {
|
||||
//! Which keys/timelocks/hash preimages are available.
|
||||
std::set<Challenge> supported;
|
||||
|
||||
//! Implement simplified CLTV logic: stack value must exactly match an entry in `supported`.
|
||||
bool CheckAfter(uint32_t value) const {
|
||||
return supported.count(Challenge(ChallengeType::AFTER, value));
|
||||
}
|
||||
|
||||
//! Implement simplified CSV logic: stack value must exactly match an entry in `supported`.
|
||||
bool CheckOlder(uint32_t value) const {
|
||||
return supported.count(Challenge(ChallengeType::OLDER, value));
|
||||
}
|
||||
|
||||
//! Produce a signature for the given key.
|
||||
miniscript::Availability Sign(const CPubKey& key, std::vector<unsigned char>& sig) const {
|
||||
if (supported.count(Challenge(ChallengeType::PK, ChallengeNumber(key)))) {
|
||||
auto it = g_testdata->signatures.find(key);
|
||||
if (it == g_testdata->signatures.end()) return miniscript::Availability::NO;
|
||||
sig = it->second;
|
||||
return miniscript::Availability::YES;
|
||||
}
|
||||
return miniscript::Availability::NO;
|
||||
}
|
||||
|
||||
//! Helper function for the various hash based satisfactions.
|
||||
miniscript::Availability SatHash(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage, ChallengeType chtype) const {
|
||||
if (!supported.count(Challenge(chtype, ChallengeNumber(hash)))) return miniscript::Availability::NO;
|
||||
const auto& m =
|
||||
chtype == ChallengeType::SHA256 ? g_testdata->sha256_preimages :
|
||||
chtype == ChallengeType::HASH256 ? g_testdata->hash256_preimages :
|
||||
chtype == ChallengeType::RIPEMD160 ? g_testdata->ripemd160_preimages :
|
||||
g_testdata->hash160_preimages;
|
||||
auto it = m.find(hash);
|
||||
if (it == m.end()) return miniscript::Availability::NO;
|
||||
preimage = it->second;
|
||||
return miniscript::Availability::YES;
|
||||
}
|
||||
|
||||
// Functions that produce the preimage for hashes of various types.
|
||||
miniscript::Availability SatSHA256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::SHA256); }
|
||||
miniscript::Availability SatRIPEMD160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::RIPEMD160); }
|
||||
miniscript::Availability SatHASH256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH256); }
|
||||
miniscript::Availability SatHASH160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH160); }
|
||||
};
|
||||
|
||||
/** Mocking signature/timelock checker.
|
||||
*
|
||||
* It holds a pointer to a Satisfier object, to determine which timelocks are supposed to be available.
|
||||
*/
|
||||
class TestSignatureChecker : public BaseSignatureChecker {
|
||||
const Satisfier& ctx;
|
||||
|
||||
public:
|
||||
TestSignatureChecker(const Satisfier& in_ctx LIFETIMEBOUND) : ctx(in_ctx) {}
|
||||
|
||||
bool CheckECDSASignature(const std::vector<unsigned char>& sig, const std::vector<unsigned char>& pubkey, const CScript& scriptcode, SigVersion sigversion) const override {
|
||||
CPubKey pk(pubkey);
|
||||
if (!pk.IsValid()) return false;
|
||||
// Instead of actually running signature validation, check if the signature matches the precomputed one for this key.
|
||||
auto it = g_testdata->signatures.find(pk);
|
||||
if (it == g_testdata->signatures.end()) return false;
|
||||
return sig == it->second;
|
||||
}
|
||||
|
||||
bool CheckLockTime(const CScriptNum& locktime) const override {
|
||||
// Delegate to Satisfier.
|
||||
return ctx.CheckAfter(locktime.GetInt64());
|
||||
}
|
||||
|
||||
bool CheckSequence(const CScriptNum& sequence) const override {
|
||||
// Delegate to Satisfier.
|
||||
return ctx.CheckOlder(sequence.GetInt64());
|
||||
}
|
||||
};
|
||||
|
||||
//! Singleton instance of KeyConverter.
|
||||
const KeyConverter CONVERTER{};
|
||||
|
||||
using Fragment = miniscript::Fragment;
|
||||
using NodeRef = miniscript::NodeRef<CPubKey>;
|
||||
// https://github.com/llvm/llvm-project/issues/53444
|
||||
// NOLINTNEXTLINE(misc-unused-using-decls)
|
||||
using miniscript::operator"" _mst;
|
||||
using Node = miniscript::Node<CPubKey>;
|
||||
|
||||
/** Compute all challenges (pubkeys, hashes, timelocks) that occur in a given Miniscript. */
|
||||
std::set<Challenge> FindChallenges(const NodeRef& ref) {
|
||||
std::set<Challenge> chal;
|
||||
for (const auto& key : ref->keys) {
|
||||
chal.emplace(ChallengeType::PK, ChallengeNumber(key));
|
||||
}
|
||||
if (ref->fragment == miniscript::Fragment::OLDER) {
|
||||
chal.emplace(ChallengeType::OLDER, ref->k);
|
||||
} else if (ref->fragment == miniscript::Fragment::AFTER) {
|
||||
chal.emplace(ChallengeType::AFTER, ref->k);
|
||||
} else if (ref->fragment == miniscript::Fragment::SHA256) {
|
||||
chal.emplace(ChallengeType::SHA256, ChallengeNumber(ref->data));
|
||||
} else if (ref->fragment == miniscript::Fragment::RIPEMD160) {
|
||||
chal.emplace(ChallengeType::RIPEMD160, ChallengeNumber(ref->data));
|
||||
} else if (ref->fragment == miniscript::Fragment::HASH256) {
|
||||
chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->data));
|
||||
} else if (ref->fragment == miniscript::Fragment::HASH160) {
|
||||
chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->data));
|
||||
}
|
||||
for (const auto& sub : ref->subs) {
|
||||
auto sub_chal = FindChallenges(sub);
|
||||
chal.insert(sub_chal.begin(), sub_chal.end());
|
||||
}
|
||||
return chal;
|
||||
}
|
||||
|
||||
/** Run random satisfaction tests. */
|
||||
void TestSatisfy(const std::string& testcase, const NodeRef& node) {
|
||||
auto script = node->ToScript(CONVERTER);
|
||||
auto challenges = FindChallenges(node); // Find all challenges in the generated miniscript.
|
||||
std::vector<Challenge> challist(challenges.begin(), challenges.end());
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
Shuffle(challist.begin(), challist.end(), g_insecure_rand_ctx);
|
||||
Satisfier satisfier;
|
||||
TestSignatureChecker checker(satisfier);
|
||||
bool prev_mal_success = false, prev_nonmal_success = false;
|
||||
// Go over all challenges involved in this miniscript in random order.
|
||||
for (int add = -1; add < (int)challist.size(); ++add) {
|
||||
if (add >= 0) satisfier.supported.insert(challist[add]); // The first iteration does not add anything
|
||||
|
||||
// Run malleable satisfaction algorithm.
|
||||
const CScript script_pubkey = CScript() << OP_0 << WitnessV0ScriptHash(script);
|
||||
CScriptWitness witness_mal;
|
||||
const bool mal_success = node->Satisfy(satisfier, witness_mal.stack, false) == miniscript::Availability::YES;
|
||||
witness_mal.stack.push_back(std::vector<unsigned char>(script.begin(), script.end()));
|
||||
|
||||
// Run non-malleable satisfaction algorithm.
|
||||
CScriptWitness witness_nonmal;
|
||||
const bool nonmal_success = node->Satisfy(satisfier, witness_nonmal.stack, true) == miniscript::Availability::YES;
|
||||
witness_nonmal.stack.push_back(std::vector<unsigned char>(script.begin(), script.end()));
|
||||
|
||||
if (nonmal_success) {
|
||||
// Non-malleable satisfactions are bounded by GetStackSize().
|
||||
BOOST_CHECK(witness_nonmal.stack.size() <= node->GetStackSize());
|
||||
// If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
|
||||
BOOST_CHECK(mal_success);
|
||||
BOOST_CHECK(witness_nonmal.stack == witness_mal.stack);
|
||||
|
||||
// Test non-malleable satisfaction.
|
||||
ScriptError serror;
|
||||
bool res = VerifyScript(CScript(), script_pubkey, &witness_nonmal, STANDARD_SCRIPT_VERIFY_FLAGS, checker, &serror);
|
||||
// Non-malleable satisfactions are guaranteed to be valid if ValidSatisfactions().
|
||||
if (node->ValidSatisfactions()) BOOST_CHECK(res);
|
||||
// More detailed: non-malleable satisfactions must be valid, or could fail with ops count error (if CheckOpsLimit failed),
|
||||
// or with a stack size error (if CheckStackSize check fails).
|
||||
BOOST_CHECK(res ||
|
||||
(!node->CheckOpsLimit() && serror == ScriptError::SCRIPT_ERR_OP_COUNT) ||
|
||||
(!node->CheckStackSize() && serror == ScriptError::SCRIPT_ERR_STACK_SIZE));
|
||||
}
|
||||
|
||||
if (mal_success && (!nonmal_success || witness_mal.stack != witness_nonmal.stack)) {
|
||||
// Test malleable satisfaction only if it's different from the non-malleable one.
|
||||
ScriptError serror;
|
||||
bool res = VerifyScript(CScript(), script_pubkey, &witness_mal, STANDARD_SCRIPT_VERIFY_FLAGS, checker, &serror);
|
||||
// Malleable satisfactions are not guaranteed to be valid under any conditions, but they can only
|
||||
// fail due to stack or ops limits.
|
||||
BOOST_CHECK(res || serror == ScriptError::SCRIPT_ERR_OP_COUNT || serror == ScriptError::SCRIPT_ERR_STACK_SIZE);
|
||||
}
|
||||
|
||||
if (node->IsSane()) {
|
||||
// For sane nodes, the two algorithms behave identically.
|
||||
BOOST_CHECK_EQUAL(mal_success, nonmal_success);
|
||||
}
|
||||
|
||||
// Adding more satisfied conditions can never remove our ability to produce a satisfaction.
|
||||
BOOST_CHECK(mal_success >= prev_mal_success);
|
||||
// For nonmalleable solutions this is only true if the added condition is PK;
|
||||
// for other conditions, adding one may make an valid satisfaction become malleable. If the script
|
||||
// is sane, this cannot happen however.
|
||||
if (node->IsSane() || add < 0 || challist[add].first == ChallengeType::PK) {
|
||||
BOOST_CHECK(nonmal_success >= prev_nonmal_success);
|
||||
}
|
||||
// Remember results for the next added challenge.
|
||||
prev_mal_success = mal_success;
|
||||
prev_nonmal_success = nonmal_success;
|
||||
}
|
||||
|
||||
bool satisfiable = node->IsSatisfiable([](const Node&) { return true; });
|
||||
// If the miniscript was satisfiable at all, a satisfaction must be found after all conditions are added.
|
||||
BOOST_CHECK_EQUAL(prev_mal_success, satisfiable);
|
||||
// If the miniscript is sane and satisfiable, a nonmalleable satisfaction must eventually be found.
|
||||
if (node->IsSane()) BOOST_CHECK_EQUAL(prev_nonmal_success, satisfiable);
|
||||
}
|
||||
}
|
||||
|
||||
enum TestMode : int {
|
||||
TESTMODE_INVALID = 0,
|
||||
@@ -152,6 +379,7 @@ void Test(const std::string& ms, const std::string& hexscript, int mode, int ops
|
||||
BOOST_CHECK_MESSAGE(inferred_miniscript->ToScript(CONVERTER) == computed_script, "Roundtrip failure: miniscript->script != miniscript->script->miniscript->script: " + ms);
|
||||
if (opslimit != -1) BOOST_CHECK_MESSAGE((int)node->GetOps() == opslimit, "Ops limit mismatch: " << ms << " (" << node->GetOps() << " vs " << opslimit << ")");
|
||||
if (stacklimit != -1) BOOST_CHECK_MESSAGE((int)node->GetStackSize() == stacklimit, "Stack limit mismatch: " << ms << " (" << node->GetStackSize() << " vs " << stacklimit << ")");
|
||||
TestSatisfy(ms, node);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user