Merge bitcoin/bitcoin#24149: Signing support for Miniscript Descriptors

6c7a17a8e0 psbt: support externally provided preimages for Miniscript satisfaction (Antoine Poinsot)
840a396029 qa: add a "smart" Miniscript fuzz target (Antoine Poinsot)
17e3547241 qa: add a fuzz target generating random nodes from a binary encoding (Antoine Poinsot)
611e12502a qa: functional test Miniscript signing with key and timelocks (Antoine Poinsot)
d57b7f2021 refactor: make descriptors in Miniscript functional test more readable (Antoine Poinsot)
0a8fc9e200 wallet: check solvability using descriptor in AvailableCoins (Antoine Poinsot)
560e62b1e2 script/sign: signing support for Miniscripts with hash preimage challenges (Antoine Poinsot)
a2f81b6a8f script/sign: signing support for Miniscript with timelocks (Antoine Poinsot)
61c6d1a844 script/sign: basic signing support for Miniscript descriptors (Antoine Poinsot)
4242c1c521 Align 'e' property of or_d and andor with website spec (Pieter Wuille)
f5deb41780 Various additional explanations of the satisfaction logic from Pieter (Pieter Wuille)
22c5b00345 miniscript: 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:
    ACK 6c7a17a8e0
  sipa:
    utACK 6c7a17a8e0 (to the extent that it's not my own code).

Tree-SHA512: a71ec002aaf66bd429012caa338fc58384067bcd2f453a46e21d381ed1bacc8e57afb9db57c0fb4bf40de43b30808815e9ebc0ae1fbd9e61df0e7b91a17771cc
This commit is contained in:
fanquake
2023-02-16 09:55:21 +00:00
17 changed files with 1879 additions and 58 deletions

View File

@@ -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