Skip to content

Commit b7a0f5a

Browse files
committed
Add Non interactive siganture example
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.
1 parent 884b00e commit b7a0f5a

File tree

4 files changed

+139
-29
lines changed

4 files changed

+139
-29
lines changed
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* NON-INTERACTIVE FEE BUMPING
3+
*
4+
* This feature allows anyone, including miners, to increase a transaction's fee by reducing the change amount,
5+
* following a predefined rule that adds 1 satoshi to the fee every second.
6+
*
7+
* Allowed modifications without affecting the signature:
8+
* - Increase the transaction's nLockTime, delaying its inclusion in a block.
9+
* - Decrease the change output or increase the fee output.
10+
*
11+
* This enables miners to maximize their fees from transactions without needing external fee bumping services like
12+
* sponsors, Child-Pays-For-Parent (CPFP), or anchor outputs, simplifying fee management for transaction inclusion.
13+
*/
14+
15+
// This function computes a signature hash for transactions that allows non-interactive fee bumping.
16+
// It omits certain fields from the transaction that can be modified by anyone,
17+
// specifically nLockTime and change/fee outputs amounts.
18+
fn sighash_tx_nifb() -> u256 {
19+
let ctx: Ctx8 = jet::sha_256_ctx_8_init();
20+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, jet::version());
21+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::inputs_hash());
22+
// Note that nlocktime is not signed.
23+
// Add the hash of the first output (assumed the ONLY non-change output)
24+
let ctx: Ctx8 = match jet::output_hash(0) {
25+
Some(sighash : u256) => jet::sha_256_ctx_8_add_32(ctx, sighash),
26+
None => panic!(),
27+
};
28+
// Add all output script pubkeys to the hash, including change and fee outputs script pubkeys
29+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::output_scripts_hash());
30+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::input_utxos_hash());
31+
jet::sha_256_ctx_8_finalize(ctx)
32+
}
33+
34+
// Combines the transaction hash with additional taproot-related data to form the overall transaction signature hash.
35+
fn sighash_nifb() -> u256 {
36+
let ctx: Ctx8 = jet::sha_256_ctx_8_init();
37+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::genesis_block_hash());
38+
// Add the transaction-specific hash computed earlier
39+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, sighash_tx_nifb());
40+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::tap_env_hash());
41+
let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, jet::current_index());
42+
jet::sha_256_ctx_8_finalize(ctx)
43+
}
44+
45+
// Helper function to ensure the provided boolean value is not negative.
46+
fn check_neg(v : bool) {
47+
assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(v)), 0));
48+
}
49+
50+
// Enforces a linear increase in transaction fee over time by adjusting the maximum fee allowed before a transaction is mined.
51+
fn total_fee_check() {
52+
let curr_time : u32 = jet::tx_lock_time();
53+
// [ELEMENTS]:Asset type for the transaction fee (explicitly specifying asset type, typically BTC asset)
54+
let fee_asset : ExplicitAsset = 0x0000000000000000000000000000000000000000000000000000000000000000;
55+
let fees : u64 = jet::total_fee(fee_asset);
56+
let time_at_broadcast : u32 = 1734967235; // Dec 23 ~8:33am PST
57+
let (carry, time_elapsed) : (bool, u32) = jet::subtract_32(curr_time, time_at_broadcast);
58+
check_neg(carry); // Check for negative time difference, which shouldn't happen
59+
let base_fee : u64 = 1000; // Base fee at the time of broadcast
60+
// Calculate the maximum allowed fee as a function of elapsed time
61+
let (carry, max_fee) : (bool, u64) = jet::add_64(base_fee, jet::left_pad_low_32_64(time_elapsed));
62+
check_neg(carry); // Ensure there's no overflow in fee calculation
63+
// Assert that the current fees are less than the maximum allowed fee
64+
assert!(jet::lt_64(fees, max_fee));
65+
// Optionally, you could limit the total fee here
66+
}
67+
68+
fn main() {
69+
let sighash : u256 = sighash_nifb();
70+
total_fee_check();
71+
let alice_pk : Pubkey = 0x9bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964;
72+
jet::bip_0340_verify((alice_pk, sighash), witness::ALICE_SIGNATURE);
73+
}

examples/non_interactive_fee_bump.wit

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ALICE_SIGNATURE": {
3+
"value": "0xe6608ceb66f62896ca07661964dd2ab0867df94caaeb089ac09089d13c23cf64ee3a0d42f8f84c2f627d4230c9f357919c48a274117e38c9c3d32a0e87570b45",
4+
"type": "Signature"
5+
}
6+
}
7+

src/dummy_env.rs

+41-27
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,70 @@ use std::sync::Arc;
44

55
use elements::{confidential, taproot::ControlBlock, AssetIssuance};
66
use hashes::Hash;
7+
use simplicity::elements::{AssetId, TxOut};
78
use simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
89
use simplicity::Cmr;
910
use simplicity::{elements, hashes};
1011

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

