Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement BIP 340-342 validation (Schnorr/taproot/tapscript) #2772

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
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
16 changes: 11 additions & 5 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,18 @@ jobs:
- name: Build binaries
run: ./make.sh build

- name: Unit tests
run: ./make.sh test-cpp

# Temp. workaround to save space to prevent GH runners from running out
- name: Cleanup cargo deps
run: rm -rf build/lib/target/debug/deps

- name: E2E tests
run: ./make.sh test
- name: Cleanup deps
run: |
rm -rf build/lib/target/debug
rm -rf build/depends
find build/src -name "*.o" -type f -delete

- name: Functional tests
run: ./make.sh test-py

rust-tests:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,7 @@ if test x$need_bundled_univalue = xyes; then
AC_CONFIG_SUBDIRS([src/univalue])
fi

ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --with-bignum=no --enable-module-recovery --disable-jni"
ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --with-bignum=no --enable-module-recovery --enable-module-schnorrsig --enable-experimental --disable-jni"
AC_CONFIG_SUBDIRS([src/secp256k1])

AC_OUTPUT
Expand Down
6 changes: 3 additions & 3 deletions contrib/linearize/linearize-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,9 @@ def run(self):
if "netmagic" not in settings:
settings["netmagic"] = "f9beb4d9"
if "genesis" not in settings:
settings["genesis"] = (
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
)
settings[
"genesis"
] = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
if "input" not in settings:
settings["input"] = "input"
if "hashlist" not in settings:
Expand Down
16 changes: 16 additions & 0 deletions src/hash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,19 @@ void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char he
num[3] = (nChild >> 0) & 0xFF;
CHMAC_SHA512(chainCode.begin(), chainCode.size()).Write(&header, 1).Write(data, 32).Write(num, 4).Finalize(output);
}

uint256 SHA256Uint256(const uint256& input)
{
uint256 result;
CSHA256().Write(input.begin(), 32).Finalize(result.begin());
return result;
}

CHashWriter TaggedHash(const std::string& tag)
{
CHashWriter writer(SER_GETHASH, 0);
uint256 taghash;
CSHA256().Write((const unsigned char*)tag.data(), tag.size()).Finalize(taghash.begin());
writer << taghash << taghash;
return writer;
}
38 changes: 32 additions & 6 deletions src/hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef DEFI_HASH_H
#define DEFI_HASH_H

#include <attributes.h>
#include <crypto/common.h>
#include <crypto/ripemd160.h>
#include <crypto/sha256.h>
Expand All @@ -15,6 +16,7 @@
#include <uint256.h>
#include <version.h>

#include <string>
#include <vector>

typedef uint256 ChainCode;
Expand Down Expand Up @@ -141,7 +143,7 @@ inline uint160 EthHash160(const std::vector<unsigned char>& vch) {
class CHashWriter
{
private:
CHash256 ctx;
CSHA256 ctx;

const int nType;
const int nVersion;
Expand All @@ -156,20 +158,33 @@ class CHashWriter
ctx.Write((const unsigned char*)pch, size);
}

// invalidates the object
/** Compute the double-SHA256 hash of all data written to this object.
*
* Invalidates this object.
*/
uint256 GetHash() {
uint256 result;
ctx.Finalize((unsigned char*)&result);
ctx.Finalize(result.begin());
ctx.Reset().Write(result.begin(), CSHA256::OUTPUT_SIZE).Finalize(result.begin());
return result;
}

/** Compute the SHA256 hash of all data written to this object.
*
* Invalidates this object.
*/
uint256 GetSHA256() {
uint256 result;
ctx.Finalize(result.begin());
return result;
}

/**
* Returns the first 64 bits from the resulting hash.
*/
inline uint64_t GetCheapHash() {
unsigned char result[CHash256::OUTPUT_SIZE];
ctx.Finalize(result);
return ReadLE64(result);
uint256 result = GetHash();
return ReadLE64(result.begin());
}

template<typename T>
Expand Down Expand Up @@ -224,8 +239,19 @@ uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL
return ss.GetHash();
}

/** Single-SHA256 a 32-byte input (represented as uint256). */
NODISCARD uint256 SHA256Uint256(const uint256& input);

unsigned int MurmurHash3(unsigned int nHashSeed, const uint8_t *vDataToHash, uint32_t vDataSize);

void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char header, const unsigned char data[32], unsigned char output[64]);

/** Return a CHashWriter primed for tagged hashes (as specified in BIP 340).
*
* The returned object will have SHA256(tag) written to it twice (= 64 bytes).
* A tagged hash can be computed by feeding the message into this object, and
* then calling CHashWriter::GetSHA256().
*/
CHashWriter TaggedHash(const std::string& tag);

