Skip to content

Commit

Permalink
txhash: Move #in/out to in/out byte and add control bit
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenroose committed Oct 12, 2023
1 parent 28fcd5c commit ec98a85
Showing 1 changed file with 119 additions and 62 deletions.
181 changes: 119 additions & 62 deletions bip-txhash.mediawiki
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,30 @@ OP_TXHASH does the following:
The TxFieldSelector has the following semantics. We will give a brief conceptual
summary, followed by a reference implementation of the CalculateTxHash function.

* If the TxFieldSelector is zero bytes long, it is set equal to
0xff|0xf6|0x3f|0x3f, the de-facto default value which means everything except
the prevouts and the prevout scriptPubkeys.
* There are two special cases for the TxFieldSelector:
- the empty value, zero bytes long: it is set equal to 0xff|0xf6|0xbf|0xbf,
the de-facto default value which means everything except the prevouts and
the prevout scriptPubkeys.
- the 0x00 byte: it is set equal to 0xff|0xff|0xbf|0xbf, which means "ALL"
and is primarily useful to emulate SIGHASH_ALL when OP_TXHASH is used in
combination with OP_CHECKSIGFROMSTACK.
* The first byte of the TxFieldSelector has its 8 bits assigned as follows,
from lowest to highest:
1. version
2. locktime
3. nb_inputs
4. nb_outputs
5. current input index
6. current input control block (only in case of tapscript spend)
7. inputs
8. outputs
3. current input index
4. current input control block (only in case of tapscript spend)
5. current script last OP_CODESEPARATOR position
6. inputs
7. outputs
* The last (highest) bit of the first byte, we will call the "control bit", and
it can be used to control the behavior of the opcode. For OP_TXHASH and
OP_CHECKTXHASHVERIFY, 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.)
* If either "inputs" or "outputs" is set to 1, expect another byte with its 8
bits assigning the following variables, from lowest to highest:
Expand All @@ -84,21 +95,24 @@ summary, followed by a reference implementation of the CalculateTxHash function.
For both inputs and then outputs, do the following:

* If the "in/outputs" field is set to 1, another additional byte is expected:
* There are two exceptional values:
- 0x00 means "select only the in/output of the current input index".
- 0x3f means "select all in/outputs". //TODO(stevenroose) make this 0xff?
* The highest bit is the "specification mode":
* The highest bit indicates whether the "number of in-/outputs" should be
committed to.
* For the remaining bits, there are three exceptional values:
- 0x00 means "no in/outputs" (hence only the number of them as 0x80)
- 0x40 means "select only the in/output of the current input index".
- 0x3f means "select all in/outputs".
* The second highest bit is the "specification mode":
- Set to 0 it means "prefix mode".
- Set to 1 it means "individual mode".
* The second highest bit is used to indicate the "index size", i.e. the number
* The third highest bit is used to indicate the "index size", i.e. the number
of bytes will be used to represent in/output indices.
* In "prefix mode",
- With "index size" set to 0, the remaining lowest 6 bits of the first byte
- 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 6 bits of the first byte
- 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 remaining lowest 6 bits of the first byte will be
* In "individual mode", the remaining lowest 5 bits of the first byte will be
interpreted as `n`, the number of individual in/outputs to select.
- With "index size" set to 0, interpret the following `n` individual bytes
as the indices of an individual in/outputs to select.
Expand Down Expand Up @@ -224,21 +238,22 @@ some potential future upgrades:
<source lang="rust">
pub const TXFS_VERSION: u8 = 1 << 0;
pub const TXFS_LOCKTIME: u8 = 1 << 1;
pub const TXFS_NB_INPUTS: u8 = 1 << 2;
pub const TXFS_NB_OUTPUTS: u8 = 1 << 3;
pub const TXFS_CURRENT_INPUT_IDX: u8 = 1 << 4;
pub const TXFS_CURRENT_INPUT_CONTROL_BLOCK: u8 = 1 << 5;
pub const TXFS_INPUTS: u8 = 1 << 6;
pub const TXFS_OUTPUTS: u8 = 1 << 7;
pub const TXFS_CURRENT_INPUT_IDX: u8 = 1 << 2;
pub const TXFS_CURRENT_INPUT_CONTROL_BLOCK: u8 = 1 << 3;
pub const TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS: u8 = 1 << 4;
pub const TXFS_INPUTS: u8 = 1 << 5;
pub const TXFS_OUTPUTS: u8 = 1 << 6;

pub const TXFS_CONTROL: u8 = 1 << 7;

pub const TXFS_ALL: u8 = TXFS_VERSION
| TXFS_LOCKTIME
| TXFS_NB_INPUTS
| TXFS_NB_OUTPUTS
| TXFS_CURRENT_INPUT_IDX
| TXFS_CURRENT_INPUT_CONTROL_BLOCK
| TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS
| TXFS_INPUTS
| TXFS_OUTPUTS;
| TXFS_OUTPUTS
| TXFS_CONTROL;

