Skip to content

Commit 1424855

Browse files
committed
Merge #109: Add Non interactive fee bumping example
ab42d67 Fix lint issues (Sanket Kanjalkar) f2ccc62 Add Non interactive siganture example (Sanket Kanjalkar) Pull request description: 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. ACKs for top commit: uncomputable: ACK ab42d67 Feel free to optimize the `check_neg` function. apoelstra: ACK ab42d67; successfully ran local tests Tree-SHA512: ec4ccf979336f42464c640c139b2e8229e5e5ae1cd2cff91af4f933702d1891961fdd3be221ebc4b8a051573d6f0f33b6412751ba2b59500ff517a01a7a24166
2 parents 884b00e + ab42d67 commit 1424855

11 files changed

+177
-54
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/array.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl<'a, A> BTreeSlice<'a, A> {
2020
}
2121
}
2222

23-
impl<'a, A: Clone> BTreeSlice<'a, A> {
23+
impl<A: Clone> BTreeSlice<'_, A> {
2424
/// Fold the tree in post order, using the binary function `f`.
2525
///
2626
/// Returns `None` if the tree is empty.
@@ -57,7 +57,7 @@ impl<'a, A: Clone> BTreeSlice<'a, A> {
5757
}
5858
}
5959

60-
impl<'a, A: Clone> TreeLike for BTreeSlice<'a, A> {
60+
impl<A: Clone> TreeLike for BTreeSlice<'_, A> {
6161
fn as_node(&self) -> Tree<Self> {
6262
match self.0.len() {
6363
0 | 1 => Tree::Nullary,
@@ -169,7 +169,7 @@ impl<'a, A> Partition<'a, A> {
169169
}
170170
}
171171

172-
impl<'a, A: Clone> Partition<'a, A> {
172+
impl<A: Clone> Partition<'_, A> {
173173
/// Check if the partition is complete.
174174
///
175175
/// A complete partition contains no empty blocks.
@@ -213,7 +213,7 @@ impl<'a, A: Clone> Partition<'a, A> {
213213
}
214214

215215
#[rustfmt::skip]
216-
impl<'a, A: Clone> TreeLike for Partition<'a, A> {
216+
impl<A: Clone> TreeLike for Partition<'_, A> {
217217
fn as_node(&self) -> Tree<Self> {
218218
match self {
219219
Self::Leaf {..} => Tree::Nullary,

src/ast.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ pub enum ExprTree<'a> {
426426
Match(&'a Match),
427427
}
428428

429-
impl<'a> TreeLike for ExprTree<'a> {
429+
impl TreeLike for ExprTree<'_> {
430430
fn as_node(&self) -> Tree<Self> {
431431
use SingleExpressionInner as S;
432432

src/dummy_env.rs

+52-27
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,81 @@ 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(
17+
/// Returns a default transaction for the Elements network.
18+
fn create_default_transaction(
1819
lock_time: elements::LockTime,
1920
sequence: elements::Sequence,
20-
) -> ElementsEnv<Arc<elements::Transaction>> {
21+
include_fee_output: bool,
22+
) -> elements::Transaction {
23+
let mut tx = elements::Transaction {
24+
version: 2,
25+
lock_time,
26+
input: vec![elements::TxIn {
27+
previous_output: elements::OutPoint::default(),
28+
is_pegin: false,
29+
script_sig: elements::Script::new(),
30+
sequence,
31+
asset_issuance: AssetIssuance::default(),
32+
witness: elements::TxInWitness::default(),
33+
}],
34+
output: vec![elements::TxOut {
35+
asset: confidential::Asset::default(),
36+
value: confidential::Value::default(),
37+
nonce: confidential::Nonce::default(),
38+
script_pubkey: elements::Script::default(),
39+
witness: elements::TxOutWitness::default(),
40+
}],
41+
};
42+
43+
if include_fee_output {
44+
tx.output.push(TxOut::new_fee(1_000, AssetId::default()));
45+
}
46+
tx
47+
}
48+
49+
/// Returns a dummy Elements environment with a provided transaction.
50+
pub fn dummy_with_tx(tx: elements::Transaction) -> ElementsEnv<Arc<elements::Transaction>> {
2151
let ctrl_blk: [u8; 33] = [
2252
0xc0, 0xeb, 0x04, 0xb6, 0x8e, 0x9a, 0x26, 0xd1, 0x16, 0x04, 0x6c, 0x76, 0xe8, 0xff, 0x47,
2353
0x33, 0x2f, 0xb7, 0x1d, 0xda, 0x90, 0xff, 0x4b, 0xef, 0x53, 0x70, 0xf2, 0x52, 0x26, 0xd3,
2454
0xbc, 0x09, 0xfc,
2555
];
56+
let num_inputs = tx.input.len();
2657

2758
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 {
59+
Arc::new(tx),
60+
vec![
61+
ElementsUtxo {
62+
script_pubkey: elements::Script::default(),
4163
asset: confidential::Asset::default(),
4264
value: confidential::Value::default(),
43-
nonce: confidential::Nonce::default(),
44-
script_pubkey: elements::Script::default(),
45-
witness: elements::TxOutWitness::default(),
46-
}],
47-
}),
48-
vec![ElementsUtxo {
49-
script_pubkey: elements::Script::default(),
50-
asset: confidential::Asset::default(),
51-
value: confidential::Value::default(),
52-
}],
65+
};
66+
num_inputs
67+
],
5368
0,
5469
Cmr::from_byte_array([0; 32]),
5570
ControlBlock::from_slice(&ctrl_blk).unwrap(),
5671
None,
5772
elements::BlockHash::all_zeros(),
5873
)
5974
}
75+
76+
/// Returns a dummy Elements environment with the given locktime and sequence.
77+
pub fn dummy_with(
78+
lock_time: elements::LockTime,
79+
sequence: elements::Sequence,
80+
include_fee_output: bool,
81+
) -> ElementsEnv<Arc<elements::Transaction>> {
82+
let default_tx = create_default_transaction(lock_time, sequence, include_fee_output);
83+
dummy_with_tx(default_tx)
84+
}

src/error.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ impl<'a> From<&'a pest::iterators::Pair<'_, Rule>> for Span {
144144
}
145145
}
146146

147-
impl<'a> From<&'a str> for Span {
147+
impl From<&str> for Span {
148148
fn from(s: &str) -> Self {
149149
let start = Position::new(1, 1);
150150
let end_line = std::cmp::max(1, s.lines().count());

src/lib.rs

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// Library for parsing and compiling simfony
1+
//! Library for parsing and compiling simfony
22
33
pub type ProgNode = Arc<named::ConstructNode>;
44

@@ -247,6 +247,8 @@ pub trait ArbitraryOfType: Sized {
247247
mod tests {
248248
use base64::display::Base64Display;
249249
use base64::engine::general_purpose::STANDARD;
250+
#[cfg(feature = "serde")]
251+
use elements::LockTime;
250252
use simplicity::BitMachine;
251253
use std::borrow::Cow;
252254
use std::path::Path;
@@ -257,6 +259,7 @@ mod tests {
257259
program: T,
258260
lock_time: elements::LockTime,
259261
sequence: elements::Sequence,
262+
include_fee_output: bool,
260263
}
261264

262265
impl TestCase<TemplateProgram> {
@@ -274,6 +277,7 @@ mod tests {
274277
program,
275278
lock_time: elements::LockTime::ZERO,
276279
sequence: elements::Sequence::MAX,
280+
include_fee_output: false,
277281
}
278282
}
279283

@@ -299,6 +303,7 @@ mod tests {
299303
program,
300304
lock_time: self.lock_time,
301305
sequence: self.sequence,
306+
include_fee_output: self.include_fee_output,
302307
}
303308
}
304309
}
@@ -339,6 +344,7 @@ mod tests {
339344
program,
340345
lock_time: self.lock_time,
341346
sequence: self.sequence,
347+
include_fee_output: self.include_fee_output,
342348
}
343349
}
344350
}
@@ -362,7 +368,7 @@ mod tests {
362368

363369
#[allow(dead_code)]
364370
pub fn print_sighash_all(self) -> Self {
365-
let env = dummy_env::dummy_with(self.lock_time, self.sequence);
371+
let env = dummy_env::dummy_with(self.lock_time, self.sequence, self.include_fee_output);
366372
dbg!(env.c_tx_env().sighash_all());
367373
self
368374
}
@@ -385,7 +391,7 @@ mod tests {
385391

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

@@ -411,6 +417,17 @@ mod tests {
411417
.assert_run_success();
412418
}
413419

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

src/parse.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ pub enum ExprTree<'a> {
544544
Match(&'a Match),
545545
}
546546

547-
impl<'a> TreeLike for ExprTree<'a> {
547+
impl TreeLike for ExprTree<'_> {
548548
fn as_node(&self) -> Tree<Self> {
549549
use SingleExpressionInner as S;
550550

@@ -596,7 +596,7 @@ impl<'a> TreeLike for ExprTree<'a> {
596596
}
597597
}
598598

599-
impl<'a> fmt::Display for ExprTree<'a> {
599+
impl fmt::Display for ExprTree<'_> {
600600
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601601
use SingleExpressionInner as S;
602602

@@ -1512,7 +1512,7 @@ impl PestParse for ModuleAssignment {
15121512
#[derive(Clone, Debug)]
15131513
struct PatternPair<'a>(pest::iterators::Pair<'a, Rule>);
15141514

1515-
impl<'a> TreeLike for PatternPair<'a> {
1515+
impl TreeLike for PatternPair<'_> {
15161516
fn as_node(&self) -> Tree<Self> {
15171517
let mut it = self.0.clone().into_inner();
15181518
match self.0.as_rule() {
@@ -1534,7 +1534,7 @@ impl<'a> TreeLike for PatternPair<'a> {
15341534
#[derive(Clone, Debug)]
15351535
struct TyPair<'a>(pest::iterators::Pair<'a, Rule>);
15361536

1537-
impl<'a> TreeLike for TyPair<'a> {
1537+
impl TreeLike for TyPair<'_> {
15381538
fn as_node(&self) -> Tree<Self> {
15391539
let mut it = self.0.clone().into_inner();
15401540
match self.0.as_rule() {

src/pattern.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl Pattern {
7272
}
7373
}
7474

75-
impl<'a> TreeLike for &'a Pattern {
75+
impl TreeLike for &Pattern {
7676
fn as_node(&self) -> Tree<Self> {
7777
match self {
7878
Pattern::Identifier(_) | Pattern::Ignore => Tree::Nullary,
@@ -170,7 +170,7 @@ pub enum BasePattern {
170170
Product(Arc<Self>, Arc<Self>),
171171
}
172172

173-
impl<'a> TreeLike for &'a BasePattern {
173+
impl TreeLike for &BasePattern {
174174
fn as_node(&self) -> Tree<Self> {
175175
match self {
176176
BasePattern::Ignore | BasePattern::Identifier(_) => Tree::Nullary,

0 commit comments

Comments
 (0)