16-
/// Return a dummy Elements environment with the given locktime and input sequence.
17-
pub fn dummy_with(
18-
lock_time: elements::LockTime,
19-
sequence: elements::Sequence,
20-
) -> ElementsEnv<Arc<elements::Transaction>> {
17+
/// Returns a default transaction for the Elements network.
18+
fn create_default_transaction(lock_time: elements::LockTime, sequence: elements::Sequence, include_fee_output: bool) -> elements::Transaction {
19+
let mut tx = elements::Transaction {
20+
version: 2,
21+
lock_time,
22+
input: vec![elements::TxIn {
23+
previous_output: elements::OutPoint::default(),
24+
is_pegin: false,
25+
script_sig: elements::Script::new(),
26+
sequence,
27+
asset_issuance: AssetIssuance::default(),
28+
witness: elements::TxInWitness::default(),
29+
}],
30+
output: vec![elements::TxOut {
31+
asset: confidential::Asset::default(),
32+
value: confidential::Value::default(),
33+
nonce: confidential::Nonce::default(),
34+
script_pubkey: elements::Script::default(),
35+
witness: elements::TxOutWitness::default(),
36+
}],
37+
};
38+
39+
if include_fee_output {
40+
tx.output.push(TxOut::new_fee(1_000, AssetId::default()));
41+
}
42+
tx
43+
}
44+
45+
/// Returns a dummy Elements environment with a provided transaction.
46+
pub fn dummy_with_tx(tx: elements::Transaction) -> ElementsEnv<Arc<elements::Transaction>> {
2147
let ctrl_blk: [u8; 33] = [
2248
0xc0, 0xeb, 0x04, 0xb6, 0x8e, 0x9a, 0x26, 0xd1, 0x16, 0x04, 0x6c, 0x76, 0xe8, 0xff, 0x47,
2349
0x33, 0x2f, 0xb7, 0x1d, 0xda, 0x90, 0xff, 0x4b, 0xef, 0x53, 0x70, 0xf2, 0x52, 0x26, 0xd3,
2450
0xbc, 0x09, 0xfc,
2551
];
52+
let num_inputs = tx.input.len();
2653

2754
ElementsEnv::new(
28-
Arc::new(elements::Transaction {
29-
version: 2,
30-
lock_time,
31-
// Enable locktime in dummy txin
32-
input: vec![elements::TxIn {
33-
previous_output: elements::OutPoint::default(),
34-
is_pegin: false,
35-
script_sig: elements::Script::new(),
36-
sequence,
37-
asset_issuance: AssetIssuance::default(),
38-
witness: elements::TxInWitness::default(),
39-
}],
40-
output: vec![elements::TxOut {
41-
asset: confidential::Asset::default(),
42-
value: confidential::Value::default(),
43-
nonce: confidential::Nonce::default(),
44-
script_pubkey: elements::Script::default(),
45-
witness: elements::TxOutWitness::default(),
46-
}],
47-
}),
55+
Arc::new(tx),
4856
vec![ElementsUtxo {
4957
script_pubkey: elements::Script::default(),
5058
asset: confidential::Asset::default(),
5159
value: confidential::Value::default(),
52-
}],
60+
}; num_inputs],
5361
0,
5462
Cmr::from_byte_array([0; 32]),
5563
ControlBlock::from_slice(&ctrl_blk).unwrap(),
5664
None,
5765
elements::BlockHash::all_zeros(),
5866
)
5967
}
68+
69+
/// Returns a dummy Elements environment with the given locktime and sequence.
70+
pub fn dummy_with(lock_time: elements::LockTime, sequence: elements::Sequence, include_fee_output: bool) -> ElementsEnv<Arc<elements::Transaction>> {
71+
let default_tx = create_default_transaction(lock_time, sequence, include_fee_output);
72+
dummy_with_tx(default_tx)
73+
}

src/lib.rs

+18-2
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ pub trait ArbitraryOfType: Sized {
247247
mod tests {
248248
use base64::display::Base64Display;
249249
use base64::engine::general_purpose::STANDARD;
250+
use elements::LockTime;
250251
use simplicity::BitMachine;
251252
use std::borrow::Cow;
252253
use std::path::Path;
@@ -257,6 +258,7 @@ mod tests {
257258
program: T,
258259
lock_time: elements::LockTime,
259260
sequence: elements::Sequence,
261+
include_fee_output: bool,
260262
}
261263

262264
impl TestCase<TemplateProgram> {
@@ -274,6 +276,7 @@ mod tests {
274276
program,
275277
lock_time: elements::LockTime::ZERO,
276278
sequence: elements::Sequence::MAX,
279+
include_fee_output: false,
277280
}
278281
}
279282

@@ -299,6 +302,7 @@ mod tests {
299302
program,
300303
lock_time: self.lock_time,
301304
sequence: self.sequence,
305+
include_fee_output: self.include_fee_output,
302306
}
303307
}
304308
}
@@ -339,6 +343,7 @@ mod tests {
339343
program,
340344
lock_time: self.lock_time,
341345
sequence: self.sequence,
346+
include_fee_output: self.include_fee_output,
342347
}
343348
}
344349
}
@@ -362,7 +367,7 @@ mod tests {
362367

363368
#[allow(dead_code)]
364369
pub fn print_sighash_all(self) -> Self {
365-
let env = dummy_env::dummy_with(self.lock_time, self.sequence);
370+
let env = dummy_env::dummy_with(self.lock_time, self.sequence, self.include_fee_output);
366371
dbg!(env.c_tx_env().sighash_all());
367372
self
368373
}
@@ -385,7 +390,7 @@ mod tests {
385390

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

@@ -411,6 +416,17 @@ mod tests {
411416
.assert_run_success();
412417
}
413418

419+
#[test]
420+
#[cfg(feature = "serde")]
421+
fn sighash_non_interactive_fee_bump() {
422+
let mut t = TestCase::program_file("./examples/non_interactive_fee_bump.simf")
423+
.with_witness_file("./examples/non_interactive_fee_bump.wit");
424+
t.sequence = elements::Sequence::ENABLE_LOCKTIME_NO_RBF;
425+
t.lock_time = LockTime::from_time(1734967235 + 600).unwrap();
426+
t.include_fee_output = true;
427+
t.assert_run_success();
428+
}
429+
414430
#[test]
415431
#[cfg(feature = "serde")]
416432
fn escrow_with_delay_timeout() {

0 commit comments

Comments
 (0)