pub const TXFS_INPUTS_PREVOUTS: u8 = 1 << 0;
pub const TXFS_INPUTS_SEQUENCES: u8 = 1 << 1;
Expand All @@ -261,25 +276,39 @@ pub const TXFS_INPUTS_DEFAULT: u8 = TXFS_INPUTS_SEQUENCES
| TXFS_INPUTS_TAPROOT_ANNEXES;
pub const TXFS_OUTPUTS_ALL: u8 = TXFS_OUTPUTS_SCRIPT_PUBKEYS | TXFS_OUTPUTS_VALUES;

pub const TXFS_INOUT_RANGE_CURRENT: u8 = 0x00;
pub const TXFS_INOUT_RANGE_ALL: u8 = 0xf3;
pub const TXFS_INOUT_RANGE_MODE: u8 = 1 << 7;
pub const TXFS_INOUT_RANGE_SIZE: u8 = 1 << 6;
pub const TXFS_INOUT_RANGE_MASK: u8 = 0xff ^ TXFS_INOUT_RANGE_MODE ^ TXFS_INOUT_RANGE_SIZE;
pub const TXFS_INOUT_NUMBER: u8 = 1 << 7;
pub const TXFS_INOUT_RANGE_NONE: u8 = 0x00;
pub const TXFS_INOUT_RANGE_CURRENT: u8 = 0x40;
pub const TXFS_INOUT_RANGE_ALL: u8 = 0x3f;
pub const TXFS_INOUT_RANGE_MODE: u8 = 1 << 6;
pub const TXFS_INOUT_RANGE_SIZE: u8 = 1 << 5;
pub const TXFS_INOUT_RANGE_MASK: u8 =
0xff ^ TXFS_INOUT_NUMBER ^ TXFS_INOUT_RANGE_MODE ^ TXFS_INOUT_RANGE_SIZE;

