Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 232 additions & 20 deletions src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <key_io.h>
#include <pubkey.h>
#include <script/miniscript.h>
#include <script/script.h>
#include <script/standard.h>

Expand Down Expand Up @@ -161,6 +162,20 @@ struct PubkeyProvider

virtual ~PubkeyProvider() = default;

/** Compare two public keys represented by this provider.
* Used by the Miniscript descriptors to check for duplicate keys in the script.
*/
bool operator<(PubkeyProvider& other) const {
CPubKey a, b;
SigningProvider dummy;
KeyOriginInfo dummy_info;

GetPubKey(0, dummy, a, dummy_info);
other.GetPubKey(0, dummy, b, dummy_info);

return a < b;
}

/** Derive a public key.
* read_cache is the cache to read keys from (if not nullptr)
* write_cache is the cache to write keys to (if not nullptr)
Expand Down Expand Up @@ -516,12 +531,12 @@ class BIP32PubkeyProvider final : public PubkeyProvider
/** Base class for all Descriptor implementations. */
class DescriptorImpl : public Descriptor
{
protected:
//! Public key arguments for this descriptor (size 1 for PK, PKH; any size for Multisig).
const std::vector<std::unique_ptr<PubkeyProvider>> m_pubkey_args;
//! The string name of the descriptor function.
const std::string m_name;

protected:
//! The sub-descriptor arguments (empty for everything but SH and WSH).
//! In doc/descriptors.m this is referred to as SCRIPT expressions sh(SCRIPT)
//! and wsh(SCRIPT), and distinct from KEY expressions and ADDR expressions.
Expand Down Expand Up @@ -585,7 +600,7 @@ class DescriptorImpl : public Descriptor
return true;
}

bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type, const DescriptorCache* cache = nullptr) const
virtual bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type, const DescriptorCache* cache = nullptr) const
{
std::string extra = ToStringExtra();
size_t pos = extra.size() > 0 ? 1 : 0;
Expand Down Expand Up @@ -854,6 +869,89 @@ class ComboDescriptor final : public DescriptorImpl
bool IsSingleType() const final { return false; }
};

/* We instantiate Miniscript here with a simple integer as key type.
* The value of these key integers are an index in the
* DescriptorImpl::m_pubkey_args vector.
*/

/**
* The context for converting a Miniscript descriptor into a Script.
*/
class ScriptMaker {
//! Keys contained in the Miniscript (the evaluation of DescriptorImpl::m_pubkey_args).
const std::vector<CPubKey>& m_keys;

public:
ScriptMaker(const std::vector<CPubKey>& keys LIFETIMEBOUND) : m_keys(keys) {}

std::vector<unsigned char> ToPKBytes(uint32_t key) const {
return {m_keys[key].begin(), m_keys[key].end()};
}

std::vector<unsigned char> ToPKHBytes(uint32_t key) const {
auto id = m_keys[key].GetID();
return {id.begin(), id.end()};
}
};

/**
* The context for converting a Miniscript descriptor to its textual form.
*/
class StringMaker {
//! To convert private keys for private descriptors.
const SigningProvider* m_arg;
//! Keys contained in the Miniscript (a reference to DescriptorImpl::m_pubkey_args).
const std::vector<std::unique_ptr<PubkeyProvider>>& m_pubkeys;
//! Whether to serialize keys as private or public.
bool m_private;

public:
StringMaker(const SigningProvider* arg LIFETIMEBOUND, const std::vector<std::unique_ptr<PubkeyProvider>>& pubkeys LIFETIMEBOUND, bool priv)
: m_arg(arg), m_pubkeys(pubkeys), m_private(priv) {}

std::optional<std::string> ToString(uint32_t key) const
{
std::string ret;
if (m_private) {
if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) return {};
} else {
ret = m_pubkeys[key]->ToString();
}
return ret;
}
};

class MiniscriptDescriptor final : public DescriptorImpl
{
private:
miniscript::NodeRef<uint32_t> m_node;

protected:
std::vector<CScript> MakeScripts(const std::vector<CPubKey>& keys, Span<const CScript> scripts,
FlatSigningProvider& provider) const override
{
for (const auto& key : keys) provider.pubkeys.emplace(key.GetID(), key);
return Vector(m_node->ToScript(ScriptMaker(keys)));
}

public:
MiniscriptDescriptor(std::vector<std::unique_ptr<PubkeyProvider>> providers, miniscript::NodeRef<uint32_t> node)
: DescriptorImpl(std::move(providers), "?"), m_node(std::move(node)) {}

bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type,
const DescriptorCache* cache = nullptr) const override
{
if (const auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type == StringType::PRIVATE))) {
out = *res;
return true;
}
return false;
}