#endif // DEFI_HASH_H
2 changes: 2 additions & 0 deletions src/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ const CLogCategoryDesc LogCategories[] =
{BCLog::TOKENSPLIT, "tokensplit"},
{BCLog::RPCCACHE, "rpccache"},
{BCLog::CUSTOMTXBENCH, "customtxbench"},
{BCLog::CONNECTBLOCK, "connectblock"},
{BCLog::SIGNTX, "signtx"},
{BCLog::ALL, "1"},
{BCLog::ALL, "all"},
};
Expand Down
4 changes: 2 additions & 2 deletions src/logging.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ namespace BCLog {
TOKENSPLIT = (1ull << 28ull),
RPCCACHE = (1ull << 29ull),
CUSTOMTXBENCH = (1ull << 30ull),
CONNECT = (1ull << 31ull),
SIGN = (1ull << 32ull),
CONNECTBLOCK = (1ull << 31ull),
SIGNTX = (1ull << 32ull),
ALL = ~(0ull),
};

Expand Down
34 changes: 33 additions & 1 deletion src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

#include <consensus/validation.h>
#include <coins.h>

#include <span.h>

CAmount GetDustThreshold(const CTxOut& txout, int32_t txVersion, const CFeeRate& dustRelayFeeIn)
{
Expand Down Expand Up @@ -205,6 +205,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
// get the scriptPubKey corresponding to this input:
CScript prevScript = prev.scriptPubKey;

bool p2sh{};
if (prevScript.IsPayToScriptHash()) {
std::vector <std::vector<unsigned char> > stack;
// If the scriptPubKey is P2SH, we try to extract the redeemScript casually by converting the scriptSig
Expand All @@ -215,6 +216,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
if (stack.empty())
return false;
prevScript = CScript(stack.back().begin(), stack.back().end());
p2sh = true;
}

int witnessversion = 0;
Expand All @@ -236,6 +238,36 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
return false;
}
}

// Check policy limits for Taproot spends:
// - MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit for stack item size
// - No annexes
if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) {
// Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341)
auto stack = MakeSpan(tx.vin[i].scriptWitness.stack);
if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) {
// Annexes are nonstandard as long as no semantics are defined for them.
return false;
}
if (stack.size() >= 2) {
// Script path spend (2 or more stack elements after removing optional annex)
const auto& control_block = SpanPopBack(stack);
SpanPopBack(stack); // Ignore script
if (control_block.empty()) return false; // Empty control block is invalid
if ((control_block[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSCRIPT) {
// Leaf version 0xc0 (aka Tapscript, see BIP 342)
for (const auto& item : stack) {
if (item.size() > MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE) return false;
}
}
} else if (stack.size() == 1) {
// Key path spend (1 stack element after removing optional annex)
// (no policy rules apply)
} else {
// 0 stack elements; this is already invalid by consensus rules
return false;
}
}
}
return true;
}
Expand Down
8 changes: 7 additions & 1 deletion src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ static const bool DEFAULT_PERMIT_BAREMULTISIG = true;
static const unsigned int MAX_STANDARD_P2WSH_STACK_ITEMS = 100;
/** The maximum size of each witness stack item in a standard P2WSH script */
static const unsigned int MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80;
/** The maximum size of each witness stack item in a standard BIP 342 script (Taproot, leaf version 0xc0) */
static const unsigned int MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80;
/** The maximum size of a standard witnessScript */
static const unsigned int MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600;
/** Min feerate for defining dust. Historically this has been based on the
Expand Down Expand Up @@ -68,7 +70,11 @@ static constexpr unsigned int STANDARD_SCRIPT_VERIFY_FLAGS = MANDATORY_SCRIPT_VE
SCRIPT_VERIFY_WITNESS |
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM |
SCRIPT_VERIFY_WITNESS_PUBKEYTYPE |
SCRIPT_VERIFY_CONST_SCRIPTCODE;
SCRIPT_VERIFY_CONST_SCRIPTCODE |
SCRIPT_VERIFY_TAPROOT |
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION |
SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS |
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE;

/** For convenience, standard but not mandatory verify flags. */
static constexpr unsigned int STANDARD_NOT_MANDATORY_VERIFY_FLAGS = STANDARD_SCRIPT_VERIFY_FLAGS & ~MANDATORY_SCRIPT_VERIFY_FLAGS;
Expand Down
22 changes: 22 additions & 0 deletions src/pubkey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <secp256k1.h>
#include <secp256k1_recovery.h>
#include <secp256k1_schnorrsig.h>

