Skip to content

Commit

Permalink
Add Non interactive siganture example
Browse files Browse the repository at this point in the history
Had to refactor some of the dummy_env stuff. If I try to include
change output in the default env, everything breaks and I did not want
to spend time trying to fix all of tests. Instead, we create a new
default with fee just for this one test case.
  • Loading branch information
sanket1729 committed Dec 23, 2024
1 parent 884b00e commit b7a0f5a
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 29 deletions.
73 changes: 73 additions & 0 deletions examples/non_interactive_fee_bump.simf
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* NON-INTERACTIVE FEE BUMPING
*
* This feature allows anyone, including miners, to increase a transaction's fee by reducing the change amount,
* following a predefined rule that adds 1 satoshi to the fee every second.
*
* Allowed modifications without affecting the signature:
* - Increase the transaction's nLockTime, delaying its inclusion in a block.
* - Decrease the change output or increase the fee output.
*
* This enables miners to maximize their fees from transactions without needing external fee bumping services like
* sponsors, Child-Pays-For-Parent (CPFP), or anchor outputs, simplifying fee management for transaction inclusion.
*/

// This function computes a signature hash for transactions that allows non-interactive fee bumping.
// It omits certain fields from the transaction that can be modified by anyone,
// specifically nLockTime and change/fee outputs amounts.
fn sighash_tx_nifb() -> u256 {
let ctx: Ctx8 = jet::sha_256_ctx_8_init();
let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, jet::version());
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::inputs_hash());
// Note that nlocktime is not signed.
// Add the hash of the first output (assumed the ONLY non-change output)
let ctx: Ctx8 = match jet::output_hash(0) {
Some(sighash : u256) => jet::sha_256_ctx_8_add_32(ctx, sighash),
None => panic!(),
};
// Add all output script pubkeys to the hash, including change and fee outputs script pubkeys
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::output_scripts_hash());
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::input_utxos_hash());
jet::sha_256_ctx_8_finalize(ctx)
}

// Combines the transaction hash with additional taproot-related data to form the overall transaction signature hash.
fn sighash_nifb() -> u256 {
let ctx: Ctx8 = jet::sha_256_ctx_8_init();
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::genesis_block_hash());
// Add the transaction-specific hash computed earlier
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, sighash_tx_nifb());
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::tap_env_hash());
let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, jet::current_index());
jet::sha_256_ctx_8_finalize(ctx)
}

// Helper function to ensure the provided boolean value is not negative.
fn check_neg(v : bool) {
assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(v)), 0));
}

// Enforces a linear increase in transaction fee over time by adjusting the maximum fee allowed before a transaction is mined.
fn total_fee_check() {
let curr_time : u32 = jet::tx_lock_time();
// [ELEMENTS]:Asset type for the transaction fee (explicitly specifying asset type, typically BTC asset)
let fee_asset : ExplicitAsset = 0x0000000000000000000000000000000000000000000000000000000000000000;
let fees : u64 = jet::total_fee(fee_asset);
let time_at_broadcast : u32 = 1734967235; // Dec 23 ~8:33am PST
let (carry, time_elapsed) : (bool, u32) = jet::subtract_32(curr_time, time_at_broadcast);
check_neg(carry); // Check for negative time difference, which shouldn't happen
let base_fee : u64 = 1000; // Base fee at the time of broadcast
// Calculate the maximum allowed fee as a function of elapsed time
let (carry, max_fee) : (bool, u64) = jet::add_64(base_fee, jet::left_pad_low_32_64(time_elapsed));
check_neg(carry); // Ensure there's no overflow in fee calculation
// Assert that the current fees are less than the maximum allowed fee
assert!(jet::lt_64(fees, max_fee));
// Optionally, you could limit the total fee here
}

fn main() {
let sighash : u256 = sighash_nifb();
total_fee_check();
let alice_pk : Pubkey = 0x9bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964;
jet::bip_0340_verify((alice_pk, sighash), witness::ALICE_SIGNATURE);
}
7 changes: 7 additions & 0 deletions examples/non_interactive_fee_bump.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ALICE_SIGNATURE": {
"value": "0xe6608ceb66f62896ca07661964dd2ab0867df94caaeb089ac09089d13c23cf64ee3a0d42f8f84c2f627d4230c9f357919c48a274117e38c9c3d32a0e87570b45",
"type": "Signature"
}
}

68 changes: 41 additions & 27 deletions src/dummy_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,70 @@ use std::sync::Arc;

use elements::{confidential, taproot::ControlBlock, AssetIssuance};
use hashes::Hash;
use simplicity::elements::{AssetId, TxOut};
use simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
use simplicity::Cmr;
use simplicity::{elements, hashes};

/// Return a dummy Elements environment.
pub fn dummy() -> ElementsEnv<Arc<elements::Transaction>> {
dummy_with(elements::LockTime::ZERO, elements::Sequence::MAX)
dummy_with(elements::LockTime::ZERO, elements::Sequence::MAX, false)
}

