Skip to content
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
125 changes: 125 additions & 0 deletions doc/block-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Witness v2 Taproot with block references

This document specifies the consensus rules for witness version 2 outputs
with a 32-byte program, as implemented in this prototype. Activation
parameters are out of scope; on regtest the rules are always active.

## Motivation

A transaction that is valid on both sides of a chain split can be replayed.
Holders who want to spend on one side only currently need a coin whose
history already differs between the two chains, such as a coinbase
descendant. A block reference lets any spender opt in to validity on one
chain, at the cost of a few witness bytes, with no coordination with anyone
else.

## Rules

A witness v2 output with a 32-byte program is spent under the
[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) and
[BIP342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)
rules, with one addition.

### Block reference annex

If the spend carries an annex and the first byte after the `0x50` annex
marker is `0x01`, the annex is a *block reference*:

0x50 || 0x01 || height (4 bytes, little endian) || further bytes (ignored)

An annex shorter than 6 bytes whose second byte is `0x01` makes the spend
invalid. Bytes after the height have no consensus meaning, exactly like the
rest of the annex today. Annexes whose second byte is not `0x01` keep their
BIP341 treatment: no meaning, always valid.

The type byte `0x01` means "this input references the block at `height`".
This document attaches two effects to the reference; future rules may attach
more.

### Maturity and activation

A block containing the transaction at height `H` is invalid unless
`height + 100 <= H` for every block reference in the transaction. The
constant is `COINBASE_MATURITY`. As a consequence the referenced block is at
least 100 deep, and a transaction with a block reference is exactly as safe
against reorganisation as a spend of a matured coinbase output.

A reference to a block below the activation height of these rules is
invalid. Before activation, witness v2 spends are anyone-can-spend and an
annex has no meaning, exactly as for any other undefined witness version;
neither rule in this document applies to blocks before activation. (The
mempool applies the witness v2 rules as policy regardless.)

### Signature message

Every signature checked for an input with a block reference, on the key
path or in tapscript, is over the BIP341 message with `ext_flag` bit 1 set
(so `spend_type` gains the value 4) and the 32-byte hash of the block at
`height` in the active chain appended after the message, after the BIP342
extension if present.

Since the hash of the referenced block is part of the message and not of
the transaction, a signature made for one chain does not verify on any chain
whose block at that height differs, nor on a chain that computes the plain
BIP341 message. An input without a block reference is signed exactly as a
witness v1 input and is valid on every chain.

## Non-consensus considerations

- The annex is committed to by every signature on the input, so a third
party cannot add, remove, or alter a block reference on a signed input.
A script-path spend that checks no signature can carry a reference, but
it then only imposes the maturity rule.
- A transaction with a block reference never expires. It becomes invalid
only if a reorganisation removes the referenced block, which requires a
reorganisation of at least 100 blocks.
- Wallets that want a spend to be specific to the current chain should
reference the most recent eligible block, that is the block at
`tip - 99`.
- Policy: a block reference annex of exactly 6 bytes is standard for v2
spends. All other annexes remain non-standard. A transaction whose
reference is not yet mature is rejected from the mempool with a
retryable, non-punishable error, like a premature coinbase spend.

## Wallet