pub const DEFAULT_TXFS: [u8; 4] = [
pub const TXFS_TEMPLATE_ALL: [u8; 4] = [
TXFS_ALL,
TXFS_INPUTS_ALL | TXFS_OUTPUTS_ALL,
TXFS_INOUT_NUMBER | TXFS_INOUT_RANGE_ALL,
TXFS_INOUT_NUMBER | TXFS_INOUT_RANGE_ALL,
];
pub const TXFS_TEMPLATE_DEFAULT: [u8; 4] = [
TXFS_ALL,
TXFS_INPUTS_DEFAULT | TXFS_OUTPUTS_ALL,
TXFS_INOUT_RANGE_ALL,
TXFS_INOUT_RANGE_ALL,
TXFS_INOUT_NUMBER | TXFS_INOUT_RANGE_ALL,
TXFS_INOUT_NUMBER | TXFS_INOUT_RANGE_ALL,
];


fn validate_txfieldselector_inout_range(
bytes: &mut impl Iterator<Item = u8>,
nb_items: usize,
) -> Result<(), &'static str> {
let range = bytes.next().ok_or("unexpected EOF")?;
if range == TXFS_INOUT_RANGE_ALL || range == TXFS_INOUT_RANGE_CURRENT {
let nb_mask = 0xff ^ TXFS_INOUT_NUMBER;
let range = bytes.next().ok_or("unexpected EOF")? & nb_mask;
if range == TXFS_INOUT_RANGE_NONE
|| range == TXFS_INOUT_RANGE_ALL
|| range == TXFS_INOUT_RANGE_CURRENT
{
return Ok(())
} else if range & TXFS_INOUT_RANGE_MODE == 0 { // leading mode
if range & TXFS_INOUT_RANGE_SIZE == 0 {
Expand Down Expand Up @@ -343,7 +372,7 @@ pub fn validate_txfieldselector(
nb_inputs: usize,
nb_outputs: usize,
) -> Result<(), &'static str> {
if txfs.is_empty() {
if txfs.is_empty() || (txfs.len() == 1 && txfs[0] == 0x00) {
return Ok(());
}

Expand Down Expand Up @@ -383,54 +412,70 @@ pub fn validate_txfieldselector(
Ok(())
}

///
///
/// 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>,
) -> sha256::Hash {
assert!(validate_txfieldselector(txfs, tx.input.len(), tx.output.len()).is_ok());
assert_eq!(tx.input.len(), prevouts.len());

let txfs = if txfs.is_empty() {
&DEFAULT_TXFS
&TXFS_TEMPLATE_DEFAULT
} else if txfs.len() == 1 && txfs[0] == 0x00 {
&TXFS_TEMPLATE_ALL
} else {
txfs
};

let mut engine = sha256::Hash::engine();

//TODO(stevenroose) up for debate still
// engine.input(txfs).unwrap();
if txfs[0] & TXFS_CONTROL != 0 {
engine.input(txfs);
}

let mut bytes = txfs.iter().copied();
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_NB_INPUTS != 0 {
(tx.input.len() as u32).consensus_encode(&mut engine).unwrap();
}

if global & TXFS_NB_OUTPUTS != 0 {
(tx.output.len() as u32).consensus_encode(&mut engine).unwrap();
}

if global & TXFS_CURRENT_INPUT_IDX != 0 {
(current_input_idx as u32).consensus_encode(&mut engine).unwrap();
}
let cur = current_input_idx as usize;
if global & TXFS_CURRENT_INPUT_CONTROL_BLOCK != 0 {
if prevouts[cur].script_pubkey.is_v1_p2tr() {
if let Some(cb) = tx.input[cur].witness.taproot_control_block() {
encode::consensus_encode_with_size(cb, &mut engine).unwrap();
}
//TODO(stevenroose) should we hash something in case of no CB?
}
}
if global & TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS != 0 {
if let Some(pos) = current_input_last_codeseparator_pos {
(pos as u32).consensus_encode(&mut engine).unwrap();
}
//TODO(stevenroose) should we hash something in case of no codeseparator?
}

let inout_fields = bytes.next();
if global & TXFS_INPUTS != 0 {
let inout_fields = inout_fields.unwrap();
let range = bytes.next().unwrap();
let input_byte = bytes.next().unwrap();
let number = input_byte & TXFS_INOUT_NUMBER != 0;
let range = input_byte & (0xff ^ TXFS_INOUT_NUMBER);

let inputs: Vec<usize> = if range == TXFS_INOUT_RANGE_ALL {
let inputs: Vec<usize> = if range == TXFS_INOUT_RANGE_NONE {
vec![]
} else if range == TXFS_INOUT_RANGE_ALL {
(0..tx.input.len()).collect()
} else if range == TXFS_INOUT_RANGE_CURRENT {
vec![current_input_idx as usize]
Expand All @@ -457,7 +502,11 @@ pub fn calculate_txhash(
selected
};

if inout_fields & TXFS_INPUTS_PREVOUTS != 0 {
if number {
(tx.input.len() as u32).consensus_encode(&mut engine).unwrap();
}

if !inputs.is_empty() && inout_fields & TXFS_INPUTS_PREVOUTS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
Expand All @@ -468,7 +517,7 @@ pub fn calculate_txhash(
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_SEQUENCES != 0 {
if !inputs.is_empty() && inout_fields & TXFS_INPUTS_SEQUENCES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
Expand All @@ -479,7 +528,7 @@ pub fn calculate_txhash(
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_SCRIPTSIGS != 0 {
if !inputs.is_empty() && inout_fields & TXFS_INPUTS_SCRIPTSIGS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
Expand All @@ -490,7 +539,7 @@ pub fn calculate_txhash(
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_PREV_SCRIPTPUBKEYS != 0 {
if !inputs.is_empty() && inout_fields & TXFS_INPUTS_PREV_SCRIPTPUBKEYS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
Expand All @@ -501,7 +550,7 @@ pub fn calculate_txhash(
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_PREV_VALUES != 0 {
if !inputs.is_empty() && inout_fields & TXFS_INPUTS_PREV_VALUES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
Expand All @@ -512,7 +561,7 @@ pub fn calculate_txhash(
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_TAPROOT_ANNEXES != 0 {
if !inputs.is_empty() && inout_fields & TXFS_INPUTS_TAPROOT_ANNEXES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
Expand All @@ -532,9 +581,13 @@ pub fn calculate_txhash(

if global & TXFS_OUTPUTS != 0 {
let output_fields = inout_fields.unwrap();
let range = bytes.next().unwrap();
let output_byte = bytes.next().unwrap();
let number = output_byte & TXFS_INOUT_NUMBER != 0;
let range = output_byte & (0xff ^ TXFS_INOUT_NUMBER);

let outputs: Vec<usize> = if range == TXFS_INOUT_RANGE_ALL {
let outputs: Vec<usize> = if range == TXFS_INOUT_RANGE_NONE {
vec![]
} else if range == TXFS_INOUT_RANGE_ALL {
(0..tx.output.len()).collect()
} else if range == TXFS_INOUT_RANGE_CURRENT {
vec![current_input_idx as usize]
Expand All @@ -561,7 +614,11 @@ pub fn calculate_txhash(
selected
};

if output_fields & TXFS_OUTPUTS_SCRIPT_PUBKEYS != 0 {
if number {
(tx.output.len() as u32).consensus_encode(&mut engine).unwrap();
}

if !outputs.is_empty() && output_fields & TXFS_OUTPUTS_SCRIPT_PUBKEYS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &outputs {
Expand All @@ -572,7 +629,7 @@ pub fn calculate_txhash(
hash.consensus_encode(&mut engine).unwrap();
}

if output_fields & TXFS_OUTPUTS_VALUES != 0 {
if !outputs.is_empty() && output_fields & TXFS_OUTPUTS_VALUES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &outputs {
Expand Down

0 comments on commit ec98a85

Please sign in to comment.