/// Return a dummy Elements environment with the given locktime and input sequence.
pub fn dummy_with(
lock_time: elements::LockTime,
sequence: elements::Sequence,
) -> ElementsEnv<Arc<elements::Transaction>> {
/// Returns a default transaction for the Elements network.
fn create_default_transaction(lock_time: elements::LockTime, sequence: elements::Sequence, include_fee_output: bool) -> elements::Transaction {
let mut tx = elements::Transaction {
version: 2,
lock_time,
input: vec![elements::TxIn {
previous_output: elements::OutPoint::default(),
is_pegin: false,
script_sig: elements::Script::new(),
sequence,
asset_issuance: AssetIssuance::default(),
witness: elements::TxInWitness::default(),
}],
output: vec![elements::TxOut {
asset: confidential::Asset::default(),
value: confidential::Value::default(),
nonce: confidential::Nonce::default(),
script_pubkey: elements::Script::default(),
witness: elements::TxOutWitness::default(),
}],
};

if include_fee_output {
tx.output.push(TxOut::new_fee(1_000, AssetId::default()));
}
tx
}

/// Returns a dummy Elements environment with a provided transaction.
pub fn dummy_with_tx(tx: elements::Transaction) -> ElementsEnv<Arc<elements::Transaction>> {
let ctrl_blk: [u8; 33] = [
0xc0, 0xeb, 0x04, 0xb6, 0x8e, 0x9a, 0x26, 0xd1, 0x16, 0x04, 0x6c, 0x76, 0xe8, 0xff, 0x47,
0x33, 0x2f, 0xb7, 0x1d, 0xda, 0x90, 0xff, 0x4b, 0xef, 0x53, 0x70, 0xf2, 0x52, 0x26, 0xd3,
0xbc, 0x09, 0xfc,
];
let num_inputs = tx.input.len();

ElementsEnv::new(
Arc::new(elements::Transaction {
version: 2,
lock_time,
// Enable locktime in dummy txin
input: vec![elements::TxIn {
previous_output: elements::OutPoint::default(),
is_pegin: false,
script_sig: elements::Script::new(),
sequence,
asset_issuance: AssetIssuance::default(),
witness: elements::TxInWitness::default(),
}],
output: vec![elements::TxOut {
asset: confidential::Asset::default(),
value: confidential::Value::default(),
nonce: confidential::Nonce::default(),
script_pubkey: elements::Script::default(),
witness: elements::TxOutWitness::default(),
}],
}),
Arc::new(tx),
vec![ElementsUtxo {
script_pubkey: elements::Script::default(),
asset: confidential::Asset::default(),
value: confidential::Value::default(),
}],
}; num_inputs],
0,
Cmr::from_byte_array([0; 32]),
ControlBlock::from_slice(&ctrl_blk).unwrap(),
None,
elements::BlockHash::all_zeros(),
)
}

/// Returns a dummy Elements environment with the given locktime and sequence.
pub fn dummy_with(lock_time: elements::LockTime, sequence: elements::Sequence, include_fee_output: bool) -> ElementsEnv<Arc<elements::Transaction>> {
let default_tx = create_default_transaction(lock_time, sequence, include_fee_output);
dummy_with_tx(default_tx)
}
20 changes: 18 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ pub trait ArbitraryOfType: Sized {
mod tests {
use base64::display::Base64Display;
use base64::engine::general_purpose::STANDARD;
use elements::LockTime;
use simplicity::BitMachine;
use std::borrow::Cow;
use std::path::Path;
Expand All @@ -257,6 +258,7 @@ mod tests {
program: T,
lock_time: elements::LockTime,
sequence: elements::Sequence,
include_fee_output: bool,
}

impl TestCase<TemplateProgram> {
Expand All @@ -274,6 +276,7 @@ mod tests {
program,
lock_time: elements::LockTime::ZERO,
sequence: elements::Sequence::MAX,
include_fee_output: false,
}
}

Expand All @@ -299,6 +302,7 @@ mod tests {
program,
lock_time: self.lock_time,
sequence: self.sequence,
include_fee_output: self.include_fee_output,
}
}
}
Expand Down Expand Up @@ -339,6 +343,7 @@ mod tests {
program,
lock_time: self.lock_time,
sequence: self.sequence,
include_fee_output: self.include_fee_output,
}
}
}
Expand All @@ -362,7 +367,7 @@ mod tests {

#[allow(dead_code)]
pub fn print_sighash_all(self) -> Self {
let env = dummy_env::dummy_with(self.lock_time, self.sequence);
let env = dummy_env::dummy_with(self.lock_time, self.sequence, self.include_fee_output);
dbg!(env.c_tx_env().sighash_all());
self
}
Expand All @@ -385,7 +390,7 @@ mod tests {

fn run(self) -> Result<(), simplicity::bit_machine::ExecutionError> {
let mut mac = BitMachine::for_program(self.program.redeem());
let env = dummy_env::dummy_with(self.lock_time, self.sequence);
let env = dummy_env::dummy_with(self.lock_time, self.sequence, self.include_fee_output);
mac.exec(self.program.redeem(), &env).map(|_| ())
}

Expand All @@ -411,6 +416,17 @@ mod tests {
.assert_run_success();
}

#[test]
#[cfg(feature = "serde")]
fn sighash_non_interactive_fee_bump() {
let mut t = TestCase::program_file("./examples/non_interactive_fee_bump.simf")
.with_witness_file("./examples/non_interactive_fee_bump.wit");
t.sequence = elements::Sequence::ENABLE_LOCKTIME_NO_RBF;
t.lock_time = LockTime::from_time(1734967235 + 600).unwrap();
t.include_fee_output = true;
t.assert_run_success();
}

#[test]
#[cfg(feature = "serde")]
fn escrow_with_delay_timeout() {
Expand Down

0 comments on commit b7a0f5a

Please sign in to comment.