- `tr2(KEY, TREE)` is `tr()` with a witness v2 output. Addresses are
bech32m with witness version 2 (`bc1z...`).
- `send`, `sendall` and `walletcreatefundedpsbt` accept a
`block_reference` option. When set, every witness v2 input references
the block at `tip - 99`.
- A PSBT input carries the reference in field type `0x7f` (prototype,
not assigned by any BIP) as a 4-byte little endian height followed by
the 32-byte block hash. Signers use it to build the annex and the
message; it is kept after finalization so the final witness can be
verified. `decodepsbt` shows it as `block_reference`.
- For a nonzero height-based locktime between the reference height and the
wallet's tip, the wallet also includes headers from the referenced block
through the locktime height, inclusive. No headers are included for zero
or timestamp-based locktimes, or when the locktime height is before the
referenced block or beyond the tip. The chosen locktime is not changed.
Headers are shared by all inputs in global PSBT records of type `0x7f`
(prototype, not assigned by any BIP).
Each key is the type byte followed by a 4-byte little endian height;
its value is the serialized 80-byte header. `decodepsbt` exposes these
as `block_headers`, an object mapping heights to hex-encoded headers.
Combining and joining PSBTs reject conflicting headers at the same height.
A chain-aware combiner can resolve such conflicts before combining.
Combining, joining and finalizing PSBTs preserve these records. They are
not part of the final transaction.
- These optional headers let an offline signer inspect the chain extending
a referenced block and its proof of work. A verifier can derive each block
hash from its header and check continuity using the next header's previous
block hash. Their presence does not establish the claimed height, the
intended side of a fork, or that this is the most-work chain. Signers must
apply their own verification and trust policy;
this prototype continues to sign PSBTs without headers. An external signer
can use the transaction's height locktime as the endpoint without knowing
the wallet's tip. Reference maturity is still checked against the containing
block's height, not the locktime; this metadata does not change consensus.
- `signrawtransactionwithwallet` and `signrawtransactionwithkey` sign v2
inputs without a reference.
- When a reorg replaces a block that an unconfirmed wallet transaction
references, the wallet abandons that transaction: its signatures can never
verify again, and its inputs become spendable. A reorg that puts the same
block back leaves the transaction alone.
4 changes: 4 additions & 0 deletions src/addresstype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
addressRet = PayToAnchor();
return true;
}
case TxoutType::WITNESS_V2_TAPROOT: {
addressRet = WitnessUnknown{2, vSolutions[0]};
return true;
}
case TxoutType::WITNESS_UNKNOWN: {
addressRet = WitnessUnknown{vSolutions[0][0], vSolutions[1]};
return true;
Expand Down
13 changes: 12 additions & 1 deletion src/bench/verify_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@

#include <cstddef>
#include <map>
#include <optional>
#include <span>
#include <utility>
#include <vector>

enum class ScriptType {
P2WPKH, // segwitv0, witness-pubkey-hash (ECDSA signature)
P2TR_KeyPath, // segwitv1, taproot key-path spend (Schnorr signature)
P2TR_ScriptPath, // segwitv1, taproot script-path spend (Tapscript leaf with a single OP_CHECKSIG)
P2TR2_KeyPath_BlockRef, // segwitv2, taproot key-path spend with a block reference (annex, message extension)
};

static size_t ExpectedWitnessStackSize(ScriptType script_type)
Expand All @@ -38,6 +41,7 @@ static size_t ExpectedWitnessStackSize(ScriptType script_type)
case ScriptType::P2WPKH: return 2; // [pubkey, signature]
case ScriptType::P2TR_KeyPath: return 1; // [signature]
case ScriptType::P2TR_ScriptPath: return 3; // [signature, tapscript, control block]
case ScriptType::P2TR2_KeyPath_BlockRef: return 2; // [signature, annex]
} // no default case, so the compiler can warn about missing cases
assert(false);
}
Expand All @@ -63,6 +67,7 @@ static void VerifyScriptBench(benchmark::Bench& bench, ScriptType script_type)
switch (script_type) {
case ScriptType::P2WPKH: return WitnessV0KeyHash(pubkey);
case ScriptType::P2TR_KeyPath: return WitnessV1Taproot(xonly_pubkey);
case ScriptType::P2TR2_KeyPath_BlockRef: return WitnessUnknown{2, ToByteVector(xonly_pubkey)};
case ScriptType::P2TR_ScriptPath:
TaprootBuilder builder;
builder.Add(0, CScript() << ToByteVector(xonly_pubkey) << OP_CHECKSIG, TAPROOT_LEAF_TAPSCRIPT);
Expand All @@ -83,11 +88,15 @@ static void VerifyScriptBench(benchmark::Bench& bench, ScriptType script_type)
{txSpend.vin[0].prevout, Coin(txCredit.vout[0], /*nHeightIn=*/100, /*fCoinBaseIn=*/false)}
};
std::map<int, bilingual_str> input_errors;
bool complete = SignTransaction(txSpend, &keystore, coins, {.sighash_type = SIGHASH_ALL}, input_errors);
const std::pair<int, uint256> block_ref{100, uint256::ONE};
SignOptions options{.sighash_type = SIGHASH_ALL};
if (script_type == ScriptType::P2TR2_KeyPath_BlockRef) options.block_reference = block_ref;
bool complete = SignTransaction(txSpend, &keystore, coins, options, input_errors);
assert(complete);
// Weak sanity check on witness data to ensure we produced the intended spending type
assert(txSpend.vin[0].scriptWitness.stack.size() == ExpectedWitnessStackSize(script_type));
txdata.Init(txSpend, /*spent_outputs=*/{txCredit.vout[0]});
if (options.block_reference) txdata.m_block_hashes.push_back(block_ref);
}

