miniscript: satisfaction support

This introduces the logic to "sign for" a Miniscript.

Co-Authored-By: Pieter Wuille <pieter.wuille@gmail.com>
This commit is contained in:
Antoine Poinsot
2021-08-24 15:39:47 +02:00
parent d0b1f613c2
commit 22c5b00345
3 changed files with 598 additions and 2 deletions

View File

@@ -279,6 +279,76 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_
assert(false);
}
InputStack& InputStack::SetAvailable(Availability avail) {
available = avail;
if (avail == Availability::NO) {
stack.clear();
size = std::numeric_limits<size_t>::max();
has_sig = false;
malleable = false;
non_canon = false;
}
return *this;
}
InputStack& InputStack::SetWithSig() {
has_sig = true;
return *this;
}
InputStack& InputStack::SetNonCanon() {
non_canon = true;
return *this;
}
InputStack& InputStack::SetMalleable(bool x) {
malleable = x;
return *this;
}
InputStack operator+(InputStack a, InputStack b) {
a.stack = Cat(std::move(a.stack), std::move(b.stack));
if (a.available != Availability::NO && b.available != Availability::NO) a.size += b.size;
a.has_sig |= b.has_sig;
a.malleable |= b.malleable;
a.non_canon |= b.non_canon;
if (a.available == Availability::NO || b.available == Availability::NO) {
a.SetAvailable(Availability::NO);
} else if (a.available == Availability::MAYBE || b.available == Availability::MAYBE) {
a.SetAvailable(Availability::MAYBE);
}
return a;
}
InputStack operator|(InputStack a, InputStack b) {
// If only one is invalid, pick the other one. If both are invalid, pick an arbitrary one.
if (a.available == Availability::NO) return b;
if (b.available == Availability::NO) return a;
// If only one of the solutions has a signature, we must pick the other one.
if (!a.has_sig && b.has_sig) return a;
if (!b.has_sig && a.has_sig) return b;
if (!a.has_sig && !b.has_sig) {
// If neither solution requires a signature, the result is inevitably malleable.
a.malleable = true;
b.malleable = true;
} else {
// If both options require a signature, prefer the non-malleable one.
if (b.malleable && !a.malleable) return a;
if (a.malleable && !b.malleable) return b;
}
// Between two malleable or two non-malleable solutions, pick the smaller one between
// YESes, and the bigger ones between MAYBEs. Prefer YES over MAYBE.
if (a.available == Availability::YES && b.available == Availability::YES) {
return std::move(a.size <= b.size ? a : b);
} else if (a.available == Availability::MAYBE && b.available == Availability::MAYBE) {
return std::move(a.size >= b.size ? a : b);
} else if (a.available == Availability::YES) {
return a;
} else {
return b;
}
}
std::optional<std::vector<Opcode>> DecomposeScript(const CScript& script)
{
std::vector<Opcode> out;

View File

@@ -223,6 +223,11 @@ enum class Fragment {
// WRAP_U(X) is represented as OR_I(X,0)
};
enum class Availability {
NO,
YES,
MAYBE,
};
namespace internal {
@@ -235,6 +240,62 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_
//! A helper sanitizer/checker for the output of CalcType.
Type SanitizeType(Type x);
//! An object representing a sequence of witness stack elements.
struct InputStack {
/** Whether this stack is valid for its intended purpose (satisfaction or dissatisfaction of a Node).
* The MAYBE value is used for size estimation, when keys/preimages may actually be unavailable,
* but may be available at signing time. This makes the InputStack structure and signing logic,
* filled with dummy signatures/preimages usable for witness size estimation.
*/
Availability available = Availability::YES;
//! Whether this stack contains a digital signature.
bool has_sig = false;
//! Whether this stack is malleable (can be turned into an equally valid other stack by a third party).
bool malleable = false;
//! Whether this stack is non-canonical (using a construction known to be unnecessary for satisfaction).
//! Note that this flag does not affect the satisfaction algorithm; it is only used for sanity checking.
bool non_canon = false;
//! Serialized witness size.
size_t size = 0;
//! Data elements.
std::vector<std::vector<unsigned char>> stack;
//! Construct an empty stack (valid).
InputStack() {}
//! Construct a valid single-element stack (with an element up to 75 bytes).
InputStack(std::vector<unsigned char> in) : size(in.size() + 1), stack(Vector(std::move(in))) {}
//! Change availability
InputStack& SetAvailable(Availability avail);
//! Mark this input stack as having a signature.
InputStack& SetWithSig();
//! Mark this input stack as non-canonical (known to not be necessary in non-malleable satisfactions).
InputStack& SetNonCanon();
//! Mark this input stack as malleable.
InputStack& SetMalleable(bool x = true);
//! Concatenate two input stacks.
friend InputStack operator+(InputStack a, InputStack b);
//! Choose between two potential input stacks.
friend InputStack operator|(InputStack a, InputStack b);
};
/** A stack consisting of a single zero-length element (interpreted as 0 by the script interpreter in numeric context). */
static const auto ZERO = InputStack(std::vector<unsigned char>());
/** A stack consisting of a single malleable 32-byte 0x0000...0000 element (for dissatisfying hash challenges). */
static const auto ZERO32 = InputStack(std::vector<unsigned char>(32, 0)).SetMalleable();
/** A stack consisting of a single 0x01 element (interpreted as 1 by the script interpreted in numeric context). */
static const auto ONE = InputStack(Vector((unsigned char)1));
/** The empty stack. */
static const auto EMPTY = InputStack();
/** A stack representing the lack of any (dis)satisfactions. */
static const auto INVALID = InputStack().SetAvailable(Availability::NO);
//! A pair of a satisfaction and a dissatisfaction InputStack.
struct InputResult {
InputStack nsat, sat;
template<typename A, typename B>
InputResult(A&& in_nsat, B&& in_sat) : nsat(std::forward<A>(in_nsat)), sat(std::forward<B>(in_sat)) {}
};
//! Class whose objects represent the maximum of a list of integers.
template<typename I>
struct MaxInt {
@@ -785,6 +846,190 @@ private:
assert(false);
}
template<typename Ctx>
internal::InputResult ProduceInput(const Ctx& ctx) const {
using namespace internal;
// Internal function which is invoked for every tree node, constructing satisfaction/dissatisfactions
// given those of its subnodes.
auto helper = [&ctx](const Node& node, Span<InputResult> subres) -> InputResult {
switch (node.fragment) {
case Fragment::PK_K: {
std::vector<unsigned char> sig;
Availability avail = ctx.Sign(node.keys[0], sig);
return {ZERO, InputStack(std::move(sig)).SetWithSig().SetAvailable(avail)};
}
case Fragment::PK_H: {
std::vector<unsigned char> key = ctx.ToPKBytes(node.keys[0]), sig;
Availability avail = ctx.Sign(node.keys[0], sig);
return {ZERO + InputStack(key), (InputStack(std::move(sig)).SetWithSig() + InputStack(key)).SetAvailable(avail)};
}
case Fragment::MULTI: {
std::vector<InputStack> sats = Vector(ZERO);
for (size_t i = 0; i < node.keys.size(); ++i) {
std::vector<unsigned char> sig;
Availability avail = ctx.Sign(node.keys[i], sig);
auto sat = InputStack(std::move(sig)).SetWithSig().SetAvailable(avail);
std::vector<InputStack> next_sats;
next_sats.push_back(sats[0]);
for (size_t j = 1; j < sats.size(); ++j) next_sats.push_back(sats[j] | (std::move(sats[j - 1]) + sat));
next_sats.push_back(std::move(sats[sats.size() - 1]) + std::move(sat));
sats = std::move(next_sats);
}
InputStack nsat = ZERO;
for (size_t i = 0; i < node.k; ++i) nsat = std::move(nsat) + ZERO;
assert(node.k <= sats.size());
return {std::move(nsat), std::move(sats[node.k])};
}
case Fragment::THRESH: {
std::vector<InputStack> sats = Vector(EMPTY);
for (size_t i = 0; i < subres.size(); ++i) {
auto& res = subres[subres.size() - i - 1];
std::vector<InputStack> next_sats;
next_sats.push_back(sats[0] + res.nsat);
for (size_t j = 1; j < sats.size(); ++j) next_sats.push_back((sats[j] + res.nsat) | (std::move(sats[j - 1]) + res.sat));
next_sats.push_back(std::move(sats[sats.size() - 1]) + std::move(res.sat));
sats = std::move(next_sats);
}
InputStack nsat = INVALID;
for (size_t i = 0; i < sats.size(); ++i) {
// i==k is the satisfaction; i==0 is the canonical dissatisfaction; the rest are non-canonical.
if (i != 0 && i != node.k) sats[i].SetNonCanon();
if (i != node.k) nsat = std::move(nsat) | std::move(sats[i]);
}
assert(node.k <= sats.size());
return {std::move(nsat), std::move(sats[node.k])};
}
case Fragment::OLDER: {
return {INVALID, ctx.CheckOlder(node.k) ? EMPTY : INVALID};
}
case Fragment::AFTER: {
return {INVALID, ctx.CheckAfter(node.k) ? EMPTY : INVALID};
}
case Fragment::SHA256: {
std::vector<unsigned char> preimage;
Availability avail = ctx.SatSHA256(node.data, preimage);
return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
}
case Fragment::RIPEMD160: {
std::vector<unsigned char> preimage;
Availability avail = ctx.SatRIPEMD160(node.data, preimage);
return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
}
case Fragment::HASH256: {
std::vector<unsigned char> preimage;
Availability avail = ctx.SatHASH256(node.data, preimage);
return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
}
case Fragment::HASH160: {
std::vector<unsigned char> preimage;
Availability avail = ctx.SatHASH160(node.data, preimage);
return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
}
case Fragment::AND_V: {
auto& x = subres[0], &y = subres[1];
return {(y.nsat + x.sat).SetNonCanon(), y.sat + x.sat};
}
case Fragment::AND_B: {
auto& x = subres[0], &y = subres[1];
return {(y.nsat + x.nsat) | (y.sat + x.nsat).SetMalleable().SetNonCanon() | (y.nsat + x.sat).SetMalleable().SetNonCanon(), y.sat + x.sat};
}
case Fragment::OR_B: {
auto& x = subres[0], &z = subres[1];
// The (sat(Z) sat(X)) solution is overcomplete (attacker can change either into dsat).
return {z.nsat + x.nsat, (z.nsat + x.sat) | (z.sat + x.nsat) | (z.sat + x.sat).SetMalleable().SetNonCanon()};
}
case Fragment::OR_C: {
auto& x = subres[0], &z = subres[1];
return {INVALID, std::move(x.sat) | (z.sat + x.nsat)};
}
case Fragment::OR_D: {
auto& x = subres[0], &z = subres[1];
return {z.nsat + x.nsat, std::move(x.sat) | (z.sat + x.nsat)};
}
case Fragment::OR_I: {
auto& x = subres[0], &z = subres[1];
return {(x.nsat + ONE) | (z.nsat + ZERO), (x.sat + ONE) | (z.sat + ZERO)};
}
case Fragment::ANDOR: {
auto& x = subres[0], &y = subres[1], &z = subres[2];
return {(y.nsat + x.sat).SetNonCanon() | (z.nsat + x.nsat), (y.sat + x.sat) | (z.sat + x.nsat)};
}
case Fragment::WRAP_A:
case Fragment::WRAP_S:
case Fragment::WRAP_C:
case Fragment::WRAP_N:
return std::move(subres[0]);
case Fragment::WRAP_D: {
auto &x = subres[0];
return {ZERO, x.sat + ONE};
}
case Fragment::WRAP_J: {
auto &x = subres[0];
// If a dissatisfaction with a nonzero top stack element exists, an alternative dissatisfaction exists.
// As the dissatisfaction logic currently doesn't keep track of this nonzeroness property, and thus even
// if a dissatisfaction with a top zero element is found, we don't know whether another one with a
// nonzero top stack element exists. Make the conservative assumption that whenever the subexpression is weakly
// dissatisfiable, this alternative dissatisfaction exists and leads to malleability.
return {InputStack(ZERO).SetMalleable(x.nsat.available != Availability::NO && !x.nsat.has_sig), std::move(x.sat)};
}
case Fragment::WRAP_V: {
auto &x = subres[0];
return {INVALID, std::move(x.sat)};
}
case Fragment::JUST_0: return {EMPTY, INVALID};
case Fragment::JUST_1: return {INVALID, EMPTY};
}
assert(false);
return {INVALID, INVALID};
};
auto tester = [&helper](const Node& node, Span<InputResult> subres) -> InputResult {
auto ret = helper(node, subres);
// Do a consistency check between the satisfaction code and the type checker
// (the actual satisfaction code in ProduceInputHelper does not use GetType)
// For 'z' nodes, available satisfactions/dissatisfactions must have stack size 0.
if (node.GetType() << "z"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.stack.size() == 0);
if (node.GetType() << "z"_mst && ret.sat.available != Availability::NO) assert(ret.sat.stack.size() == 0);
// For 'o' nodes, available satisfactions/dissatisfactions must have stack size 1.
if (node.GetType() << "o"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.stack.size() == 1);
if (node.GetType() << "o"_mst && ret.sat.available != Availability::NO) assert(ret.sat.stack.size() == 1);
// For 'n' nodes, available satisfactions/dissatisfactions must have stack size 1 or larger. For satisfactions,
// the top element cannot be 0.
if (node.GetType() << "n"_mst && ret.sat.available != Availability::NO) assert(ret.sat.stack.size() >= 1);
if (node.GetType() << "n"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.stack.size() >= 1);
if (node.GetType() << "n"_mst && ret.sat.available != Availability::NO) assert(!ret.sat.stack.back().empty());
// For 'd' nodes, a dissatisfaction must exist, and they must not need a signature. If it is non-malleable,
// it must be canonical.
if (node.GetType() << "d"_mst) assert(ret.nsat.available != Availability::NO);
if (node.GetType() << "d"_mst) assert(!ret.nsat.has_sig);
if (node.GetType() << "d"_mst && !ret.nsat.malleable) assert(!ret.nsat.non_canon);
// For 'f'/'s' nodes, dissatisfactions/satisfactions must have a signature.
if (node.GetType() << "f"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.has_sig);
if (node.GetType() << "s"_mst && ret.sat.available != Availability::NO) assert(ret.sat.has_sig);
// For 'e' nodes, a non-malleable dissatisfaction must exist.
if (node.GetType() << "e"_mst) assert(ret.nsat.available != Availability::NO);
if (node.GetType() << "e"_mst) assert(!ret.nsat.malleable);
// For 'm' nodes, if a satisfaction exists, it must be non-malleable.
if (node.GetType() << "m"_mst && ret.sat.available != Availability::NO) assert(!ret.sat.malleable);
// If a non-malleable satisfaction exists, it must be canonical.
if (ret.sat.available != Availability::NO && !ret.sat.malleable) assert(!ret.sat.non_canon);
return ret;
};
return TreeEval<InputResult>(tester);
}
public:
/** Update duplicate key information in this Node.
*
@@ -877,6 +1122,47 @@ public:
});
}
//! Determine whether a Miniscript node is satisfiable. fn(node) will be invoked for all
//! key, time, and hashing nodes, and should return their satisfiability.
template<typename F>
bool IsSatisfiable(F fn) const
{
// TreeEval() doesn't support bool as NodeType, so use int instead.
return TreeEval<int>([&fn](const Node& node, Span<int> subs) -> bool {
switch (node.fragment) {
case Fragment::JUST_0:
return false;
case Fragment::JUST_1:
return true;
case Fragment::PK_K:
case Fragment::PK_H:
case Fragment::MULTI:
case Fragment::AFTER:
case Fragment::OLDER:
case Fragment::HASH256:
case Fragment::HASH160:
case Fragment::SHA256:
case Fragment::RIPEMD160:
return bool{fn(node)};
case Fragment::ANDOR:
return (subs[0] && subs[1]) || subs[2];
case Fragment::AND_V:
case Fragment::AND_B:
return subs[0] && subs[1];
case Fragment::OR_B:
case Fragment::OR_C:
case Fragment::OR_D:
case Fragment::OR_I:
return subs[0] || subs[1];
case Fragment::THRESH:
return std::count(subs.begin(), subs.end(), true) >= node.k;
default: // wrappers
assert(subs.size() == 1);
return subs[0];
}
});
}
//! Check whether this node is valid at all.
bool IsValid() const { return !(GetType() == ""_mst) && ScriptSize() <= MAX_STANDARD_P2WSH_SCRIPT_SIZE; }
@@ -904,6 +1190,18 @@ public:
//! Check whether this node is safe as a script on its own.
bool IsSane() const { return IsValidTopLevel() && IsSaneSubexpression() && NeedsSignature(); }
//! Produce a witness for this script, if possible and given the information available in the context.
//! The non-malleable satisfaction is guaranteed to be valid if it exists, and ValidSatisfaction()
//! is true. If IsSane() holds, this satisfaction is guaranteed to succeed in case the node's
//! conditions are satisfied (private keys and hash preimages available, locktimes satsified).
template<typename Ctx>
Availability Satisfy(const Ctx& ctx, std::vector<std::vector<unsigned char>>& stack, bool nonmalleable = true) const {
auto ret = ProduceInput(ctx);
if (nonmalleable && (ret.sat.malleable || !ret.sat.has_sig)) return Availability::NO;
stack = std::move(ret.sat.stack);
return ret.sat.available;
}
//! Equality testing.
bool operator==(const Node<Key>& arg) const { return Compare(*this, arg) == 0; }