bool IsSolvable() const override { return false; } // For now, mark these descriptors as non-solvable (as we don't have signing logic for them).
bool IsSingleType() const final { return true; }
};

////////////////////////////////////////////////////////////////////////////
// Parser //
////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -1003,6 +1101,91 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
return std::make_unique<OriginPubkeyProvider>(key_exp_index, std::move(info), std::move(provider), apostrophe);
}

std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext ctx, const SigningProvider& provider)
{
// Key cannot be hybrid
if (!pubkey.IsValidNonHybrid()) {
return nullptr;
}
// Uncompressed is only allowed in TOP and P2SH contexts
if (ctx != ParseScriptContext::TOP && ctx != ParseScriptContext::P2SH && !pubkey.IsCompressed()) {
return nullptr;
}
std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey);
KeyOriginInfo info;
if (provider.GetKeyOrigin(pubkey.GetID(), info)) {
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider), /*apostrophe=*/false);
}
return key_provider;
}

/**
* The context for parsing a Miniscript descriptor (either from Script or from its textual representation).
*/
struct KeyParser {
//! The Key type is an index in DescriptorImpl::m_pubkey_args
using Key = uint32_t;
//! Must not be nullptr if parsing from string.
FlatSigningProvider* m_out;
//! Must not be nullptr if parsing from Script.
const SigningProvider* m_in;
//! List of keys contained in the Miniscript.
mutable std::vector<std::unique_ptr<PubkeyProvider>> m_keys;
//! Used to detect key parsing errors within a Miniscript.
mutable std::string m_key_parsing_error;

KeyParser(FlatSigningProvider* out LIFETIMEBOUND, const SigningProvider* in LIFETIMEBOUND) : m_out(out), m_in(in) {}

bool KeyCompare(const Key& a, const Key& b) const {
return *m_keys.at(a) < *m_keys.at(b);
}

template<typename I> std::optional<Key> FromString(I begin, I end) const
{
assert(m_out);
Key key = m_keys.size();
auto pk = ParsePubkey(key, {&*begin, &*end}, ParseScriptContext::P2SH, *m_out, m_key_parsing_error);
if (!pk) return {};
m_keys.push_back(std::move(pk));
return key;
}

std::optional<std::string> ToString(const Key& key) const
{
return m_keys.at(key)->ToString();
}

template<typename I> std::optional<Key> FromPKBytes(I begin, I end) const
{
assert(m_in);
CPubKey pubkey(begin, end);
if (auto pubkey_provider = InferPubkey(pubkey, ParseScriptContext::P2SH, *m_in)) {
Key key = m_keys.size();
m_keys.push_back(std::move(pubkey_provider));
return key;
}
return {};
}

template<typename I> std::optional<Key> FromPKHBytes(I begin, I end) const
{
assert(end - begin == 20);
assert(m_in);
uint160 hash;
std::copy(begin, end, hash.begin());
CKeyID keyid(hash);
CPubKey pubkey;
if (m_in->GetPubKey(keyid, pubkey)) {
if (auto pubkey_provider = InferPubkey(pubkey, ParseScriptContext::P2SH, *m_in)) {
Key key = m_keys.size();
m_keys.push_back(std::move(pubkey_provider));
return key;
}
}
return {};
}
};

/** Parse a script in a particular context. */
std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error)
{
Expand Down Expand Up @@ -1120,6 +1303,45 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const
error = "Can only have raw() at top level";
return nullptr;
}
// Process miniscript expressions.
{
KeyParser parser(&out, nullptr);
auto node = miniscript::FromString(std::string(expr.begin(), expr.end()), parser);
if (node) {
if (ctx != ParseScriptContext::P2SH) {
error = "Miniscript expressions can only be used in sh";
return nullptr;
}
if (parser.m_key_parsing_error != "") {
error = std::move(parser.m_key_parsing_error);
return nullptr;
}
if (!node->IsSane()) {
// Try to find the first insane sub for better error reporting.
auto insane_node = node.get();
if (const auto sub = node->FindInsaneSub()) insane_node = sub;
if (const auto str = insane_node->ToString(parser)) error = *str;
if (!insane_node->IsValid()) {
error += " is invalid";
} else {
error += " is not sane";
if (!insane_node->IsNonMalleable()) {
error += ": malleable witnesses exist";
} else if (insane_node == node.get() && !insane_node->NeedsSignature()) {
error += ": witnesses without signature exist";
} else if (!insane_node->CheckTimeLocksMix()) {
error += ": contains mixes of timelocks expressed in blocks and seconds";
} else if (!insane_node->CheckDuplicateKey()) {
error += ": contains duplicate public keys";
} else if (!insane_node->ValidSatisfactions()) {
error += ": needs witnesses that may exceed resource limits";
}
}
return nullptr;
}
return std::make_unique<MiniscriptDescriptor>(std::move(parser.m_keys), std::move(node));
}
}
if (ctx == ParseScriptContext::P2SH) {
error = "A function is needed within P2SH";
return nullptr;
Expand All @@ -1128,24 +1350,6 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const
return nullptr;
}