// Benchmark.
Expand All @@ -108,6 +117,7 @@ static void VerifyScriptBench(benchmark::Bench& bench, ScriptType script_type)
static void VerifyScriptP2WPKH(benchmark::Bench& bench) { VerifyScriptBench(bench, ScriptType::P2WPKH); }
static void VerifyScriptP2TR_KeyPath(benchmark::Bench& bench) { VerifyScriptBench(bench, ScriptType::P2TR_KeyPath); }
static void VerifyScriptP2TR_ScriptPath(benchmark::Bench& bench) { VerifyScriptBench(bench, ScriptType::P2TR_ScriptPath); }
static void VerifyScriptP2TR2_KeyPath_BlockRef(benchmark::Bench& bench) { VerifyScriptBench(bench, ScriptType::P2TR2_KeyPath_BlockRef); }

static void VerifyNestedIfScript(benchmark::Bench& bench)
{
Expand All @@ -134,4 +144,5 @@ static void VerifyNestedIfScript(benchmark::Bench& bench)
BENCHMARK(VerifyScriptP2WPKH);
BENCHMARK(VerifyScriptP2TR_KeyPath);
BENCHMARK(VerifyScriptP2TR_ScriptPath);
BENCHMARK(VerifyScriptP2TR2_KeyPath_BlockRef);
BENCHMARK(VerifyNestedIfScript);
2 changes: 1 addition & 1 deletion src/chainparamsbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
argsman.AddArg("-chain=<chain>", "Use the chain <chain> (default: main). Allowed values: " LIST_CHAIN_NAMES, ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-regtest", "Enter regression test mode, which uses a special chain in which blocks can be solved instantly. "
"This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv). (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv, taproot_v2). (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-testnet", "Use the testnet3 chain. Equivalent to -chain=test. Support for testnet3 is deprecated and will be removed in an upcoming release. Consider moving to testnet4 now by using -testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
Expand Down
7 changes: 6 additions & 1 deletion src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ enum BuriedDeployment : int16_t {
// SCRIPT_VERIFY_WITNESS is enforced from genesis, but the check for downloading
// missing witness data is not. BIP 147 also relies on hardcoded activation height.
DEPLOYMENT_SEGWIT,
DEPLOYMENT_TAPROOT_V2,
};
constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_SEGWIT; }
constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_TAPROOT_V2; }

enum DeploymentPos : uint16_t {
DEPLOYMENT_TESTDUMMY,
Expand Down Expand Up @@ -108,6 +109,8 @@ struct Params {
* Note that segwit v0 script rules are enforced on all blocks except the
* BIP 16 exception blocks. */
int SegwitHeight;
/** Block height at which witness v2 Taproot (with block references) becomes active */
int TaprootV2Height;
/** Don't warn about unknown BIP 9 activations below this height.
* This prevents us from warning about the CSV, segwit and taproot activations. */
int MinBIP9WarningHeight;
Expand Down Expand Up @@ -153,6 +156,8 @@ struct Params {
return CSVHeight;
case DEPLOYMENT_SEGWIT:
return SegwitHeight;
case DEPLOYMENT_TAPROOT_V2:
return TaprootV2Height;
} // no default case, so the compiler can warn about missing cases
return std::numeric_limits<int>::max();
}
Expand Down
20 changes: 20 additions & 0 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#include <algorithm>
#include <cstddef>
#include <optional>
#include <span>
#include <string>

bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime)
Expand Down Expand Up @@ -167,6 +169,24 @@ int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& i
return nSigOps;
}

