Skip to content

Commit

Permalink
txhash: Add reference implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenroose committed Oct 6, 2023
1 parent 5b4092a commit 28fcd5c
Showing 1 changed file with 368 additions and 1 deletion.
369 changes: 368 additions & 1 deletion bip-txhash.mediawiki
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ The recommended standardness rules additionally:
If either budget requriement brings the budget below zero, the script fails
immediately.
An example implementation of a caching strategy can be found in this MR for
rust-bitcoin:
https://github.com/stevenroose/rust-bitcoin/blob/txhash/bitcoin/src/blockdata/script/txhash.rs


==Motivation==

Expand Down Expand Up @@ -218,7 +222,370 @@ some potential future upgrades:
==Detailed Specification==

<source lang="rust">
wip
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_ALL: u8 = TXFS_VERSION
| TXFS_LOCKTIME
| TXFS_NB_INPUTS
| TXFS_NB_OUTPUTS
| TXFS_CURRENT_INPUT_IDX
| TXFS_CURRENT_INPUT_CONTROL_BLOCK
| TXFS_INPUTS
| TXFS_OUTPUTS;

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_SCRIPT_PUBKEYS: 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_INPUTS_DEFAULT: u8 = TXFS_INPUTS_SEQUENCES
| TXFS_INPUTS_SCRIPTSIGS
| TXFS_INPUTS_PREV_VALUES
| 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 DEFAULT_TXFS: [u8; 4] = [
TXFS_ALL,
TXFS_INPUTS_DEFAULT | TXFS_OUTPUTS_ALL,
TXFS_INOUT_RANGE_ALL,
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 {
return Ok(())
} else if range & TXFS_INOUT_RANGE_MODE == 0 { // leading mode
if range & TXFS_INOUT_RANGE_SIZE == 0 {
let count = (range & TXFS_INOUT_RANGE_MASK) as usize;
if count > nb_items {
return Err("nb of leading in/outputs too high");
}
} else {
let next_byte = bytes.next().ok_or("extra range byte expected")?;
if range & TXFS_INOUT_RANGE_MASK == 0 {
return Err("non-minimal range");
}
let count = ((range & TXFS_INOUT_RANGE_MASK) as usize) << 8 + next_byte;
if count > nb_items {
return Err("nb of leading in/outputs too high");
}
}
} else { // individual mode
let count = (range & TXFS_INOUT_RANGE_MASK) as usize;
if count == 0 {
return Err("invalid 0 range count for individual mode");
}

if range & TXFS_INOUT_RANGE_SIZE == 0 {
let mut last = None;
for _ in 0..count {
let idx = bytes.next().ok_or("not enough indices")?;
if idx as usize > nb_items {
return Err("range index out of bounds");
}
if let Some(l) = last {
if idx <= l {
return Err("indices not in increasing order");
}
}
last = Some(idx);
}
} else {
let mut last = None;
for _ in 0..count {
let first = bytes.next().ok_or("not enough index bytes")? as usize;
let second = bytes.next().ok_or("not enough index bytes")? as usize;
let idx = first << 8 + second;
if idx > nb_items {
return Err("range index out of bounds");
}
if let Some(l) = last {
if idx <= l {
return Err("indices not in increasing order");
}
}
last = Some(idx);
}
}
}
Ok(())
}

pub fn validate_txfieldselector(
txfs: &[u8],
nb_inputs: usize,
nb_outputs: usize,
) -> Result<(), &'static str> {
if txfs.is_empty() {
return Ok(());
}

let mut bytes = txfs.iter().copied();
let global = bytes.next().unwrap();

if global & TXFS_INPUTS == 0 && global & TXFS_OUTPUTS == 0 {
if !bytes.next().is_none() {
return Err("input and output bit unset and more than one byte");
}
return Ok(());
}

let inout_fields = bytes.next().ok_or("in- or output bit set but only one byte")?;
if global & TXFS_INPUTS != 0 {
if inout_fields & TXFS_INPUTS_ALL == 0 {
return Err("inputs bit set but no input bits set");
}
validate_txfieldselector_inout_range(&mut bytes, nb_inputs)?
} else {
if inout_fields & TXFS_INPUTS_ALL != 0 {
return Err("inputs bit not set but some input bits set");
}
}

if global & TXFS_OUTPUTS != 0 {
if inout_fields & TXFS_OUTPUTS_ALL == 0 {
return Err("outputs bit set but no output bits set");
}
validate_txfieldselector_inout_range(&mut bytes, nb_outputs)?;
} else {
if inout_fields & TXFS_OUTPUTS_ALL != 0 {
return Err("outputs bit not set but some output bits set");
}
}

Ok(())
}