std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext ctx, const SigningProvider& provider)
{
// Key cannot be hybrid
if (!pubkey.IsValidNonHybrid()) {
return nullptr;
}
// Uncompressed is only allowed in TOP and P2SH contexts
if (ctx != ParseScriptContext::TOP && ctx != ParseScriptContext::P2SH && !pubkey.IsCompressed()) {
return nullptr;
}
std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey);
KeyOriginInfo info;
if (provider.GetKeyOrigin(pubkey.GetID(), info)) {
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider), /*apostrophe=*/false);
}
return key_provider;
}

std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider)
{
std::vector<std::vector<unsigned char>> data;
Expand Down Expand Up @@ -1191,6 +1395,14 @@ std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptCo
}
}

if (ctx == ParseScriptContext::P2SH) {
KeyParser parser(nullptr, &provider);
auto node = miniscript::FromScript(script, parser);
if (node && node->IsSane()) {
return std::make_unique<MiniscriptDescriptor>(std::move(parser.m_keys), std::move(node));
}
}

// The following descriptors are all top-level only descriptors.
// So if we are not at the top level, return early.
if (ctx != ParseScriptContext::TOP) return nullptr;
Expand Down
34 changes: 30 additions & 4 deletions src/script/miniscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,21 @@ struct Node {
));
}

/** Like TreeEval, but without downfn or State type.
* upfn takes (const Node&, Span<Result>) and returns Result. */
template<typename Result, typename UpFn>
Result TreeEval(UpFn upfn) const
{
struct DummyState {};
return std::move(*TreeEvalMaybe<Result>(DummyState{},
[](DummyState, const Node&, size_t) { return DummyState{}; },
[&upfn](DummyState, const Node& node, Span<Result> subs) {
Result res{upfn(node, subs)};
return std::optional<Result>(std::move(res));
}
));
}

/** Compare two miniscript subtrees, using a non-recursive algorithm. */
friend int Compare(const Node<Key>& node1, const Node<Key>& node2)
{
Expand Down Expand Up @@ -815,6 +830,15 @@ struct Node {
//! Return the expression type.
Type GetType() const { return typ; }

//! Find an insane subnode which has no insane children. Nullptr if there is none.
const Node* FindInsaneSub() const {
return TreeEval<const Node*>([](const Node& node, Span<const Node*> subs) -> const Node* {
for (auto& sub: subs) if (sub) return sub;
if (!node.IsSaneSubexpression()) return &node;
return nullptr;
});
}

//! Check whether this node is valid at all.
bool IsValid() const { return !(GetType() == ""_mst); }

Expand Down Expand Up @@ -950,7 +974,11 @@ void BuildBack(const Ctx& ctx, Fragment nt, std::vector<NodeRef<Key>>& construct
}
}

//! Parse a miniscript from its textual descriptor form.
/**
* Parse a miniscript from its textual descriptor form.
* This does not check whether the script is valid, let alone sane. The caller is expected to use
* the `IsValidTopLevel()` and `IsSaneTopLevel()` to check for these properties on the node.
*/
template<typename Key, typename Ctx>
inline NodeRef<Key> Parse(Span<const char> in, const Ctx& ctx)
{
Expand Down Expand Up @@ -1252,9 +1280,7 @@ inline NodeRef<Key> Parse(Span<const char> in, const Ctx& ctx)
// Sanity checks on the produced miniscript
assert(constructed.size() == 1);
if (in.size() > 0) return {};
NodeRef<Key> tl_node = std::move(constructed.front());
if (!tl_node->IsValidTopLevel()) return {};
return tl_node;
return std::move(constructed.front());
}

/** Decode a script into opcode/push pairs.
Expand Down
Loading
Loading