std::vector<int> Consensus::GetBlockReferences(const CTransaction& tx, const CCoinsViewCache& inputs)
{
std::vector<int> heights;
for (const CTxIn& txin : tx.vin) {
const auto& stack{txin.scriptWitness.stack};
if (stack.size() < 2 || stack.back().size() < BLOCK_REF_ANNEX_SIZE || stack.back()[0] != ANNEX_TAG) continue;
int witnessversion;
std::vector<unsigned char> witnessprogram;
const CScript& spk{inputs.AccessCoin(txin.prevout).out.scriptPubKey};
if (!spk.IsWitnessProgram(witnessversion, witnessprogram) || witnessversion != 2 || witnessprogram.size() != WITNESS_V2_TAPROOT_SIZE) continue;
std::optional<int> height;
if (ParseBlockReference(stack.back(), height) && height) heights.push_back(*height);
}
std::ranges::sort(heights);
heights.erase(std::ranges::unique(heights).begin(), heights.end());
return heights;
}

bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee)
{
// are the actual inputs available?
Expand Down
8 changes: 8 additions & 0 deletions src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ namespace Consensus {
* Preconditions: tx.IsCoinBase() is false.
*/
[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee);

/**
* Heights of the blocks referenced by the inputs of a transaction (block reference annexes of
* witness v2 spends, see doc/block-reference.md), sorted and without duplicates. Malformed
* references are skipped; script validation rejects them.
* Preconditions: all inputs are available in `inputs`.
*/
std::vector<int> GetBlockReferences(const CTransaction& tx, const CCoinsViewCache& inputs);
} // namespace Consensus

/** Auxiliary functions for transaction validation (ideally should not be exposed) */
Expand Down
4 changes: 4 additions & 0 deletions src/deploymentinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ std::string DeploymentName(Consensus::BuriedDeployment dep)
return "csv";
case Consensus::DEPLOYMENT_SEGWIT:
return "segwit";
case Consensus::DEPLOYMENT_TAPROOT_V2:
return "taproot_v2";
} // no default case, so the compiler can warn about missing cases
return "";
}
Expand All @@ -45,6 +47,8 @@ std::optional<Consensus::BuriedDeployment> GetBuriedDeployment(const std::string
return Consensus::BuriedDeployment::DEPLOYMENT_CLTV;
} else if (name == "csv") {
return Consensus::BuriedDeployment::DEPLOYMENT_CSV;
} else if (name == "taproot_v2") {
return Consensus::BuriedDeployment::DEPLOYMENT_TAPROOT_V2;
}
return std::nullopt;
}
4 changes: 4 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <vector>

class CBlock;
class CBlockHeader;
class CFeeRate;
class CRPCCommand;
class CScheduler;
Expand Down Expand Up @@ -52,6 +53,8 @@ class FoundBlock
{
public:
FoundBlock& hash(uint256& hash) { m_hash = &hash; return *this; }
//! Return the header from the block index, including for pruned blocks.
FoundBlock& header(CBlockHeader& header) { m_header = &header; return *this; }
FoundBlock& height(int& height) { m_height = &height; return *this; }
FoundBlock& time(int64_t& time) { m_time = &time; return *this; }
FoundBlock& maxTime(int64_t& max_time) { m_max_time = &max_time; return *this; }
Expand All @@ -67,6 +70,7 @@ class FoundBlock
FoundBlock& data(CBlock& data) { m_data = &data; return *this; }

uint256* m_hash = nullptr;
CBlockHeader* m_header = nullptr;
int* m_height = nullptr;
int64_t* m_time = nullptr;
int64_t* m_max_time = nullptr;
Expand Down
Loading
Loading