pub fn calculate_txhash(
txfs: &[u8],
tx: &Transaction,
prevouts: &[TxOut],
current_input_idx: 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
} else {
txfs
};

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

//TODO(stevenroose) up for debate still
// engine.input(txfs).unwrap();

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 inout_fields = bytes.next();
if global & TXFS_INPUTS != 0 {
let inout_fields = inout_fields.unwrap();
let range = bytes.next().unwrap();

let inputs: Vec<usize> = if range == TXFS_INOUT_RANGE_ALL {
(0..tx.input.len()).collect()
} else if range == TXFS_INOUT_RANGE_CURRENT {
vec![current_input_idx as usize]
} else if range & TXFS_INOUT_RANGE_MODE == 0 { // leading mode
let count = if range & TXFS_INOUT_RANGE_SIZE == 0 {
(range & TXFS_INOUT_RANGE_MASK) as usize
} else {
let next_byte = bytes.next().unwrap();
((range & TXFS_INOUT_RANGE_MASK) as usize) << 8 + next_byte as usize
};
(0..count).collect()
} else { // individual mode
let count = (range & TXFS_INOUT_RANGE_MASK) as usize;
let mut selected = Vec::with_capacity(count);
for _ in 0..count {
if range & TXFS_INOUT_RANGE_SIZE == 0 {
selected.push(bytes.next().unwrap() as usize);
} else {
let first = bytes.next().unwrap() as usize;
let second = bytes.next().unwrap() as usize;
selected.push(first << 8 + second);
}
}
selected
};

if inout_fields & TXFS_INPUTS_PREVOUTS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
tx.input[*i].previous_output.consensus_encode(&mut engine).unwrap();
}
sha256::Hash::from_engine(engine)
};
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_SEQUENCES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
tx.input[*i].sequence.consensus_encode(&mut engine).unwrap();
}
sha256::Hash::from_engine(engine)
};
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_SCRIPTSIGS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
engine.input(&sha256::Hash::hash(&tx.input[*i].script_sig.as_bytes())[..]);
}
sha256::Hash::from_engine(engine)
};
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_PREV_SCRIPTPUBKEYS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
engine.input(&sha256::Hash::hash(&prevouts[*i].script_pubkey.as_bytes())[..]);
}
sha256::Hash::from_engine(engine)
};
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_PREV_VALUES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
prevouts[*i].value.consensus_encode(&mut engine).unwrap();
}
sha256::Hash::from_engine(engine)
};
engine.input(&hash[..]);
}

if inout_fields & TXFS_INPUTS_TAPROOT_ANNEXES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &inputs {
if prevouts[*i].script_pubkey.is_v1_p2tr() {
if let Some(annex) = tx.input[*i].witness.taproot_annex() {
engine.input(&sha256::Hash::hash(annex)[..]);
} else {
engine.input(&SHA256_EMPTY[..]);
}
}
}
sha256::Hash::from_engine(engine)
};
engine.input(&hash[..]);
}
}

if global & TXFS_OUTPUTS != 0 {
let output_fields = inout_fields.unwrap();
let range = bytes.next().unwrap();

let outputs: Vec<usize> = if range == TXFS_INOUT_RANGE_ALL {
(0..tx.output.len()).collect()
} else if range == TXFS_INOUT_RANGE_CURRENT {
vec![current_input_idx as usize]
} else if range & TXFS_INOUT_RANGE_MODE == 0 { // leading mode
let count = if range & TXFS_INOUT_RANGE_SIZE == 0 {
(range & TXFS_INOUT_RANGE_MASK) as usize
} else {
let next_byte = bytes.next().unwrap();
((range & TXFS_INOUT_RANGE_MASK) as usize) << 8 + next_byte as usize
};
(0..count).collect()
} else { // individual mode
let count = (range & TXFS_INOUT_RANGE_MASK) as usize;
let mut selected = Vec::with_capacity(count);
for _ in 0..count {
if range & TXFS_INOUT_RANGE_SIZE == 0 {
selected.push(bytes.next().unwrap() as usize);
} else {
let first = bytes.next().unwrap() as usize;
let second = bytes.next().unwrap() as usize;
selected.push(first << 8 + second);
}
}
selected
};

if output_fields & TXFS_OUTPUTS_SCRIPT_PUBKEYS != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &outputs {
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_fields & TXFS_OUTPUTS_VALUES != 0 {
let hash = {
let mut engine = sha256::Hash::engine();
for i in &outputs {
tx.output[*i].value.consensus_encode(&mut engine).unwrap();
}
sha256::Hash::from_engine(engine)
};
hash.consensus_encode(&mut engine).unwrap();
}
}

sha256::Hash::from_engine(engine)
}
</source>


Expand Down

0 comments on commit 28fcd5c

Please sign in to comment.