namespace
{
Expand Down Expand Up @@ -166,6 +167,27 @@ static int ecdsa_signature_parse_der_lax(const secp256k1_context* ctx, secp256k1
return 1;
}

XOnlyPubKey::XOnlyPubKey(Span<const unsigned char> bytes)
{
assert(bytes.size() == 32);
std::copy(bytes.begin(), bytes.end(), m_keydata.begin());
}

bool XOnlyPubKey::VerifySchnorr(const uint256& msg, Span<const unsigned char> sigbytes) const
{
assert(sigbytes.size() == 64);
secp256k1_xonly_pubkey pubkey;
if (!secp256k1_xonly_pubkey_parse(secp256k1_context_verify, &pubkey, m_keydata.data())) return false;
return secp256k1_schnorrsig_verify(secp256k1_context_verify, sigbytes.data(), msg.begin(), 32, &pubkey);
}

bool XOnlyPubKey::CheckPayToContract(const XOnlyPubKey& base, const uint256& hash, bool parity) const
{
secp256k1_xonly_pubkey base_point;
if (!secp256k1_xonly_pubkey_parse(secp256k1_context_verify, &base_point, base.data())) return false;
return secp256k1_xonly_pubkey_tweak_add_check(secp256k1_context_verify, m_keydata.begin(), parity, &base_point, hash.begin());
}

bool CPubKey::Verify(const uint256 &hash, const std::vector<unsigned char>& vchSig) const {
if (!IsValid())
return false;
Expand Down
24 changes: 23 additions & 1 deletion src/pubkey.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <hash.h>
#include <serialize.h>
#include <span.h>
#include <script/standard.h>
#include <uint256.h>

Expand Down Expand Up @@ -208,7 +209,7 @@ class CPubKey
/*
* Check syntactic correctness.
*
* Note that this is consensus critical as CheckSig() calls it!
* Note that this is consensus critical as CheckECDSASignature() calls it!
*/
bool IsValid() const
{
Expand Down Expand Up @@ -261,6 +262,27 @@ inline std::pair<CPubKey, CPubKey> GetBothPubkeyCompressions(const CPubKey &key)
return {key, keyCopy};
}

class XOnlyPubKey
{
private:
uint256 m_keydata;

public:
/** Construct an x-only pubkey from exactly 32 bytes. */
XOnlyPubKey(Span<const unsigned char> bytes);

/** Verify a Schnorr signature against this public key.
*
* sigbytes must be exactly 64 bytes.
*/
bool VerifySchnorr(const uint256& msg, Span<const unsigned char> sigbytes) const;
bool CheckPayToContract(const XOnlyPubKey& base, const uint256& hash, bool parity) const;

const unsigned char& operator[](int pos) const { return *(m_keydata.begin() + pos); }
const unsigned char* data() const { return m_keydata.begin(); }
size_t size() const { return m_keydata.size(); }
};

struct CExtPubKey {
unsigned char nDepth;
unsigned char vchFingerprint[4];
Expand Down
4 changes: 2 additions & 2 deletions src/rpc/rawtransaction_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ UniValue ExtendedTxToUniv(const CTransaction& tx, bool include_hex, int serializ

UniValue SignTransaction(CMutableTransaction& mtx, const UniValue& prevTxsUnival, FillableSigningProvider* keystore, std::map<COutPoint, Coin>& coins, bool is_temp_keystore, const UniValue& hashType)
{
if (LogAcceptCategory(BCLog::SIGN)) {
if (LogAcceptCategory(BCLog::SIGNTX)) {
LogPrintf("SignTransaction::Pre: %s\n", ExtendedTxToUniv(CTransaction(mtx), true, RPCSerializationFlags(), 4, true, true).write(2));
}
// Add previous txouts given in the RPC call:
Expand Down Expand Up @@ -435,7 +435,7 @@ UniValue SignTransaction(CMutableTransaction& mtx, const UniValue& prevTxsUnival
}
bool fComplete = vErrors.empty();

if (LogAcceptCategory(BCLog::SIGN)) {
if (LogAcceptCategory(BCLog::SIGNTX)) {
LogPrintf("SignTransaction::Post: %s\n", ExtendedTxToUniv(CTransaction(mtx), true, RPCSerializationFlags(), 4, true, true).write(2));
}

Expand Down
6 changes: 3 additions & 3 deletions src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ std::string DescriptorChecksum(const Span<const char>& span)
return ret;
}

std::string AddChecksum(const std::string& str) { return str + "#" + DescriptorChecksum(MakeSpan(str)); }
std::string AddChecksum(const std::string& str) { return str + "#" + DescriptorChecksum(str); }

////////////////////////////////////////////////////////////////////////////
// Internal representation //
Expand Down Expand Up @@ -1036,7 +1036,7 @@ bool CheckChecksum(Span<const char>& sp, bool require_checksum, std::string& err

std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum)
{
Span<const char> sp(descriptor.data(), descriptor.size());
Span<const char> sp(descriptor);
if (!CheckChecksum(sp, require_checksum, error)) return nullptr;
auto ret = ParseScript(sp, ParseScriptContext::TOP, out, error);
if (sp.size() == 0 && ret) return std::unique_ptr<Descriptor>(std::move(ret));
Expand All @@ -1047,7 +1047,7 @@ std::string GetDescriptorChecksum(const std::string& descriptor)
{
std::string ret;
std::string error;
Span<const char> sp(descriptor.data(), descriptor.size());
Span<const char> sp(descriptor);
if (!CheckChecksum(sp, false, error, &ret)) return "";
return ret;
}
Expand Down
Loading
Loading