mirror of
https://github.com/bitcoin/bips.git
synced 2026-02-09 15:23:09 +00:00
Add BIP-346: OP_TXHASH
This commit is contained in:
parent
ecb074d487
commit
6a0636da32
@ -1171,6 +1171,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority
|
||||
| Specification
|
||||
| Closed
|
||||
|-
|
||||
| [[bip-0346.md|346]]
|
||||
| Consensus (soft fork)
|
||||
| OP_TXHASH
|
||||
| Steven Roose, Brandon Black
|
||||
| Specification
|
||||
| Draft
|
||||
|-
|
||||
| [[bip-0347.mediawiki|347]]
|
||||
| Consensus (soft fork)
|
||||
| OP_CAT in Tapscript
|
||||
|
||||
441
bip-0346.md
Normal file
441
bip-0346.md
Normal file
@ -0,0 +1,441 @@
|
||||
```
|
||||
BIP: 346
|
||||
Layer: Consensus (soft fork)
|
||||
Title: OP_TXHASH
|
||||
Authors: Steven Roose <steven@roose.io>
|
||||
Brandon Black <freedom@reardencode.com>
|
||||
Status: Draft
|
||||
Type: Specification
|
||||
Assigned: 2024-04-24
|
||||
License: BSD-3-Clause
|
||||
```
|
||||
|
||||
# Abstract
|
||||
|
||||
This BIP proposes a new opcode `OP_TXHASH`, to be activated as a change to the
|
||||
semantics of `OP_SUCCESS189` in tapscript contexts.
|
||||
|
||||
This opcode provides a generalized method for introspecting certain details of
|
||||
the spending transaction, which enables non-interactive enforcement of certain
|
||||
properties of the transaction spending a certain UTXO.
|
||||
|
||||
Together with an opcode like `OP_CHECKSIGFROMSTACK`, this opcode effectively
|
||||
provides a fully generalized signature hash construction, fully supporting
|
||||
all existing SIGHASH flags, the proposed sighash flags from
|
||||
[BIP-118](bip-0118.mediawiki) (`SIGHASH_ANYPREVOUT`) and many other new signature hash
|
||||
combinations.
|
||||
|
||||
The constructions specified in this BIP also open up the way for other
|
||||
potential updates; see Motivation section for more details.
|
||||
|
||||
|
||||
# Specification
|
||||
|
||||
|
||||
## OP_TXHASH
|
||||
|
||||
`OP_TXHASH` redefines the `OP_SUCCESS189` tapscript opcode (`0xbd`) as a soft
|
||||
fork upgrade. This opcode is only active in tapscript contexts.
|
||||
|
||||
Note that `OP_SUCCESS187` is used by [BIP-443](bip-0443.mediawiki) (`OP_CHECKCONTRACTVERIFY`) and
|
||||
`OP_SUCCESS188` was used by [BIP-345](bip-0345.mediawiki) (`OP_VAULT`) at the time of first draft
|
||||
of this BIP, making `OP_SUCCESS189` the next available opcode for this purpose.
|
||||
|
||||
It has the following semantics:
|
||||
|
||||
* There is at least one element on the stack, fail otherwise.
|
||||
* The element is interpreted as the TxFieldSelector and is popped off the stack.
|
||||
* The TxFieldSelector must be valid, fail otherwise.
|
||||
* The 32-byte TxHash of the transaction at the current input index, calculated
|
||||
using the given TxFieldSelector is pushed onto the stack.
|
||||
|
||||
## TxFieldSelector
|
||||
|
||||
The TxFieldSelector has the following encoding. We will give a brief conceptual
|
||||
summary, followed by a reference implementation of the CalculateTxHash function.
|
||||
|
||||
In the following specifications, the `|` operator is used for the bitwise OR
|
||||
operation.
|
||||
|
||||
* There are two special cases for the TxFieldSelector:
|
||||
* the empty value, zero bytes long: it is set equal to `TXFS_SPECIAL_TEMPLATE`,
|
||||
the de-facto default value which means everything except the prevouts and
|
||||
the prevout scriptPubkeys and amounts.
|
||||
|
||||
Special case `TXFS_SPECIAL_TEMPLATE` is 4 bytes long, as follows:
|
||||
* 1: `TXFS_VERSION | TXFS_LOCKTIME | TXFS_CURRENT_INPUT_IDX`
|
||||
* 2: `TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_OUTPUTS_ALL`
|
||||
* 3: `TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL`
|
||||
* 4: `TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL`
|
||||
|
||||
* If the TxFieldSelector has exactly 1 byte, we use a _short notation_.
|
||||
It has its 8 bits assigned as follows, from lowest to highest:
|
||||
* 2/1: Inputs
|
||||
* 00: `TXFS_INOUT_SELECTION_NONE`
|
||||
* 01: `TXFS_INOUT_SELECTION_CURRENT`
|
||||
* 11: `TXFS_INOUT_SELECTION_ALL`
|
||||
* 4/3: Outputs
|
||||
* 00: `TXFS_INOUT_SELECTION_NONE`
|
||||
* 01: `TXFS_INOUT_SELECTION_CURRENT`
|
||||
* 11: `TXFS_INOUT_SELECTION_ALL`
|
||||
* 5: `TXFS_INPUTS_PREVOUTS`
|
||||
* 6: `TXFS_INPUTS_PREV_SCRIPTPUBKEYS | TXFS_INPUTS_PREV_VALUES`
|
||||
* 7: `TXFS_CURRENT_INPUT_CONTROL_BLOCK | TXFS_CURRENT_INPUT_SPENTSCRIPT | TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS`
|
||||
* 8: `TXFS_CURRENT_INPUT_IDX`
|
||||
|
||||
Additionally, it includes `TXFS_VERSION | TXFS_LOCKTIME | TXFS_CONTROL | TXFS_CURRENT_INPUT_TAPROOT_ANNEX`
|
||||
as global fields and `TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_OUTPUTS_ALL`
|
||||
as input and output fields.
|
||||
|
||||
These 1-byte selections allow the TxFieldSelector to emulate current
|
||||
signature hashing modes and those defined in [BIP-118](bip-0118.mediawiki):
|
||||
|
||||
| BIP-341/118 sighash type | 1-byte TxFieldSelector |
|
||||
| :--------------------------- | :--------------------- |
|
||||
| `ALL` | `0b11111111` |
|
||||
| `SINGLE` | `0b11110111` |
|
||||
| `NONE` | `0b11110011` |
|
||||
| `ALL|ANYONECANPAY` | `0b11111101` |
|
||||
| `SINGLE|ANYONECANPAY` | `0b11110101` |
|
||||
| `NONE|ANYONECANPAY` | `0b11110001` |
|
||||
| `ALL|ANYPREVOUT` | `0b11101101` |
|
||||
| `SINGLE|ANYPREVOUT` | `0b11100101` |
|
||||
| `NONE|ANYPREVOUT` | `0b11100001` |
|
||||
| `ALL|ANYPREVOUTANYSCRIPT` | `0b11001101` |
|
||||
| `SINGLE|ANYPREVOUTANYSCRIPT` | `0b11000101` |
|
||||
| `NONE|ANYPREVOUTANYSCRIPT` | `0b11000001` |
|
||||
|
||||
|
||||
* If the TxFieldSelector is longer than one byte, the first byte of the TxFieldSelector
|
||||
has its 8 bits assigned as follows, from lowest to highest:
|
||||
* 1: version (`TXFS_VERSION`)
|
||||
* 2: locktime (`TXFS_LOCKTIME`)
|
||||
* 3: current input index (`TXFS_CURRENT_INPUT_IDX`)
|
||||
* 4: current input control block (`TXFS_CURRENT_INPUT_CONTROL_BLOCK`)
|
||||
* 5: current input spent script (`TXFS_CURRENT_INPUT_SPENTSCRIPT`)
|
||||
* 6: current script last `OP_CODESEPARATOR` position (or 0xffffffff)
|
||||
(`TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS`)
|
||||
* 7: current input annex including prefix byte (or empty) (`TXFS_CURRENT_INPUT_TAPROOT_ANNEX`)
|
||||
* 8: `TXFS_CONTROL` (i.e. include TxFieldSelector into hash)
|
||||
|
||||
* The highest bit of the first byte (`TXFS_CONTROL`), we will call the
|
||||
"control bit", and it can be used to control the behavior of the opcode. For
|
||||
`OP_TXHASH`, the control bit is used to determine
|
||||
whether the TxFieldSelector itself has to be included in the resulting hash.
|
||||
(For potential other uses of the TxFieldSelector (like a hypothetical
|
||||
`OP_TX`), this bit can be repurposed.)
|
||||
|
||||
* The second byte will be used to indicate fields from the inputs and outputs.
|
||||
The 8 bits are assigned the following variables, from lowest to highest:
|
||||
* Specifying which fields of the inputs will be selected:
|
||||
* 1: prevouts (`TXFS_INPUTS_PREVOUTS`)
|
||||
* 2: sequences (`TXFS_INPUTS_SEQUENCES`)
|
||||
* 3: scriptSigs (`TXFS_INPUTS_SCRIPTSIGS`)
|
||||
* 4: prevout scriptPubkeys (`TXFS_INPUTS_PREV_SCRIPTPUBKEYS`)
|
||||
* 5: prevout values (`TXFS_INPUTS_PREV_VALUES`)
|
||||
* 6: taproot annexes (`TXFS_INPUTS_TAPROOT_ANNEXES`)
|
||||
|
||||
* Specifying which fields of the outputs will be selected:
|
||||
* 7: scriptPubkeys (`TXFS_OUTPUTS_SCRIPTPUBKEYS`)
|
||||
* 8: values (`TXFS_OUTPUTS_VALUES`)
|
||||
|
||||
* We define as follows:
|
||||
* `TXFS_ALL = TXFS_VERSION | TXFS_LOCKTIME | TXFS_CURRENT_INPUT_IDX | TXFS_CURRENT_INPUT_CONTROL_BLOCK | TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS | TXFS_CONTROL`
|
||||
* `TXFS_INPUTS_ALL = TXFS_INPUTS_PREVOUTS | TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_INPUTS_PREV_SCRIPTPUBKEYS | TXFS_INPUTS_PREV_VALUES | TXFS_INPUTS_TAPROOT_ANNEXES`
|
||||
* `TXFS_OUTPUTS_ALL = TXFS_OUTPUTS_SCRIPTPUBKEYS | TXFS_OUTPUTS_VALUES`
|
||||
|
||||
|
||||
* For both inputs and then outputs, expect an additional byte as follows:
|
||||
* The highest bit (`TXFS_INOUT_NUMBER`) indicates whether the "number of
|
||||
in-/outputs" should be committed to.
|
||||
* For the remaining bits, there are three exceptional values:
|
||||
* 0x00 (`TXFS_INOUT_SELECTION_NONE`) means "no in/outputs" (hence only the
|
||||
number of them as `0x80` (`TXFS_INOUT_NUMBER`)).
|
||||
* `0x40` (`TXFS_INOUT_SELECTION_CURRENT`) means "select only the in/output
|
||||
of the current input index" (it is invalid when current index exceeds
|
||||
number of outputs).
|
||||
* `0x3f` (`TXFS_INOUT_SELECTION_ALL`) means "select all in/outputs".
|
||||
|
||||
* The second highest bit (`TXFS_INOUT_SELECTION_MODE`) is the "specification mode":
|
||||
* Set to 0 it means "leading mode".
|
||||
* Set to 1 it means "individual mode".
|
||||
|
||||
* In "leading mode", the third highest bit (`TXFS_INOUT_LEADING_SIZE`) is
|
||||
used to indicate the "index size", i.e. the number of bytes will be used to
|
||||
represent the number of in/output.
|
||||
* With "index size" set to 0, the remaining lowest 5 bits of the first byte
|
||||
will be interpreted as the number of leading in/outputs to select.
|
||||
* With "index size" set to 1, the remaining lowest 5 bits of the first byte
|
||||
together with the 8 bits of the next byte will be interpreted as the
|
||||
number of leading in/outputs to select.
|
||||
|
||||
* In "individual mode", the third highest bit (`TXFS_INOUT_INDIVIDUAL_MODE`)
|
||||
indicates whether we are passing absolute indices (0) or indices relative
|
||||
to the current input (1), the remaining lowest 5 bits will be interpreted
|
||||
as `n`, the number of individual in/outputs follow.
|
||||
* In absolute mode (second highest bit is 0), for each of the `n` indices,
|
||||
at least one extra byte is expected.
|
||||
* If that byte's highest bit is set to 0, the remaining 7 bits represent
|
||||
the absolute index to select.
|
||||
* If that byte's highest bit is set to 1, the remaining 7 bits, together
|
||||
with the next byte's 8 bits represent the absolute index to select.
|
||||
* In relative mode (second highest bit is 1), for each of the `n` indices,
|
||||
at least one extra byte is expected.
|
||||
* If that byte's highest bit is set to 0, the remaining 7 bits represent
|
||||
the relative index in two's complement.
|
||||
* If that byte's highest bit is set to 1, the remaining 7 bits, together
|
||||
with the next byte's 8 bits represent the relative index in two's
|
||||
complement.
|
||||
|
||||
|
||||
Effectively, this allows a user to select
|
||||
* all in/outputs
|
||||
* the current input index
|
||||
* the leading in/outputs up to 8,191
|
||||
* up to 32 individually selected in/outputs
|
||||
** using absolute indices up to 32,767
|
||||
** using indices relative to the current input index from -16382 to +16383.
|
||||
|
||||
|
||||
### TxFieldSelector malleability
|
||||
|
||||
It is possible to represent the same selected data using multiple different
|
||||
TxFieldSelectors. For this reason, users are advised to always set the
|
||||
`TXFS_CONTROL` field flag that commits to the TxFieldSelector that was used
|
||||
to get the hash.
|
||||
|
||||
|
||||
### Visualization
|
||||
|
||||
* first byte
|
||||
|
||||
```
|
||||
1 1 1 1 1 1 1 1
|
||||
| | | | | | | ^ version
|
||||
| | | | | | ^ locktime
|
||||
| | | | | ^ current input index
|
||||
| | | | ^ current input control block
|
||||
| | | ^ current input spent script
|
||||
| | ^ current script last OP_CODESEPARATOR
|
||||
| ^ current input taproot annex
|
||||
^ control bit (ie. include TXFS in hash)
|
||||
```
|
||||
|
||||
* second byte
|
||||
|
||||
```
|
||||
<-> outputs
|
||||
| | <---------> inputs
|
||||
1 1 1 1 1 1 1 1
|
||||
| | | | | | | ^ prevouts
|
||||
| | | | | | ^ sequences
|
||||
| | | | | ^ scriptSigs
|
||||
| | | | ^ prevout scriptPubkeys
|
||||
| | | ^ prevout values
|
||||
| | ^ taproot annexes
|
||||
| ^ scriptPubkeys
|
||||
^ values
|
||||
```
|
||||
|
||||
* in/output selector byte
|
||||
|
||||
"leading 3 in/outputs"
|
||||
```
|
||||
1 0 0 0 0 0 1 1
|
||||
| | | <-------> integer 0b00011 == 3
|
||||
| | ^ index size: single byte
|
||||
| ^ leading mode
|
||||
^ commit the number of in/outputs
|
||||
```
|
||||
|
||||
"leading 257 in/outputs"
|
||||
```
|
||||
1 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1
|
||||
| | | <------------------------> integer 0b00001 00000001 == 257
|
||||
| | ^ index size 1: two bytes
|
||||
| ^ leading mode
|
||||
^ commit the number of in/outputs
|
||||
```
|
||||
|
||||
"indices 1 and 3"
|
||||
```
|
||||
0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1
|
||||
| | | | <--------------> second idx: 3
|
||||
| | | | <-------------> first idx: 1
|
||||
| | | | <-----> selection count: 0b0010 == 2 indices
|
||||
| | | ^ index size: single byte per index
|
||||
| | ^ absolute index
|
||||
| ^ individual mode
|
||||
^ don't commit the number of in/outputs
|
||||
```
|
||||
|
||||
* total example
|
||||
|
||||
```
|
||||
ff ff c2 01 03 83
|
||||
| | ^ commit number of outputs + leading 3 outputs
|
||||
| | <------> commit number of inputs + inputs at indices 1 and 3
|
||||
| ^ all input and output fields
|
||||
^ all regular fields
|
||||
```
|
||||
|
||||
|
||||
## Resource limits
|
||||
|
||||
Using the same validation budget ("sigops budget") introduced in BIP-0342,
|
||||
each TransactionHash decreases the validation budget by 25. If this brings the
|
||||
budget below zero, the script fails immediately.<br>The following
|
||||
considerations should be made:
|
||||
|
||||
* All fields that can be of arbitrary size are cacheable as TransactionHash
|
||||
always hashes their hashed values.
|
||||
* In "individual mode", a user can at most commit 32 inputs or outputs,
|
||||
which we don't consider excessive for potential repeated use.
|
||||
* In "leading mode", a caching strategy can be used where the SHA256 context
|
||||
is stored every N in/outputs so that multiple executions of the
|
||||
TransactionHash function can use the caches and only have to hash an
|
||||
additional N-1 items at most.
|
||||
|
||||
|
||||
# Motivation
|
||||
|
||||
This BIP specifies a basic transaction introspection primitive that is useful
|
||||
to either reduce interactivity in multi-user protocols or to enforce some basic
|
||||
constraints on transactions.
|
||||
|
||||
Additionally, the constructions specified in this BIP can lay the groundwork for
|
||||
some potential future upgrades:
|
||||
* The TxFieldSelector construction would work well with a hypothetical opcode
|
||||
`OP_TX` that allows for directly introspecting the transaction by putting the
|
||||
fields selected on the stack instead of hashing them together.
|
||||
* The TransactionHash obtained by `OP_TXHASH` can be combined with `OP_CHECKSIGFROMSTACK`
|
||||
(see [BIP-348](bip-0348.md)) to effectively create an
|
||||
incredibly flexible signature hash, which would enable constructions like
|
||||
`SIGHASH_ANYPREVOUT`.
|
||||
* The TransactionHash obtained by `OP_TXHASH` can be introduced as a native
|
||||
sighash calculation in a future segwit upgrade, so that signatures using the
|
||||
TransactionHash as their sighash can be used in keyspend context.
|
||||
|
||||
|
||||
## Comparing with some alternative proposals
|
||||
|
||||
* This proposal strictly generalizes BIP-119's `OP_CHECKTEMPLATEVERIFY`, as the
|
||||
default mode of our TxFieldSelector is semantically the same (though not
|
||||
byte-for-byte identical) as what `OP_CTV` accomplishes, without costing any
|
||||
additional bytes. Additionally, using `OP_TXHASH` allows for more
|
||||
flexibility which can help in the case for
|
||||
* enabling adding fees to a transaction without breaking a multi-tx protocol;
|
||||
* multi-user protocols where users are only concerned about their own inputs and outputs.
|
||||
|
||||
* Constructions like `OP_IN_OUT_VALUE` used with `OP_EQUALVERIFY` can be
|
||||
emulated by two `OP_TXHASH` instances by using the TxFieldSelector to select
|
||||
a single input value first and a single output value second and enforcing
|
||||
equality on the hashes. Neither of these alternatives can be used to enforce
|
||||
small value differences without the availability of 64-bit arithmetic in
|
||||
Script.
|
||||
|
||||
* Like mentioned above, `SIGHASH_ANYPREVOUT` can be emulated using `OP_TXHASH`
|
||||
when combined with `OP_CHECKSIGFROMSTACK`:
|
||||
`<txfs> OP_TXHASH <pubkey> OP_CHECKSIGFROMSTACK` effectively emulates `SIGHASH_ANYPREVOUT`.
|
||||
|
||||
|
||||
# Reference Implementation
|
||||
|
||||
A reference implementation in Rust is provided attached as part of this BIP
|
||||
together with a JSON file of test vectors generated using the reference
|
||||
implementation.
|
||||
|
||||
|
||||
# Design Considerations
|
||||
|
||||
This specification in in _Draft_ and there is definitely still room for feedback
|
||||
and improvements. Some considerations that were made but could be revisited:
|
||||
|
||||
- The `0b10` in/output selector for the shorthand is unused.
|
||||
Could possibly be filled in with "current + next", "current + previous" or
|
||||
any other semantics.
|
||||
- The individual index selection semantics allow for absolute indices up to ~32k
|
||||
and relative ones up to +/- ~16k, which is probably excessive. When removing
|
||||
the second byte there would reduce that to just 256 and +/- 128 respectively.
|
||||
- Similar to `OP_TEMPLATEHASH` (BIP number to be assigned, [PR](https://github.com/bitcoin/bips/pull/1974)),
|
||||
we could not support `scriptSigs` anymore and remove
|
||||
`TXFS_INPUTS_SCRIPTSIGS`. This field could then possibly be repurposed.
|
||||
|
||||
|
||||
# Backwards Compatibility
|
||||
|
||||
`OP_TXHASH` replaces `OP_SUCCESS189`. The `SUCCESS` opcodes were
|
||||
introduced in taproot (BIP-342) to support changing the semantics of opcodes in
|
||||
ways that do allow the new semantics to change the stack. For this reason,
|
||||
`OP_TXHASH` only works in tapscript context. Since it is overriding a `SUCCESS`
|
||||
opcode, any older version of the software will always accept any script that
|
||||
uses the opcode, while the new versions of the software will validate the
|
||||
scripts according to the semantics outlined in this BIP. As such, this is also a
|
||||
soft fork change.
|
||||
|
||||
## Interactions with other BIPs
|
||||
|
||||
This proposal interacts with several other BIPs:
|
||||
|
||||
* **[BIP-118](bip-0118.mediawiki) (SIGHASH_ANYPREVOUT)**: `OP_TXHASH` can be combined with
|
||||
`OP_CHECKSIGFROMSTACK` (BIP-348) to emulate `SIGHASH_ANYPREVOUT` and other
|
||||
signature hash modes defined in BIP-118. The 1-byte TxFieldSelector format
|
||||
explicitly supports these modes.
|
||||
|
||||
* **[BIP-119](bip-0119.mediawiki) (OP_CHECKTEMPLATEVERIFY)**: `OP_TXHASH` with the empty
|
||||
TxFieldSelector produces a hash semantically equivalent to BIP-119's
|
||||
`OP_CHECKTEMPLATEVERIFY`, making this proposal a generalization of BIP-119.
|
||||
|
||||
* **[BIP-347](bip-0347.mediawiki) (OP_CAT)**: When combined with `OP_CAT`, `OP_TXHASH` enables
|
||||
powerful transaction introspection capabilities. The bit encoding format is
|
||||
designed to be explicit about endianness to ensure correct interaction with
|
||||
concatenation operations.
|
||||
|
||||
* **[BIP-348](bip-0348.md) (OP_CHECKSIGFROMSTACK)**: Together with `OP_CHECKSIGFROMSTACK`,
|
||||
`OP_TXHASH` provides a fully generalized signature hash construction,
|
||||
enabling flexible covenant designs and multi-user protocols.
|
||||
|
||||
|
||||
# Implementation
|
||||
|
||||
A reference implementation is included as part of the BIP, see
|
||||
[here](./bip-0346/ref-impl/src/main.rs). This implementation focusses on clarity
|
||||
and correctness, not on efficiency. A rudimentary set of test vectors is also
|
||||
generated from this implementation and included
|
||||
[here](./bip-0346/ref-impl/txhash_vectors.json).
|
||||
|
||||
Furthermore, following other implementation attempts exist:
|
||||
|
||||
* A proposed implementation for Bitcoin Core is available here:
|
||||
https://github.com/bitcoin/bitcoin/pull/29050
|
||||
* A proposed implementation for rust-bitcoin is available here:
|
||||
https://github.com/rust-bitcoin/rust-bitcoin/pull/2275
|
||||
|
||||
NOTE: These implementations are slightly outdated as they were made for an
|
||||
earlier version of this specification. Updates are in progress.
|
||||
|
||||
Both of the above implementations perform effective caching to avoid potential
|
||||
denial-of-service attack vectors.
|
||||
|
||||
|
||||
# Deployment
|
||||
|
||||
This BIP can be deployed using a BIP 9 VersionBits deployment. The specific
|
||||
strategy and bit assignment are left unspecified and can later be amended to the
|
||||
BIP depending on community preference.
|
||||
|
||||
|
||||
# Acknowledgement
|
||||
|
||||
Credit for this proposal mostly goes to Jeremy Rubin for his work on BIP-119's
|
||||
`OP_CHECKTEMPLATEVERIFY` and to Russell O'Connor for the original idea of
|
||||
generalizing `OP_CHECKTEMPLATEVERIFY` into `OP_TXHASH`.
|
||||
|
||||
Additional thanks to Andrew Poelstra, Greg Sanders, Rearden Code, Rusty Russell
|
||||
and others for their feedback on the specification.
|
||||
|
||||
|
||||
# Copyright
|
||||
|
||||
This document is licensed under the 3-clause BSD license.
|
||||
|
||||
206
bip-0346/ref-impl/Cargo.lock
generated
Normal file
206
bip-0346/ref-impl/Cargo.lock
generated
Normal file
@ -0,0 +1,206 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "base58ck"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
|
||||
dependencies = [
|
||||
"bitcoin-internals",
|
||||
"bitcoin_hashes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.32.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66"
|
||||
dependencies = [
|
||||
"base58ck",
|
||||
"bech32",
|
||||
"bitcoin-internals",
|
||||
"bitcoin-io",
|
||||
"bitcoin-units",
|
||||
"bitcoin_hashes",
|
||||
"hex-conservative",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-internals"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-io"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-units"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
|
||||
dependencies = [
|
||||
"bitcoin-internals",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
|
||||
dependencies = [
|
||||
"bitcoin-io",
|
||||
"hex-conservative",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"secp256k1-sys",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "txhash-ref"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
8
bip-0346/ref-impl/Cargo.toml
Normal file
8
bip-0346/ref-impl/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "txhash-ref"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { version = "=0.32.8", features = [ "serde" ] }
|
||||
serde_json = "1"
|
||||
693
bip-0346/ref-impl/src/main.rs
Normal file
693
bip-0346/ref-impl/src/main.rs
Normal file
@ -0,0 +1,693 @@
|
||||
|
||||
use bitcoin::{Transaction, TxOut};
|
||||
use bitcoin::consensus::encode::Encodable;
|
||||
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||
|
||||
pub const TXFS_VERSION: u8 = 1 << 0;
|
||||
pub const TXFS_LOCKTIME: u8 = 1 << 1;
|
||||
pub const TXFS_CURRENT_INPUT_IDX: u8 = 1 << 2;
|
||||
pub const TXFS_CURRENT_INPUT_CONTROL_BLOCK: u8 = 1 << 3;
|
||||
pub const TXFS_CURRENT_INPUT_SPENTSCRIPT: u8 = 1 << 4;
|
||||
pub const TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS: u8 = 1 << 5;
|
||||
pub const TXFS_CURRENT_INPUT_TAPROOT_ANNEX: u8 = 1 << 6;
|
||||
pub const TXFS_CONTROL: u8 = 1 << 7;
|
||||
|
||||
pub const TXFS_INPUTS_PREVOUTS: u8 = 1 << 0;
|
||||
pub const TXFS_INPUTS_SEQUENCES: u8 = 1 << 1;
|
||||
pub const TXFS_INPUTS_SCRIPTSIGS: u8 = 1 << 2;
|
||||
pub const TXFS_INPUTS_PREV_SCRIPTPUBKEYS: u8 = 1 << 3;
|
||||
pub const TXFS_INPUTS_PREV_VALUES: u8 = 1 << 4;
|
||||
pub const TXFS_INPUTS_TAPROOT_ANNEXES: u8 = 1 << 5;
|
||||
pub const TXFS_OUTPUTS_SCRIPTPUBKEYS: u8 = 1 << 6;
|
||||
pub const TXFS_OUTPUTS_VALUES: u8 = 1 << 7;
|
||||
|
||||
pub const TXFS_INPUTS_ALL: u8 = TXFS_INPUTS_PREVOUTS
|
||||
| TXFS_INPUTS_SEQUENCES
|
||||
| TXFS_INPUTS_SCRIPTSIGS
|
||||
| TXFS_INPUTS_PREV_SCRIPTPUBKEYS
|
||||
| TXFS_INPUTS_PREV_VALUES
|
||||
| TXFS_INPUTS_TAPROOT_ANNEXES;
|
||||
pub const TXFS_OUTPUTS_ALL: u8 = TXFS_OUTPUTS_SCRIPTPUBKEYS | TXFS_OUTPUTS_VALUES;
|
||||
|
||||
pub const TXFS_INOUT_NUMBER: u8 = 1 << 7;
|
||||
pub const TXFS_INOUT_SELECTION_NONE: u8 = 0x00;
|
||||
pub const TXFS_INOUT_SELECTION_CURRENT: u8 = 0x40;
|
||||
pub const TXFS_INOUT_SELECTION_ALL: u8 = 0x3f;
|
||||
pub const TXFS_INOUT_SELECTION_MODE: u8 = 1 << 6;
|
||||
pub const TXFS_INOUT_LEADING_SIZE: u8 = 1 << 5;
|
||||
pub const TXFS_INOUT_INDIVIDUAL_MODE: u8 = 1 << 5;
|
||||
pub const TXFS_INOUT_SELECTION_MASK: u8 = 0xff ^ (1 << 7) ^ (1 << 6) ^ (1 << 5);
|
||||
|
||||
|
||||
pub const TXFS_SPECIAL_TEMPLATE: [u8; 4] = [
|
||||
TXFS_VERSION | TXFS_LOCKTIME | TXFS_CURRENT_INPUT_IDX,
|
||||
TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_OUTPUTS_ALL,
|
||||
TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL,
|
||||
TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL,
|
||||
];
|
||||
|
||||
const SHA256_EMPTY: sha256::Hash = sha256::Hash::const_hash(&[]);
|
||||
|
||||
/// Interpret the bits of the input byte as a signed 7-bit integer and return the
|
||||
/// value as an i8.
|
||||
fn read_i7(input: u8) -> i8 {
|
||||
let masked = input & 0x7f;
|
||||
if (masked & 0x40) == 0 {
|
||||
masked as i8
|
||||
} else {
|
||||
0i8 - ((!(masked-1)) & 0x7f) as i8
|
||||
}
|
||||
}
|
||||
|
||||
/// Interpret the bits of the input bytes as a signed 15-bit integer and return the
|
||||
/// value as an i16.
|
||||
fn read_i15(input: u16) -> i16 {
|
||||
let masked = input & 0x7fff;
|
||||
if (masked & 0x4000) == 0 {
|
||||
masked as i16
|
||||
} else {
|
||||
0i16 - ((!(masked-1)) & 0x7fff) as i16
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_short_txfs(txfs: u8) -> Result<[u8; 4], &'static str> {
|
||||
let mut base = TXFS_VERSION | TXFS_LOCKTIME | TXFS_CONTROL | TXFS_CURRENT_INPUT_TAPROOT_ANNEX;
|
||||
let mut inout_fields = TXFS_OUTPUTS_ALL | TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS;
|
||||
|
||||
let input_selection = match txfs & 0b00000011 {
|
||||
0b00000000 => TXFS_INOUT_SELECTION_NONE,
|
||||
0b00000001 => TXFS_INOUT_SELECTION_CURRENT,
|
||||
0b00000011 => TXFS_INOUT_SELECTION_ALL,
|
||||
_ => return Err("0b10 is not a valid input selection"),
|
||||
};
|
||||
let output_selection = match txfs & 0b00001100 {
|
||||
0b00000000 => TXFS_INOUT_SELECTION_NONE,
|
||||
0b00000100 => TXFS_INOUT_SELECTION_CURRENT,
|
||||
0b00001100 => TXFS_INOUT_SELECTION_ALL,
|
||||
_ => return Err("0b10 is not a valid output selection"),
|
||||
};
|
||||
|
||||
if txfs & 0b00010000 != 0 {
|
||||
inout_fields = inout_fields | TXFS_INPUTS_PREVOUTS;
|
||||
}
|
||||
|
||||
if txfs & 0b00100000 != 0 {
|
||||
inout_fields = inout_fields | TXFS_INPUTS_PREV_SCRIPTPUBKEYS | TXFS_INPUTS_PREV_VALUES;
|
||||
}
|
||||
|
||||
if txfs & 0b01000000 != 0 {
|
||||
base = base | TXFS_CURRENT_INPUT_CONTROL_BLOCK | TXFS_CURRENT_INPUT_SPENTSCRIPT
|
||||
| TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS;
|
||||
}
|
||||
|
||||
if txfs & 0b10000000 != 0 {
|
||||
base = base | TXFS_CURRENT_INPUT_IDX;
|
||||
}
|
||||
|
||||
Ok([base, inout_fields, input_selection, output_selection])
|
||||
}
|
||||
|
||||
/// Parse an input or output selection from the TxFieldSelector bytes.
|
||||
///
|
||||
/// Returns the selected indices and a flag whether to commit the number of items.
|
||||
fn parse_inout_selection(
|
||||
first_byte: u8,
|
||||
bytes: &mut impl Iterator<Item = u8>,
|
||||
nb_items: usize,
|
||||
current_input_idx: u32,
|
||||
) -> Result<(Vec<usize>, bool), &'static str> {
|
||||
let commit_number = (first_byte & TXFS_INOUT_NUMBER) != 0;
|
||||
let selection = first_byte & (0xff ^ TXFS_INOUT_NUMBER);
|
||||
|
||||
let selected = if selection == TXFS_INOUT_SELECTION_NONE {
|
||||
vec![]
|
||||
} else if selection == TXFS_INOUT_SELECTION_ALL {
|
||||
(0..nb_items).collect()
|
||||
} else if selection == TXFS_INOUT_SELECTION_CURRENT {
|
||||
if current_input_idx as usize >= nb_items {
|
||||
// NB can only happen for outputs
|
||||
return Err("current input index exceeds number of outputs and current output selected");
|
||||
}
|
||||
vec![current_input_idx as usize]
|
||||
} else if (selection & TXFS_INOUT_SELECTION_MODE) == 0 {
|
||||
// leading mode
|
||||
let count = if (selection & TXFS_INOUT_LEADING_SIZE) == 0 {
|
||||
(selection & TXFS_INOUT_SELECTION_MASK) as usize
|
||||
} else {
|
||||
let next_byte = bytes.next().ok_or("second leading selection byte missing")?;
|
||||
(((selection & TXFS_INOUT_SELECTION_MASK) as usize) << 8) + next_byte as usize
|
||||
};
|
||||
assert_ne!(count, 0, "this should be interpreted as NONE above");
|
||||
if count > nb_items {
|
||||
return Err("selected number of leading in/outputs out of bounds");
|
||||
}
|
||||
(0..count).collect()
|
||||
} else {
|
||||
// individual mode
|
||||
let absolute = (selection & TXFS_INOUT_INDIVIDUAL_MODE) == 0;
|
||||
|
||||
let count = (selection & TXFS_INOUT_SELECTION_MASK) as usize;
|
||||
|
||||
let mut selected = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
let first = bytes.next().ok_or("expected an index byte")?;
|
||||
let single_byte = (first & (1 << 7)) == 0;
|
||||
let number = if single_byte {
|
||||
first as usize
|
||||
} else {
|
||||
let next_byte = bytes.next().ok_or("expected another index byte")?;
|
||||
(((first & (1 << 7)) as usize) << 8) + next_byte as usize
|
||||
};
|
||||
|
||||
let idx = if absolute {
|
||||
number
|
||||
} else {
|
||||
let rel = if single_byte {
|
||||
read_i7(number as u8) as isize
|
||||
} else {
|
||||
read_i15(number as u16) as isize
|
||||
};
|
||||
|
||||
if rel.is_negative() && rel.abs() > current_input_idx as isize {
|
||||
return Err("relative index out of bounds");
|
||||
}
|
||||
(current_input_idx as isize + rel) as usize
|
||||
};
|
||||
|
||||
if idx > nb_items {
|
||||
return Err("selected index out of bounds");
|
||||
}
|
||||
if let Some(last) = selected.last() {
|
||||
if idx <= *last {
|
||||
return Err("selected indices not in increasing order")
|
||||
}
|
||||
}
|
||||
selected.push(idx);
|
||||
}
|
||||
selected
|
||||
};
|
||||
Ok((selected, commit_number))
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Assumes that TxFieldSelector is valid.
|
||||
pub fn calculate_txhash(
|
||||
txfs: &[u8],
|
||||
tx: &Transaction,
|
||||
prevouts: &[TxOut],
|
||||
current_input_idx: u32,
|
||||
current_input_last_codeseparator_pos: Option<u32>,
|
||||
) -> Result<sha256::Hash, &'static str> {
|
||||
assert_eq!(tx.input.len(), prevouts.len());
|
||||
|
||||
let txfs = if txfs.is_empty() {
|
||||
TXFS_SPECIAL_TEMPLATE.to_vec()
|
||||
} else if txfs.len() == 1 {
|
||||
convert_short_txfs(txfs[0])?.to_vec()
|
||||
} else {
|
||||
txfs.to_vec()
|
||||
};
|
||||
let txfs = &txfs;
|
||||
|
||||
let mut engine = sha256::Hash::engine();
|
||||
|
||||
if (txfs[0] & TXFS_CONTROL) != 0 {
|
||||
engine.input(txfs);
|
||||
}
|
||||
|
||||
let mut bytes = txfs.iter().copied().peekable();
|
||||
let global = bytes.next().unwrap();
|
||||
|
||||
if (global & TXFS_VERSION) != 0 {
|
||||
tx.version.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if (global & TXFS_LOCKTIME) != 0 {
|
||||
tx.lock_time.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if (global & TXFS_CURRENT_INPUT_IDX) != 0 {
|
||||
(current_input_idx as u32).consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
let current_prevout = &prevouts[current_input_idx as usize];
|
||||
let current_input = &tx.input[current_input_idx as usize];
|
||||
|
||||
if (global & TXFS_CURRENT_INPUT_CONTROL_BLOCK) != 0 {
|
||||
assert!(current_prevout.script_pubkey.is_p2tr(), "only active in taproot context");
|
||||
if let Some(cb) = current_input.witness.taproot_control_block() {
|
||||
engine.input(&sha256::Hash::hash(&cb)[..]);
|
||||
} else {
|
||||
// keyspend
|
||||
engine.input(&SHA256_EMPTY[..]);
|
||||
}
|
||||
}
|
||||
|
||||
if (global & TXFS_CURRENT_INPUT_SPENTSCRIPT) != 0 {
|
||||
assert!(current_prevout.script_pubkey.is_p2tr(), "only active in taproot context");
|
||||
if let Some(script) = current_input.witness.taproot_leaf_script() {
|
||||
let mut eng = sha256::Hash::engine();
|
||||
script.version.to_consensus().consensus_encode(&mut eng).unwrap();
|
||||
script.script.consensus_encode(&mut eng).unwrap();
|
||||
engine.input(&sha256::Hash::from_engine(eng)[..]);
|
||||
} else {
|
||||
engine.input(&SHA256_EMPTY[..]);
|
||||
}
|
||||
}
|
||||
|
||||
if (global & TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS) != 0 {
|
||||
let pos = current_input_last_codeseparator_pos.unwrap_or(u32::MAX);
|
||||
(pos as u32).consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if (global & TXFS_CURRENT_INPUT_TAPROOT_ANNEX) != 0 {
|
||||
if let Some(annex) = current_input.witness.taproot_annex() {
|
||||
engine.input(&sha256::Hash::hash(annex)[..]);
|
||||
} else {
|
||||
engine.input(&SHA256_EMPTY[..]);
|
||||
}
|
||||
}
|
||||
|
||||
let inout_fields = bytes.next().unwrap_or(0x00);
|
||||
|
||||
// Inputs
|
||||
let (input_selection, commit_number_inputs) = if let Some(first_byte) = bytes.next() {
|
||||
parse_inout_selection(first_byte, &mut bytes, tx.input.len(), current_input_idx)?
|
||||
} else {
|
||||
(vec![], false)
|
||||
};
|
||||
|
||||
if commit_number_inputs {
|
||||
(tx.input.len() as u32).consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_PREVOUTS) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &input_selection {
|
||||
tx.input[*i].previous_output.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
engine.input(&hash[..]);
|
||||
}
|
||||
|
||||
if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_SEQUENCES) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &input_selection {
|
||||
tx.input[*i].sequence.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
engine.input(&hash[..]);
|
||||
}
|
||||
|
||||
if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_SCRIPTSIGS) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &input_selection {
|
||||
engine.input(&sha256::Hash::hash(&tx.input[*i].script_sig.as_bytes())[..]);
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
engine.input(&hash[..]);
|
||||
}
|
||||
|
||||
if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_PREV_SCRIPTPUBKEYS) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &input_selection {
|
||||
engine.input(&sha256::Hash::hash(&prevouts[*i].script_pubkey.as_bytes())[..]);
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
engine.input(&hash[..]);
|
||||
}
|
||||
|
||||
if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_PREV_VALUES) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &input_selection {
|
||||
prevouts[*i].value.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
engine.input(&hash[..]);
|
||||
}
|
||||
|
||||
if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_TAPROOT_ANNEXES) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &input_selection {
|
||||
if prevouts[*i].script_pubkey.is_p2tr() {
|
||||
if let Some(annex) = tx.input[*i].witness.taproot_annex() {
|
||||
engine.input(&sha256::Hash::hash(annex)[..]);
|
||||
} else {
|
||||
engine.input(&SHA256_EMPTY[..]);
|
||||
}
|
||||
} else {
|
||||
engine.input(&SHA256_EMPTY[..]);
|
||||
}
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
engine.input(&hash[..]);
|
||||
}
|
||||
|
||||
// Outputs
|
||||
let (output_selection, commit_number_outputs) = if let Some(first_byte) = bytes.next() {
|
||||
parse_inout_selection(first_byte, &mut bytes, tx.output.len(), current_input_idx)?
|
||||
} else {
|
||||
(vec![], false)
|
||||
};
|
||||
|
||||
if commit_number_outputs {
|
||||
(tx.output.len() as u32).consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if !output_selection.is_empty() && (inout_fields & TXFS_OUTPUTS_SCRIPTPUBKEYS) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &output_selection {
|
||||
engine.input(&sha256::Hash::hash(&tx.output[*i].script_pubkey.as_bytes())[..]);
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
hash.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if !output_selection.is_empty() && (inout_fields & TXFS_OUTPUTS_VALUES) != 0 {
|
||||
let hash = {
|
||||
let mut engine = sha256::Hash::engine();
|
||||
for i in &output_selection {
|
||||
tx.output[*i].value.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
sha256::Hash::from_engine(engine)
|
||||
};
|
||||
hash.consensus_encode(&mut engine).unwrap();
|
||||
}
|
||||
|
||||
if bytes.next().is_some() {
|
||||
return Err("unexpected additional txfs bytes");
|
||||
}
|
||||
Ok(sha256::Hash::from_engine(engine))
|
||||
}
|
||||
|
||||
mod test_vectors {
|
||||
use super::*;
|
||||
use std::any::Any;
|
||||
use std::ops::{self, RangeBounds};
|
||||
use bitcoin::hex::DisplayHex;
|
||||
use bitcoin::{Amount, ScriptBuf, Sequence, Witness};
|
||||
use bitcoin::blockdata::transaction::{self, TxIn};
|
||||
use bitcoin::opcodes::all::*;
|
||||
|
||||
fn test_vector_tx() -> (Transaction, Vec<TxOut>) {
|
||||
let tx = Transaction {
|
||||
version: transaction::Version::TWO,
|
||||
lock_time: bitcoin::absolute::LockTime::from_consensus(42),
|
||||
input: vec![
|
||||
TxIn {
|
||||
previous_output: "3333333333333333333333333333333333333333333333333333333333333333:3".parse().unwrap(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::from_consensus(2),
|
||||
witness: {
|
||||
let mut buf = Witness::new();
|
||||
buf.push(vec![0x12]);
|
||||
buf
|
||||
},
|
||||
},
|
||||
TxIn {
|
||||
previous_output: "4444444444444444444444444444444444444444444444444444444444444444:4".parse().unwrap(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::from_consensus(3),
|
||||
witness: {
|
||||
let mut buf = Witness::new();
|
||||
buf.push(vec![0x13]);
|
||||
buf.push(vec![0x14]);
|
||||
buf.push(vec![0x50, 0x42]); // annex
|
||||
buf
|
||||
},
|
||||
},
|
||||
TxIn {
|
||||
previous_output: "1111111111111111111111111111111111111111111111111111111111111111:1".parse().unwrap(),
|
||||
script_sig: vec![0x23].into(),
|
||||
sequence: Sequence::from_consensus(1),
|
||||
witness: Witness::new(),
|
||||
},
|
||||
TxIn {
|
||||
previous_output: "2222222222222222222222222222222222222222222222222222222222222222:2".parse().unwrap(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::from_consensus(3),
|
||||
witness: { // p2wsh annex-like stack element
|
||||
let mut buf = Witness::new();
|
||||
buf.push(vec![0x13]);
|
||||
buf.push(vec![0x14]);
|
||||
buf.push(vec![0x50, 0x42]); // annex
|
||||
buf
|
||||
},
|
||||
},
|
||||
],
|
||||
output: vec![
|
||||
TxOut {
|
||||
script_pubkey: vec![OP_PUSHNUM_6.to_u8()].into(),
|
||||
value: Amount::from_sat(350),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: vec![OP_PUSHNUM_7.to_u8()].into(),
|
||||
value: Amount::from_sat(351),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: vec![OP_PUSHNUM_8.to_u8()].into(),
|
||||
value: Amount::from_sat(353),
|
||||
},
|
||||
],
|
||||
};
|
||||
let prevs = vec![
|
||||
TxOut {
|
||||
script_pubkey: vec![ // p2tr
|
||||
0x51, 0x20, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
].into(),
|
||||
value: Amount::from_sat(361),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: vec![ // p2tr
|
||||
0x51, 0x20, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
].into(),
|
||||
value: Amount::from_sat(362),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: vec![OP_PUSHNUM_16.to_u8()].into(),
|
||||
value: Amount::from_sat(360),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: vec![ // p2wsh
|
||||
0x00, 0x20, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
].into(),
|
||||
value: Amount::from_sat(361),
|
||||
},
|
||||
];
|
||||
(tx, prevs)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestCase {
|
||||
tx: Transaction,
|
||||
prevs: Vec<TxOut>,
|
||||
vectors: Vec<TestVector>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestVector {
|
||||
txfs: Vec<u8>,
|
||||
input: usize,
|
||||
codeseparator: Option<u32>,
|
||||
txhash: sha256::Hash,
|
||||
}
|
||||
|
||||
fn generate_vectors() -> Vec<TestCase> {
|
||||
let all = 0xff;
|
||||
let allio = TXFS_INPUTS_ALL | TXFS_OUTPUTS_ALL;
|
||||
let selnone = TXFS_INOUT_SELECTION_NONE; // 0x00
|
||||
let selcur = TXFS_INOUT_SELECTION_CURRENT;
|
||||
let selall = TXFS_INOUT_SELECTION_ALL;
|
||||
let number = TXFS_INOUT_NUMBER;
|
||||
let leading = 0;
|
||||
let individual = TXFS_INOUT_SELECTION_MODE;
|
||||
let absolute = 0;
|
||||
let relative = TXFS_INOUT_INDIVIDUAL_MODE;
|
||||
|
||||
fn r<T: RangeBounds<usize> + 'static>(t: T) -> Option<Box<dyn Any>> {
|
||||
Some(Box::new(t))
|
||||
}
|
||||
|
||||
// txfs and range of inputs to run it on
|
||||
let selectors: &[(&[u8], Option<Box<dyn Any>>)] = &[
|
||||
// global
|
||||
(&[1 << 0, 0], None),
|
||||
(&[1 << 1, 0], None),
|
||||
(&[1 << 2, 0], None),
|
||||
(&[1 << 3, 0], None),
|
||||
(&[1 << 4, 0], None),
|
||||
(&[1 << 5, 0], None),
|
||||
(&[1 << 6, 0], None),
|
||||
(&[1 << 7, 0], None),
|
||||
// outputs
|
||||
(&[all, 0, 0, number | selnone], None),
|
||||
(&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, selcur], None),
|
||||
(&[all, TXFS_OUTPUTS_VALUES, 0, selcur], None),
|
||||
(&[all, TXFS_OUTPUTS_ALL, 0, selcur], None),
|
||||
(&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, selall], None),
|
||||
(&[all, TXFS_OUTPUTS_VALUES, 0, selall], None),
|
||||
(&[all, TXFS_OUTPUTS_ALL, 0, selall], None),
|
||||
(&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, number | selcur], None),
|
||||
(&[all, TXFS_OUTPUTS_VALUES, 0, number | selcur], None),
|
||||
(&[all, TXFS_OUTPUTS_ALL, 0, number | selcur], None),
|
||||
(&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, number | selall], None),
|
||||
(&[all, TXFS_OUTPUTS_VALUES, 0, number | selall], None),
|
||||
(&[all, TXFS_OUTPUTS_ALL, 0, number | selall], None),
|
||||
// inputs
|
||||
(&[all, 0, number | selnone], None),
|
||||
(&[all, TXFS_INPUTS_PREVOUTS, selcur], None),
|
||||
(&[all, TXFS_INPUTS_SEQUENCES, selcur], None),
|
||||
(&[all, TXFS_INPUTS_SCRIPTSIGS, selcur], None),
|
||||
(&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, selcur], None),
|
||||
(&[all, TXFS_INPUTS_PREV_VALUES, selcur], None),
|
||||
(&[all, TXFS_INPUTS_TAPROOT_ANNEXES, selcur], None),
|
||||
(&[all, TXFS_INPUTS_ALL, selcur], None),
|
||||
(&[all, TXFS_INPUTS_PREVOUTS, selall], None),
|
||||
(&[all, TXFS_INPUTS_SEQUENCES, selall], None),
|
||||
(&[all, TXFS_INPUTS_SCRIPTSIGS, selall], None),
|
||||
(&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, selall], None),
|
||||
(&[all, TXFS_INPUTS_PREV_VALUES, selall], None),
|
||||
(&[all, TXFS_INPUTS_TAPROOT_ANNEXES, selall], None),
|
||||
(&[all, TXFS_INPUTS_ALL, selall], None),
|
||||
(&[all, TXFS_INPUTS_PREVOUTS, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_SEQUENCES, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_SCRIPTSIGS, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_PREV_VALUES, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_TAPROOT_ANNEXES, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_ALL, number | selcur], None),
|
||||
(&[all, TXFS_INPUTS_PREVOUTS, number | selall], None),
|
||||
(&[all, TXFS_INPUTS_SEQUENCES, number | selall], None),
|
||||
(&[all, TXFS_INPUTS_SCRIPTSIGS, number | selall], None),
|
||||
(&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, number | selall], None),
|
||||
(&[all, TXFS_INPUTS_PREV_VALUES, number | selall], None),
|
||||
(&[all, TXFS_INPUTS_TAPROOT_ANNEXES, number | selall], None),
|
||||
(&[all, TXFS_INPUTS_ALL, number | selall], None),
|
||||
// both
|
||||
(&[all, allio, selall, selall], None),
|
||||
(&[all, allio, selcur, selcur], None),
|
||||
(&[all, 0, number | selnone, number | selnone], None),
|
||||
(&[all, allio, number | selall, number | selall], None),
|
||||
(&[all, allio, number | selcur, number | selcur], None),
|
||||
(&[all, allio, selcur, selall], None),
|
||||
(&[all, allio, selall, selcur], None),
|
||||
// leading
|
||||
(&[all, allio, leading | 0x01, number | leading | 0x02], None),
|
||||
(&[all, allio, number | selcur, leading | 0x02], None),
|
||||
// individual absolute
|
||||
(&[all, allio, individual | absolute | 0x01, 0x01,
|
||||
individual | absolute | 0x02, 0x00, 0x02], None),
|
||||
(&[all, allio, number | individual | absolute | 0x01, 0x01,
|
||||
number | individual | absolute | 0x02, 0x00, 0x02], None),
|
||||
// individual relative
|
||||
(&[all, allio, individual | relative | 0x01, (-1i8 as u8) >> 1,
|
||||
individual | relative | 0x02, (-1i8 as u8) >> 1, 0], r(1..2)),
|
||||
(&[all, allio, number | individual | relative | 0x01, (-1i8 as u8) >> 1,
|
||||
number | individual | relative | 0x02, (-1i8 as u8) >> 1, 0], r(1..2)),
|
||||
//TODO(stevenroose) test index size, but for that we need > 32 in/outputs
|
||||
// special case template
|
||||
(&[], None),
|
||||
// shorthand txfs, sighash examples
|
||||
(&[0b11111111], None),
|
||||
(&[0b11110111], None),
|
||||
(&[0b11110011], None),
|
||||
(&[0b11111101], None),
|
||||
(&[0b11110101], None),
|
||||
(&[0b11110001], None),
|
||||
(&[0b11101101], None),
|
||||
(&[0b11100101], None),
|
||||
(&[0b11100001], None),
|
||||
(&[0b11001101], None),
|
||||
(&[0b11000101], None),
|
||||
(&[0b11000001], None),
|
||||
];
|
||||
|
||||
let cases = vec![
|
||||
test_vector_tx(),
|
||||
];
|
||||
|
||||
fn check_range(r: &Box<dyn Any>, idx: usize) -> bool {
|
||||
if let Some(ref range) = r.downcast_ref::<ops::RangeFull>() {
|
||||
return range.contains(&idx);
|
||||
}
|
||||
if let Some(ref range) = r.downcast_ref::<ops::Range<usize>>() {
|
||||
return range.contains(&idx);
|
||||
}
|
||||
if let Some(ref range) = r.downcast_ref::<ops::RangeFrom<usize>>() {
|
||||
return range.contains(&idx);
|
||||
}
|
||||
if let Some(ref range) = r.downcast_ref::<ops::RangeTo<usize>>() {
|
||||
return range.contains(&idx);
|
||||
}
|
||||
unreachable!("invalid range type used: {:?}", r.type_id());
|
||||
}
|
||||
|
||||
cases.into_iter().enumerate().map(|(cidx, (tx, prevs))| {
|
||||
let mut vectors = Vec::new();
|
||||
for (_sidx, (txfs, idx_range)) in selectors.iter().enumerate() {
|
||||
for i in 0..tx.input.len() {
|
||||
let default = r(..2); // only 2 fist inputs are taproot
|
||||
let range = idx_range.as_ref().unwrap_or(default.as_ref().unwrap());
|
||||
if !check_range(range, i) {
|
||||
continue;
|
||||
}
|
||||
// println!("{} >> #{} ({}) >> {}", cidx, _sidx, txfs.as_hex(), i);
|
||||
|
||||
match calculate_txhash(txfs, &tx, &prevs, i as u32, None) {
|
||||
Ok(txhash) => vectors.push(TestVector {
|
||||
txfs: txfs.to_vec(),
|
||||
input: i,
|
||||
codeseparator: None,
|
||||
txhash: txhash,
|
||||
}),
|
||||
Err(e) => panic!("Error in vector #{} for selector {}: {}",
|
||||
cidx, txfs.as_hex(), e,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
TestCase { tx, prevs, vectors }
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn write_vector_file(path: impl AsRef<std::path::Path>) {
|
||||
use bitcoin::consensus::encode::serialize_hex;
|
||||
|
||||
let ret = generate_vectors().into_iter().enumerate().map(|(i_tx, c)| serde_json::json!({
|
||||
"tx": serialize_hex(&c.tx),
|
||||
"prevs": c.prevs.iter().map(|p| serialize_hex(p)).collect::<Vec<_>>(),
|
||||
"vectors": c.vectors.into_iter().enumerate().map(|(i_v, v)| serde_json::json!({
|
||||
"id": format!("{}:{} ({} #{})", i_tx, i_v, v.txfs.as_hex(), v.input),
|
||||
"txfs": v.txfs.as_hex().to_string(),
|
||||
"input": v.input,
|
||||
"codeseparator": v.codeseparator,
|
||||
"txhash": v.txhash,
|
||||
})).collect::<Vec<_>>(),
|
||||
})).collect::<Vec<_>>();
|
||||
|
||||
let mut file = std::fs::File::create(path).unwrap();
|
||||
serde_json::to_writer_pretty(&mut file, &ret).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_vectors::write_vector_file("./txhash_vectors.json");
|
||||
}
|
||||
1063
bip-0346/ref-impl/txhash_vectors.json
Normal file
1063
bip-0346/ref-impl/txhash_vectors.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user