log_trace!(logger, "Updating ChannelMonitor: channel force closed, should broadcast: {}", should_broadcast);
self.lockdown_from_offchain = true;
if *should_broadcast {
+ // There's no need to broadcast our commitment transaction if we've seen one
+ // confirmed (even with 1 confirmation) as it'll be rejected as
+ // duplicate/conflicting.
+ let detected_funding_spend = self.funding_spend_confirmed.is_some() ||
+ self.onchain_events_awaiting_threshold_conf.iter().find(|event| match event.event {
+ OnchainEvent::FundingSpendConfirmation { .. } => true,
+ _ => false,
+ }).is_some();
+ if detected_funding_spend {
+ continue;
+ }
self.broadcast_latest_holder_commitment_txn(broadcaster, logger);
// If the channel supports anchor outputs, we'll need to emit an external
// event to be consumed such that a child transaction is broadcast with a
connect_blocks(&nodes[0], TEST_FINAL_CLTV - 1); // Confirm blocks until the HTLC expires
let claim_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(claim_txn.len(), 5);
+ assert_eq!(claim_txn.len(), 3);
check_spends!(claim_txn[0], remote_txn[0]); // Immediate HTLC claim with preimage
- check_spends!(claim_txn[1], chan_1.3); // Alternative commitment tx
- check_spends!(claim_txn[2], claim_txn[1]); // HTLC spend in alternative commitment tx
-
- check_spends!(claim_txn[3], remote_txn[0]);
- check_spends!(claim_txn[4], remote_txn[0]);
+ check_spends!(claim_txn[1], remote_txn[0]);
+ check_spends!(claim_txn[2], remote_txn[0]);
let preimage_tx = &claim_txn[0];
- let (preimage_bump_tx, timeout_tx) = if claim_txn[3].input[0].previous_output == preimage_tx.input[0].previous_output {
- (&claim_txn[3], &claim_txn[4])
+ let (preimage_bump_tx, timeout_tx) = if claim_txn[1].input[0].previous_output == preimage_tx.input[0].previous_output {
+ (&claim_txn[1], &claim_txn[2])
} else {
- (&claim_txn[4], &claim_txn[3])
+ (&claim_txn[2], &claim_txn[1])
};
assert_eq!(preimage_tx.input.len(), 1);
assert_eq!(node_txn.len(), 1);
mine_transaction(&nodes[0], &node_txn[0]);
check_added_monitors!(nodes[0], 1);
- test_txn_broadcast(&nodes[0], &chan_1, None, HTLCType::NONE);
+ test_txn_broadcast(&nodes[0], &chan_1, Some(node_txn[0].clone()), HTLCType::NONE);
}
check_closed_broadcast!(nodes[0], true);
assert_eq!(nodes[0].node.list_channels().len(), 0);
test_txn_broadcast(&nodes[1], &chan_2, None, HTLCType::TIMEOUT);
mine_transaction(&nodes[2], &node_txn[0]);
check_added_monitors!(nodes[2], 1);
- test_txn_broadcast(&nodes[2], &chan_2, None, HTLCType::NONE);
+ test_txn_broadcast(&nodes[2], &chan_2, Some(node_txn[0].clone()), HTLCType::NONE);
}
check_closed_broadcast!(nodes[2], true);
assert_eq!(nodes[1].node.list_channels().len(), 0);
mine_transaction(&nodes[1], &revoked_local_txn[0]);
{
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 2); // ChannelMonitor: penalty tx, ChannelManager: local commitment tx
+ assert_eq!(node_txn.len(), 1); // ChannelMonitor: penalty tx
assert_eq!(node_txn[0].input.len(), 2); // We should claim the revoked output and the HTLC output
check_spends!(node_txn[0], revoked_local_txn[0]);
node_txn.swap_remove(0);
- node_txn.truncate(1);
}
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
- test_txn_broadcast(&nodes[1], &chan_5, None, HTLCType::NONE);
+ test_txn_broadcast(&nodes[1], &chan_5, Some(revoked_local_txn[0].clone()), HTLCType::NONE);
mine_transaction(&nodes[0], &revoked_local_txn[0]);
connect_blocks(&nodes[0], TEST_FINAL_CLTV - 1); // Confirm blocks until the HTLC expires
mine_transaction(&nodes[0], &revoked_local_txn[0]);
{
let mut node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 2); //ChannelMonitor: penalty tx, ChannelManager: local commitment tx
+ assert_eq!(node_txn.len(), 1); // ChannelMonitor: penalty tx
assert_eq!(node_txn[0].input.len(), 1); // We claim the received HTLC output
check_spends!(node_txn[0], revoked_local_txn[0]);
node_txn.swap_remove(0);
}
check_added_monitors!(nodes[0], 1);
- test_txn_broadcast(&nodes[0], &chan_6, None, HTLCType::NONE);
+ test_txn_broadcast(&nodes[0], &chan_6, Some(revoked_local_txn[0].clone()), HTLCType::NONE);
mine_transaction(&nodes[1], &revoked_local_txn[0]);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(node_txn.len(), 2); // ChannelMonitor: justice tx against revoked to_local output, ChannelManager: local commitment tx
+ assert_eq!(node_txn.len(), 1); // ChannelMonitor: justice tx against revoked to_local output
check_spends!(node_txn[0], revoked_local_txn[0]);
- check_spends!(node_txn[1], chan_1.3);
// Inform nodes[0] that a watchtower cheated on its behalf, so it will force-close the chan
mine_transaction(&nodes[0], &revoked_local_txn[0]);
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(node_txn.len(), 2); // ChannelMonitor: penalty tx, ChannelManager: local commitment
+ assert_eq!(node_txn.len(), 1); // ChannelMonitor: penalty tx
assert_eq!(node_txn[0].input.len(), 3); // Claim the revoked output + both revoked HTLC outputs
check_spends!(node_txn[0], revoked_local_txn[0]);
assert_eq!(*witness_lens.iter().skip(1).next().unwrap(), OFFERED_HTLC_SCRIPT_WEIGHT); // revoked offered HTLC
assert_eq!(*witness_lens.iter().skip(2).next().unwrap(), ACCEPTED_HTLC_SCRIPT_WEIGHT); // revoked received HTLC
- // Next nodes[1] broadcasts its current local tx state:
- assert_eq!(node_txn[1].input.len(), 1);
- check_spends!(node_txn[1], chan_1.3);
-
// Finally, mine the penalty transaction and check that we get an HTLC failure after
// ANTI_REORG_DELAY confirmations.
mine_transaction(&nodes[1], &node_txn[0]);
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert!(node_txn.len() == 9 || node_txn.len() == 10);
+ assert_eq!(node_txn.len(), 7);
// Check the pair local commitment and HTLC-timeout broadcast due to HTLC expiration
assert_eq!(node_txn[0].input.len(), 1);
assert_eq!(witness_script.len(), OFFERED_HTLC_SCRIPT_WEIGHT); //Spending an offered htlc output
check_spends!(node_txn[1], node_txn[0]);
- // Justice transactions are indices 1-2-4
+ // Justice transactions are indices 2-3-4
assert_eq!(node_txn[2].input.len(), 1);
assert_eq!(node_txn[3].input.len(), 1);
assert_eq!(node_txn[4].input.len(), 1);
check_closed_broadcast!(nodes[2], true);
check_added_monitors!(nodes[2], 1);
check_closed_event!(nodes[2], 1, ClosureReason::CommitmentTxConfirmed);
- let node_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelManager : 3 (commitment tx, 2*htlc-success tx), ChannelMonitor : 2 (2 * HTLC-Success tx)
- assert_eq!(node_txn.len(), 5);
- assert_eq!(node_txn[0], node_txn[3]);
- assert_eq!(node_txn[1], node_txn[4]);
- assert_eq!(node_txn[2], commitment_tx[0]);
+ let node_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelMonitor: 2 (2 * HTLC-Success tx)
+ assert_eq!(node_txn.len(), 2);
check_spends!(node_txn[0], commitment_tx[0]);
check_spends!(node_txn[1], commitment_tx[0]);
assert_eq!(node_txn[0].input[0].witness.clone().last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
// Verify that B's ChannelManager is able to extract preimage from HTLC Success tx and pass it backward
let header = BlockHeader { version: 0x20000000, prev_blockhash: nodes[1].best_block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42};
- connect_block(&nodes[1], &Block { header, txdata: node_txn});
+ connect_block(&nodes[1], &Block { header, txdata: vec![commitment_tx[0].clone(), node_txn[0].clone(), node_txn[1].clone()]});
connect_blocks(&nodes[1], TEST_FINAL_CLTV - 1); // Confirm blocks until the HTLC expires
{
let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap();
_ => panic!("Unexpected event"),
};
macro_rules! check_tx_local_broadcast {
- ($node: expr, $htlc_offered: expr, $commitment_tx: expr, $chan_tx: expr) => { {
+ ($node: expr, $htlc_offered: expr, $commitment_tx: expr) => { {
let mut node_txn = $node.tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 3);
- // Node[1]: ChannelManager: 3 (commitment tx, 2*HTLC-Timeout tx), ChannelMonitor: 2 (timeout tx)
- // Node[0]: ChannelManager: 3 (commtiemtn tx, 2*HTLC-Timeout tx), ChannelMonitor: 2 HTLC-timeout
+ assert_eq!(node_txn.len(), 2);
+ // Node[1]: 2 * HTLC-timeout tx
+ // Node[0]: 2 * HTLC-timeout tx
+ check_spends!(node_txn[0], $commitment_tx);
check_spends!(node_txn[1], $commitment_tx);
- check_spends!(node_txn[2], $commitment_tx);
+ assert_ne!(node_txn[0].lock_time.0, 0);
assert_ne!(node_txn[1].lock_time.0, 0);
- assert_ne!(node_txn[2].lock_time.0, 0);
if $htlc_offered {
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- assert_eq!(node_txn[2].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
+ assert!(node_txn[0].output[0].script_pubkey.is_v0_p2wsh()); // revokeable output
assert!(node_txn[1].output[0].script_pubkey.is_v0_p2wsh()); // revokeable output
- assert!(node_txn[2].output[0].script_pubkey.is_v0_p2wsh()); // revokeable output
} else {
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
- assert_eq!(node_txn[2].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
+ assert!(node_txn[0].output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
assert!(node_txn[1].output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
- assert!(node_txn[2].output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
}
- check_spends!(node_txn[0], $chan_tx);
- assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), 71);
node_txn.clear();
} }
}
- // nodes[1] now broadcasts its own local state as a fallback, suggesting an alternate
- // commitment transaction with a corresponding HTLC-Timeout transactions, as well as a
- // timeout-claim of the output that nodes[2] just claimed via success.
- check_tx_local_broadcast!(nodes[1], false, commitment_tx[0], chan_2.3);
+ // nodes[1] now broadcasts its own timeout-claim of the output that nodes[2] just claimed via success.
+ check_tx_local_broadcast!(nodes[1], false, commitment_tx[0]);
// Broadcast legit commitment tx from A on B's chain
// Broadcast preimage tx by B on offered output from A commitment tx on A's chain
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert!(node_txn.len() == 4 || node_txn.len() == 6); // ChannelManager : 3 (commitment tx + HTLC-Sucess * 2), ChannelMonitor : 3 (HTLC-Success, 2* RBF bumps of above HTLC txn)
+ assert!(node_txn.len() == 1 || node_txn.len() == 3); // HTLC-Success, 2* RBF bumps of above HTLC txn
let commitment_spend =
- if node_txn[0].input[0].previous_output.txid == node_a_commitment_tx[0].txid() {
- if node_txn.len() == 6 {
- // In some block `ConnectionStyle`s we may avoid broadcasting the double-spending
- // transactions spending the HTLC outputs of C's commitment transaction. Otherwise,
- // check that the extra broadcasts (double-)spend those here.
+ if node_txn.len() == 1 {
+ &node_txn[0]
+ } else {
+ // Certain `ConnectStyle`s will cause RBF bumps of the previous HTLC transaction to be broadcast.
+ // FullBlockViaListen
+ if node_txn[0].input[0].previous_output.txid == node_a_commitment_tx[0].txid() {
check_spends!(node_txn[1], commitment_tx[0]);
check_spends!(node_txn[2], commitment_tx[0]);
assert_ne!(node_txn[1].input[0].previous_output.vout, node_txn[2].input[0].previous_output.vout);
+ &node_txn[0]
+ } else {
+ check_spends!(node_txn[0], commitment_tx[0]);
+ check_spends!(node_txn[1], commitment_tx[0]);
+ assert_ne!(node_txn[0].input[0].previous_output.vout, node_txn[1].input[0].previous_output.vout);
+ &node_txn[2]
}
- &node_txn[0]
- } else {
- check_spends!(node_txn[0], commitment_tx[0]);
- check_spends!(node_txn[1], commitment_tx[0]);
- assert_ne!(node_txn[0].input[0].previous_output.vout, node_txn[1].input[0].previous_output.vout);
- &node_txn[2]
};
check_spends!(commitment_spend, node_a_commitment_tx[0]);
assert_eq!(commitment_spend.input[1].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
assert_eq!(commitment_spend.lock_time.0, 0);
assert!(commitment_spend.output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
- let funding_spend_offset = if node_txn.len() == 6 { 3 } else { 1 };
- check_spends!(node_txn[funding_spend_offset], chan_1.3);
- assert_eq!(node_txn[funding_spend_offset].input[0].witness.clone().last().unwrap().len(), 71);
- check_spends!(node_txn[funding_spend_offset + 1], node_txn[funding_spend_offset]);
- check_spends!(node_txn[funding_spend_offset + 2], node_txn[funding_spend_offset]);
// We don't bother to check that B can claim the HTLC output on its commitment tx here as
// we already checked the same situation with A.
_ => panic!("Unexpected event"),
}
}
- check_tx_local_broadcast!(nodes[0], true, node_a_commitment_tx[0], chan_1.3);
+ check_tx_local_broadcast!(nodes[0], true, node_a_commitment_tx[0]);
}
fn do_test_htlc_on_chain_timeout(connect_style: ConnectStyle) {
check_closed_broadcast!(nodes[2], true);
check_added_monitors!(nodes[2], 1);
check_closed_event!(nodes[2], 1, ClosureReason::CommitmentTxConfirmed);
- let node_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelManager : 1 (commitment tx)
- assert_eq!(node_txn.len(), 1);
- check_spends!(node_txn[0], chan_2.3);
- assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), 71);
+ let node_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
+ assert_eq!(node_txn.len(), 0);
// Broadcast timeout transaction by B on received output from C's commitment tx on B's chain
// Verify that B's ChannelManager is able to detect that HTLC is timeout by its own tx and react backward in consequence
let timeout_tx;
{
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 5); // ChannelManager : 2 (commitment tx, HTLC-Timeout tx), ChannelMonitor : 2 (local commitment tx + HTLC-timeout), 1 timeout tx
- assert_eq!(node_txn[0], node_txn[3]);
- assert_eq!(node_txn[1], node_txn[4]);
+ assert_eq!(node_txn.len(), 3); // 2 (local commitment tx + HTLC-timeout), 1 timeout tx
check_spends!(node_txn[2], commitment_tx[0]);
assert_eq!(node_txn[2].clone().input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
check_closed_broadcast!(nodes[0], true);
check_added_monitors!(nodes[0], 1);
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed);
- let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelManager : 1 commitment tx, ChannelMonitor : 1 timeout tx
- assert_eq!(node_txn.len(), 2);
- check_spends!(node_txn[0], chan_1.3);
- assert_eq!(node_txn[0].clone().input[0].witness.last().unwrap().len(), 71);
- check_spends!(node_txn[1], commitment_tx[0]);
- assert_eq!(node_txn[1].clone().input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
+ let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // 1 timeout tx
+ assert_eq!(node_txn.len(), 1);
+ check_spends!(node_txn[0], commitment_tx[0]);
+ assert_eq!(node_txn[0].clone().input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
}
#[test]
}
// Check B's monitor was able to send back output descriptor event for preimage tx on A's commitment tx
- let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelManager : 2 (local commitment tx + HTLC-Success), ChannelMonitor: preimage tx
- assert_eq!(node_txn.len(), 3);
+ let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelMonitor: preimage tx
+ assert_eq!(node_txn.len(), 1);
check_spends!(node_txn[0], commitment_tx[0]);
assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- check_spends!(node_txn[1], chan_1.3);
- check_spends!(node_txn[2], node_txn[1]);
mine_transaction(&nodes[1], &node_txn[0]);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
// Check B's monitor was able to send back output descriptor event for timeout tx on A's commitment tx
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(node_txn.len(), 2); // ChannelManager : 1 local commitent tx, ChannelMonitor: timeout tx
- check_spends!(node_txn[0], chan_1.3.clone());
- check_spends!(node_txn[1], commitment_tx[0].clone());
- assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
+ assert_eq!(node_txn.len(), 1); // ChannelMonitor: timeout tx
+ check_spends!(node_txn[0], commitment_tx[0].clone());
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
- mine_transaction(&nodes[1], &node_txn[1]);
+ mine_transaction(&nodes[1], &node_txn[0]);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
expect_payment_failed!(nodes[1], our_payment_hash, false);
let spend_txn = check_spendable_outputs!(nodes[1], node_cfgs[1].keys_manager);
assert_eq!(spend_txn.len(), 3); // SpendableOutput: remote_commitment_tx.to_remote, timeout_tx.output
check_spends!(spend_txn[0], commitment_tx[0]);
- check_spends!(spend_txn[1], node_txn[1]);
- check_spends!(spend_txn[2], node_txn[1], commitment_tx[0]); // All outputs
+ check_spends!(spend_txn[1], node_txn[0]);
+ check_spends!(spend_txn[2], node_txn[0], commitment_tx[0]); // All outputs
}
#[test]
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(node_txn.len(), 2);
+ assert_eq!(node_txn.len(), 1);
assert_eq!(node_txn[0].input.len(), 2);
check_spends!(node_txn[0], revoked_local_txn[0]);
connect_blocks(&nodes[0], TEST_FINAL_CLTV - 1); // Confirm blocks until the HTLC expires
let revoked_htlc_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(revoked_htlc_txn.len(), 2);
- check_spends!(revoked_htlc_txn[0], chan_1.3);
- assert_eq!(revoked_htlc_txn[1].input.len(), 1);
- assert_eq!(revoked_htlc_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- check_spends!(revoked_htlc_txn[1], revoked_local_txn[0]);
- assert_ne!(revoked_htlc_txn[1].lock_time.0, 0); // HTLC-Timeout
+ assert_eq!(revoked_htlc_txn.len(), 1);
+ assert_eq!(revoked_htlc_txn[0].input.len(), 1);
+ assert_eq!(revoked_htlc_txn[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
+ check_spends!(revoked_htlc_txn[0], revoked_local_txn[0]);
+ assert_ne!(revoked_htlc_txn[0].lock_time.0, 0); // HTLC-Timeout
// B will generate justice tx from A's revoked commitment/HTLC tx
let header = BlockHeader { version: 0x20000000, prev_blockhash: nodes[1].best_block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42 };
- connect_block(&nodes[1], &Block { header, txdata: vec![revoked_local_txn[0].clone(), revoked_htlc_txn[1].clone()] });
+ connect_block(&nodes[1], &Block { header, txdata: vec![revoked_local_txn[0].clone(), revoked_htlc_txn[0].clone()] });
check_closed_broadcast!(nodes[1], true);
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(node_txn.len(), 3); // ChannelMonitor: bogus justice tx, justice tx on revoked outputs, ChannelManager: local commitment tx
+ assert_eq!(node_txn.len(), 2); // ChannelMonitor: bogus justice tx, justice tx on revoked outputs
// The first transaction generated is bogus - it spends both outputs of revoked_local_txn[0]
// including the one already spent by revoked_htlc_txn[1]. That's OK, we'll spend with valid
// transactions next...
assert_eq!(node_txn[0].input.len(), 3);
- check_spends!(node_txn[0], revoked_local_txn[0], revoked_htlc_txn[1]);
+ check_spends!(node_txn[0], revoked_local_txn[0], revoked_htlc_txn[0]);
assert_eq!(node_txn[1].input.len(), 2);
- check_spends!(node_txn[1], revoked_local_txn[0], revoked_htlc_txn[1]);
- if node_txn[1].input[1].previous_output.txid == revoked_htlc_txn[1].txid() {
- assert_ne!(node_txn[1].input[0].previous_output, revoked_htlc_txn[1].input[0].previous_output);
+ check_spends!(node_txn[1], revoked_local_txn[0], revoked_htlc_txn[0]);
+ if node_txn[1].input[1].previous_output.txid == revoked_htlc_txn[0].txid() {
+ assert_ne!(node_txn[1].input[0].previous_output, revoked_htlc_txn[0].input[0].previous_output);
} else {
- assert_eq!(node_txn[1].input[0].previous_output.txid, revoked_htlc_txn[1].txid());
- assert_ne!(node_txn[1].input[1].previous_output, revoked_htlc_txn[1].input[0].previous_output);
+ assert_eq!(node_txn[1].input[0].previous_output.txid, revoked_htlc_txn[0].txid());
+ assert_ne!(node_txn[1].input[1].previous_output, revoked_htlc_txn[0].input[0].previous_output);
}
- assert_eq!(node_txn[2].input.len(), 1);
- check_spends!(node_txn[2], chan_1.3);
-
mine_transaction(&nodes[1], &node_txn[1]);
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let revoked_htlc_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(revoked_htlc_txn.len(), 2);
+ assert_eq!(revoked_htlc_txn.len(), 1);
assert_eq!(revoked_htlc_txn[0].input.len(), 1);
assert_eq!(revoked_htlc_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
check_spends!(revoked_htlc_txn[0], revoked_local_txn[0]);
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed);
let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(node_txn.len(), 3); // ChannelMonitor: justice tx on revoked commitment, justice tx on revoked HTLC-success, ChannelManager: local commitment tx
+ assert_eq!(node_txn.len(), 2); // ChannelMonitor: justice tx on revoked commitment, justice tx on revoked HTLC-success
// The first transaction generated is bogus - it spends both outputs of revoked_local_txn[0]
// including the one already spent by revoked_htlc_txn[0]. That's OK, we'll spend with valid
assert_eq!(node_txn[1].input.len(), 1);
check_spends!(node_txn[1], revoked_htlc_txn[0]);
- check_spends!(node_txn[2], chan_1.3);
-
mine_transaction(&nodes[0], &node_txn[1]);
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
check_added_monitors!(nodes[2], 1);
check_closed_event!(nodes[2], 1, ClosureReason::CommitmentTxConfirmed);
- let c_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelManager : 2 (commitment tx, HTLC-Success tx), ChannelMonitor : 1 (HTLC-Success tx)
- assert_eq!(c_txn.len(), 3);
- assert_eq!(c_txn[0], c_txn[2]);
- assert_eq!(commitment_tx[0], c_txn[1]);
- check_spends!(c_txn[1], chan_2.3);
- check_spends!(c_txn[2], c_txn[1]);
- assert_eq!(c_txn[1].input[0].witness.clone().last().unwrap().len(), 71);
- assert_eq!(c_txn[2].input[0].witness.clone().last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
+ let c_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone(); // ChannelMonitor: 1 (HTLC-Success tx)
+ assert_eq!(c_txn.len(), 1);
+ check_spends!(c_txn[0], commitment_tx[0]);
+ assert_eq!(c_txn[0].input[0].witness.clone().last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
assert!(c_txn[0].output[0].script_pubkey.is_v0_p2wsh()); // revokeable output
assert_eq!(c_txn[0].lock_time.0, 0); // Success tx
// So we broadcast C's commitment tx and HTLC-Success on B's chain, we should successfully be able to extract preimage and update downstream monitor
let header = BlockHeader { version: 0x20000000, prev_blockhash: nodes[1].best_block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42};
- connect_block(&nodes[1], &Block { header, txdata: vec![c_txn[1].clone(), c_txn[2].clone()]});
+ connect_block(&nodes[1], &Block { header, txdata: vec![commitment_tx[0].clone(), c_txn[0].clone()]});
check_added_monitors!(nodes[1], 1);
let events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2);
},
_ => panic!("Unexpected event"),
}
- {
- let mut b_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- // ChannelMonitor: claim tx
- assert_eq!(b_txn.len(), 1);
- check_spends!(b_txn[0], chan_2.3); // B local commitment tx, issued by ChannelManager
- b_txn.clear();
- }
check_added_monitors!(nodes[1], 1);
let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
assert_eq!(msg_events.len(), 3);
mine_transaction(&nodes[1], &commitment_tx[0]);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let b_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- // ChannelMonitor: HTLC-Success tx, ChannelManager: local commitment tx + HTLC-Success tx
- assert_eq!(b_txn.len(), 3);
- check_spends!(b_txn[1], chan_1.3);
- check_spends!(b_txn[2], b_txn[1]);
+ // ChannelMonitor: HTLC-Success tx
+ assert_eq!(b_txn.len(), 1);
check_spends!(b_txn[0], commitment_tx[0]);
assert_eq!(b_txn[0].input[0].witness.clone().last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
assert!(b_txn[0].output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
let htlc_timeout_tx;
{ // Extract one of the two HTLC-Timeout transaction
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- // ChannelMonitor: timeout tx * 2-or-3, ChannelManager: local commitment tx
- assert!(node_txn.len() == 4 || node_txn.len() == 3);
- check_spends!(node_txn[0], chan_2.3);
+ // ChannelMonitor: timeout tx * 2-or-3
+ assert!(node_txn.len() == 2 || node_txn.len() == 3);
- check_spends!(node_txn[1], commitment_txn[0]);
- assert_eq!(node_txn[1].input.len(), 1);
+ check_spends!(node_txn[0], commitment_txn[0]);
+ assert_eq!(node_txn[0].input.len(), 1);
- if node_txn.len() > 3 {
- check_spends!(node_txn[2], commitment_txn[0]);
- assert_eq!(node_txn[2].input.len(), 1);
- assert_eq!(node_txn[1].input[0].previous_output, node_txn[2].input[0].previous_output);
+ if node_txn.len() > 2 {
+ check_spends!(node_txn[1], commitment_txn[0]);
+ assert_eq!(node_txn[1].input.len(), 1);
+ assert_eq!(node_txn[0].input[0].previous_output, node_txn[1].input[0].previous_output);
- check_spends!(node_txn[3], commitment_txn[0]);
- assert_ne!(node_txn[1].input[0].previous_output, node_txn[3].input[0].previous_output);
- } else {
check_spends!(node_txn[2], commitment_txn[0]);
- assert_ne!(node_txn[1].input[0].previous_output, node_txn[2].input[0].previous_output);
+ assert_ne!(node_txn[0].input[0].previous_output, node_txn[2].input[0].previous_output);
+ } else {
+ check_spends!(node_txn[1], commitment_txn[0]);
+ assert_ne!(node_txn[0].input[0].previous_output, node_txn[1].input[0].previous_output);
}
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
- assert_eq!(node_txn[2].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
- if node_txn.len() > 3 {
- assert_eq!(node_txn[3].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
+ if node_txn.len() > 2 {
+ assert_eq!(node_txn[2].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
}
- htlc_timeout_tx = node_txn[1].clone();
+ htlc_timeout_tx = node_txn[0].clone();
}
nodes[2].node.claim_funds(our_payment_preimage);
_ => panic!("Unexepected event"),
}
let htlc_success_txn: Vec<_> = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(htlc_success_txn.len(), 5); // ChannelMonitor: HTLC-Success txn (*2 due to 2-HTLC outputs), ChannelManager: local commitment tx + HTLC-Success txn (*2 due to 2-HTLC outputs)
+ assert_eq!(htlc_success_txn.len(), 2); // ChannelMonitor: HTLC-Success txn (*2 due to 2-HTLC outputs)
check_spends!(htlc_success_txn[0], commitment_txn[0]);
check_spends!(htlc_success_txn[1], commitment_txn[0]);
assert_eq!(htlc_success_txn[0].input.len(), 1);
assert_eq!(htlc_success_txn[1].input.len(), 1);
assert_eq!(htlc_success_txn[1].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
assert_ne!(htlc_success_txn[0].input[0].previous_output, htlc_success_txn[1].input[0].previous_output);
- assert_eq!(htlc_success_txn[2], commitment_txn[0]);
- assert_eq!(htlc_success_txn[3], htlc_success_txn[0]);
- assert_eq!(htlc_success_txn[4], htlc_success_txn[1]);
assert_ne!(htlc_success_txn[1].input[0].previous_output, htlc_timeout_tx.input[0].previous_output);
mine_transaction(&nodes[1], &htlc_timeout_tx);
}
let node_tx = {
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 3);
- assert_eq!(node_txn[0], node_txn[2]);
- assert_eq!(node_txn[1], local_txn[0]);
+ assert_eq!(node_txn.len(), 1);
assert_eq!(node_txn[0].input.len(), 1);
assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
check_spends!(node_txn[0], local_txn[0]);
let htlc_timeout = {
let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 2);
- check_spends!(node_txn[0], chan_1.3);
- assert_eq!(node_txn[1].input.len(), 1);
- assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- check_spends!(node_txn[1], local_txn[0]);
- node_txn[1].clone()
+ assert_eq!(node_txn.len(), 1);
+ assert_eq!(node_txn[0].input.len(), 1);
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
+ check_spends!(node_txn[0], local_txn[0]);
+ node_txn[0].clone()
};
mine_transaction(&nodes[0], &htlc_timeout);
let htlc_timeout = {
let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn[1].input.len(), 1);
- assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- check_spends!(node_txn[1], local_txn_1[0]);
- node_txn[1].clone()
+ assert_eq!(node_txn.len(), 1);
+ assert_eq!(node_txn[0].input.len(), 1);
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
+ check_spends!(node_txn[0], local_txn_1[0]);
+ node_txn[0].clone()
};
mine_transaction(&nodes[0], &htlc_timeout);
check_closed_broadcast!(nodes[0], true);
check_added_monitors!(nodes[0], 1);
assert_eq!(nodes[0].node.get_and_clear_pending_events().len(), 0);
- timeout_tx.push(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[1].clone());
+ timeout_tx.push(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0].clone());
assert_eq!(timeout_tx[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
// We fail non-dust-HTLC 2 by broadcast of local HTLC-timeout tx on local commitment tx
assert_eq!(nodes[0].node.get_and_clear_pending_events().len(), 0);
let feerate_1;
{
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 2); // justice tx (broadcasted from ChannelMonitor) + local commitment tx
+ assert_eq!(node_txn.len(), 1); // justice tx (broadcasted from ChannelMonitor)
assert_eq!(node_txn[0].input.len(), 3); // Penalty txn claims to_local, offered_htlc and received_htlc outputs
assert_eq!(node_txn[0].output.len(), 1);
check_spends!(node_txn[0], revoked_txn[0]);
connect_blocks(&nodes[1], 49); // Confirm blocks until the HTLC expires (note CLTV was explicitly 50 above)
let revoked_htlc_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(revoked_htlc_txn.len(), 3);
- check_spends!(revoked_htlc_txn[1], chan.3);
+ assert_eq!(revoked_htlc_txn.len(), 2);
assert_eq!(revoked_htlc_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
assert_eq!(revoked_htlc_txn[0].input.len(), 1);
check_spends!(revoked_htlc_txn[0], revoked_local_txn[0]);
- assert_eq!(revoked_htlc_txn[2].input.len(), 1);
- assert_eq!(revoked_htlc_txn[2].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- assert_eq!(revoked_htlc_txn[2].output.len(), 1);
- check_spends!(revoked_htlc_txn[2], revoked_local_txn[0]);
+ assert_eq!(revoked_htlc_txn[1].input.len(), 1);
+ assert_eq!(revoked_htlc_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
+ assert_eq!(revoked_htlc_txn[1].output.len(), 1);
+ check_spends!(revoked_htlc_txn[1], revoked_local_txn[0]);
// Broadcast set of revoked txn on A
let hash_128 = connect_blocks(&nodes[0], 40);
let header_11 = BlockHeader { version: 0x20000000, prev_blockhash: hash_128, merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42 };
connect_block(&nodes[0], &Block { header: header_11, txdata: vec![revoked_local_txn[0].clone()] });
let header_129 = BlockHeader { version: 0x20000000, prev_blockhash: header_11.block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42 };
- connect_block(&nodes[0], &Block { header: header_129, txdata: vec![revoked_htlc_txn[0].clone(), revoked_htlc_txn[2].clone()] });
+ connect_block(&nodes[0], &Block { header: header_129, txdata: vec![revoked_htlc_txn[0].clone(), revoked_htlc_txn[1].clone()] });
let events = nodes[0].node.get_and_clear_pending_events();
expect_pending_htlcs_forwardable_from_events!(nodes[0], events[0..1], true);
match events.last().unwrap() {
let penalty_txn;
{
let mut node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 5); // 3 penalty txn on revoked commitment tx + A commitment tx + 1 penalty tnx on revoked HTLC txn
+ assert_eq!(node_txn.len(), 4); // 3 penalty txn on revoked commitment tx + 1 penalty tnx on revoked HTLC txn
// Verify claim tx are spending revoked HTLC txn
// node_txn 0-2 each spend a separate revoked output from revoked_local_txn[0]
assert_ne!(node_txn[1].input[0].previous_output, node_txn[2].input[0].previous_output);
assert_eq!(node_txn[0].input[0].previous_output, revoked_htlc_txn[0].input[0].previous_output);
- assert_eq!(node_txn[1].input[0].previous_output, revoked_htlc_txn[2].input[0].previous_output);
+ assert_eq!(node_txn[1].input[0].previous_output, revoked_htlc_txn[1].input[0].previous_output);
- // node_txn[3] is the local commitment tx broadcast just because (and somewhat in case of
- // reorgs, though its not clear its ever worth broadcasting conflicting txn like this when
- // a remote commitment tx has already been confirmed).
- check_spends!(node_txn[3], chan.3);
-
- // node_txn[4] spends the revoked outputs from the revoked_htlc_txn (which only have one
+ // node_txn[3] spends the revoked outputs from the revoked_htlc_txn (which only have one
// output, checked above).
- assert_eq!(node_txn[4].input.len(), 2);
- assert_eq!(node_txn[4].output.len(), 1);
- check_spends!(node_txn[4], revoked_htlc_txn[0], revoked_htlc_txn[2]);
+ assert_eq!(node_txn[3].input.len(), 2);
+ assert_eq!(node_txn[3].output.len(), 1);
+ check_spends!(node_txn[3], revoked_htlc_txn[0], revoked_htlc_txn[1]);
- first = node_txn[4].txid();
+ first = node_txn[3].txid();
// Store both feerates for later comparison
- let fee_1 = revoked_htlc_txn[0].output[0].value + revoked_htlc_txn[2].output[0].value - node_txn[4].output[0].value;
- feerate_1 = fee_1 * 1000 / node_txn[4].weight() as u64;
+ let fee_1 = revoked_htlc_txn[0].output[0].value + revoked_htlc_txn[1].output[0].value - node_txn[3].output[0].value;
+ feerate_1 = fee_1 * 1000 / node_txn[3].weight() as u64;
penalty_txn = vec![node_txn[2].clone()];
node_txn.clear();
}
assert_eq!(node_txn.len(), 1);
assert_eq!(node_txn[0].input.len(), 2);
- check_spends!(node_txn[0], revoked_htlc_txn[0], revoked_htlc_txn[2]);
+ check_spends!(node_txn[0], revoked_htlc_txn[0], revoked_htlc_txn[1]);
// Verify bumped tx is different and 25% bump heuristic
assert_ne!(first, node_txn[0].txid());
- let fee_2 = revoked_htlc_txn[0].output[0].value + revoked_htlc_txn[2].output[0].value - node_txn[0].output[0].value;
+ let fee_2 = revoked_htlc_txn[0].output[0].value + revoked_htlc_txn[1].output[0].value - node_txn[0].output[0].value;
let feerate_2 = fee_2 * 1000 / node_txn[0].weight() as u64;
assert!(feerate_2 * 100 > feerate_1 * 125);
let txn = vec![node_txn[0].clone()];
let feerate_preimage;
{
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
- // 5 transactions including:
- // local commitment + HTLC-Success
+ // 3 transactions including:
// preimage and timeout sweeps from remote commitment + preimage sweep bump
- assert_eq!(node_txn.len(), 5);
+ assert_eq!(node_txn.len(), 3);
assert_eq!(node_txn[0].input.len(), 1);
- assert_eq!(node_txn[3].input.len(), 1);
- assert_eq!(node_txn[4].input.len(), 1);
+ assert_eq!(node_txn[1].input.len(), 1);
+ assert_eq!(node_txn[2].input.len(), 1);
check_spends!(node_txn[0], remote_txn[0]);
- check_spends!(node_txn[3], remote_txn[0]);
- check_spends!(node_txn[4], remote_txn[0]);
-
- check_spends!(node_txn[1], chan.3); // local commitment
- check_spends!(node_txn[2], node_txn[1]); // local HTLC-Success
+ check_spends!(node_txn[1], remote_txn[0]);
+ check_spends!(node_txn[2], remote_txn[0]);
preimage = node_txn[0].txid();
let index = node_txn[0].input[0].previous_output.vout;
let fee = remote_txn[0].output[index as usize].value - node_txn[0].output[0].value;
feerate_preimage = fee * 1000 / node_txn[0].weight() as u64;
- let (preimage_bump_tx, timeout_tx) = if node_txn[3].input[0].previous_output == node_txn[0].input[0].previous_output {
- (node_txn[3].clone(), node_txn[4].clone())
+ let (preimage_bump_tx, timeout_tx) = if node_txn[2].input[0].previous_output == node_txn[0].input[0].previous_output {
+ (node_txn[2].clone(), node_txn[1].clone())
} else {
- (node_txn[4].clone(), node_txn[3].clone())
+ (node_txn[1].clone(), node_txn[2].clone())
};
preimage_bump = preimage_bump_tx;
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed);
let penalty_txn = {
let mut node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn.len(), 4); //ChannelMonitor: justice txn * 3, ChannelManager: local commitment tx
+ assert_eq!(node_txn.len(), 3); //ChannelMonitor: justice txn * 3
check_spends!(node_txn[0], revoked_local_txn[0]);
check_spends!(node_txn[1], revoked_local_txn[0]);
check_spends!(node_txn[2], revoked_local_txn[0]);
let htlc_timeout = {
let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- assert_eq!(node_txn[1].input.len(), 1);
- assert_eq!(node_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
- check_spends!(node_txn[1], local_txn[0]);
- node_txn[1].clone()
+ assert_eq!(node_txn.len(), 1);
+ assert_eq!(node_txn[0].input.len(), 1);
+ assert_eq!(node_txn[0].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
+ check_spends!(node_txn[0], local_txn[0]);
+ node_txn[0].clone()
};
let header_201 = BlockHeader { version: 0x20000000, prev_blockhash: nodes[0].best_block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42 };
};
let header = BlockHeader { version: 0x20000000, prev_blockhash: nodes[1].best_block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: 42, bits: 42, nonce: 42};
connect_block(&nodes[1], &Block { header, txdata: vec![txn_to_broadcast[0].clone()]});
- let mut bob_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
if broadcast_alice {
check_closed_broadcast!(nodes[1], true);
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
}
- assert_eq!(bob_txn.len(), 1);
- check_spends!(bob_txn[0], chan_ab.3);
}
// Step (5):
}
let mut bob_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
if broadcast_alice {
- // In `connect_block()`, the ChainMonitor and ChannelManager are separately notified about a
- // new block being connected. The ChannelManager being notified triggers a monitor update,
- // which triggers broadcasting our commitment tx and an HTLC-claiming tx. The ChainMonitor
- // being notified triggers the HTLC-claiming tx redundantly, resulting in 3 total txs being
- // broadcasted.
- assert_eq!(bob_txn.len(), 3);
- check_spends!(bob_txn[1], chan_ab.3);
+ assert_eq!(bob_txn.len(), 1);
+ check_spends!(bob_txn[0], txn_to_broadcast[0]);
} else {
assert_eq!(bob_txn.len(), 2);
check_spends!(bob_txn[0], chan_ab.3);
// Finally, check that Bob broadcasted a preimage-claiming transaction for the HTLC output on the
// broadcasted commitment transaction.
{
- let bob_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- if go_onchain_before_fulfill {
- // Bob should now have an extra broadcasted tx, for the preimage-claiming transaction.
- assert_eq!(bob_txn.len(), 2);
- }
let script_weight = match broadcast_alice {
true => OFFERED_HTLC_SCRIPT_WEIGHT,
false => ACCEPTED_HTLC_SCRIPT_WEIGHT
};
- // If Alice force-closed and Bob didn't receive her commitment transaction until after he
- // received Carol's fulfill, he broadcasts the HTLC-output-claiming transaction first. Else if
- // Bob force closed or if he found out about Alice's commitment tx before receiving Carol's
- // fulfill, then he broadcasts the HTLC-output-claiming transaction second.
- if broadcast_alice && !go_onchain_before_fulfill {
+ // If Alice force-closed, Bob only broadcasts a HTLC-output-claiming transaction. Otherwise,
+ // Bob force-closed and broadcasts the commitment transaction along with a
+ // HTLC-output-claiming transaction.
+ let bob_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
+ if broadcast_alice {
+ assert_eq!(bob_txn.len(), 1);
check_spends!(bob_txn[0], txn_to_broadcast[0]);
assert_eq!(bob_txn[0].input[0].witness.last().unwrap().len(), script_weight);
} else {
+ assert_eq!(bob_txn.len(), 2);
check_spends!(bob_txn[1], txn_to_broadcast[0]);
assert_eq!(bob_txn[1].input[0].witness.last().unwrap().len(), script_weight);
}
check_closed_broadcast!(nodes[1], true);
let bs_spend_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(bs_spend_txn.len(), 2);
+ assert_eq!(bs_spend_txn.len(), 1);
check_spends!(bs_spend_txn[0], revoked_local_txn[0]);
- check_spends!(bs_spend_txn[1], chan.3);
// After the commitment transaction confirms, we should still wait on the HTLC spend
// transaction to confirm before resolving the HTLC.
mine_transaction(&nodes[1], &remote_txn[0]);
let b_broadcast_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(b_broadcast_txn.len(), if prev_commitment_tx { 4 } else { 5 });
- if prev_commitment_tx {
- check_spends!(b_broadcast_txn[3], b_broadcast_txn[2]);
- } else {
- assert_eq!(b_broadcast_txn[0], b_broadcast_txn[3]);
- assert_eq!(b_broadcast_txn[1], b_broadcast_txn[4]);
- }
- // b_broadcast_txn[0] should spend the HTLC output of the commitment tx for 3_000 sats
+ assert_eq!(b_broadcast_txn.len(), 2);
+ // b_broadcast_txn should spend the HTLCs output of the commitment tx for 3_000 and 4_000 sats
check_spends!(b_broadcast_txn[0], remote_txn[0]);
check_spends!(b_broadcast_txn[1], remote_txn[0]);
assert_eq!(b_broadcast_txn[0].input.len(), 1);
assert_eq!(b_broadcast_txn[1].input.len(), 1);
assert_eq!(remote_txn[0].output[b_broadcast_txn[0].input[0].previous_output.vout as usize].value, 3_000);
assert_eq!(remote_txn[0].output[b_broadcast_txn[1].input[0].previous_output.vout as usize].value, 4_000);
- check_spends!(b_broadcast_txn[2], funding_tx);
assert!(nodes[0].node.list_channels().is_empty());
check_closed_broadcast!(nodes[0], true);
// When the HTLC timeout output is spendable in the next block, A should broadcast it
connect_blocks(&nodes[0], htlc_cltv_timeout - nodes[0].best_block_info().1 - 1);
let a_broadcast_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(a_broadcast_txn.len(), 3);
- check_spends!(a_broadcast_txn[0], funding_tx);
+ assert_eq!(a_broadcast_txn.len(), 2);
+ assert_eq!(a_broadcast_txn[0].input.len(), 1);
+ check_spends!(a_broadcast_txn[0], remote_txn[0]);
assert_eq!(a_broadcast_txn[1].input.len(), 1);
check_spends!(a_broadcast_txn[1], remote_txn[0]);
- assert_eq!(a_broadcast_txn[2].input.len(), 1);
- check_spends!(a_broadcast_txn[2], remote_txn[0]);
- assert_ne!(a_broadcast_txn[1].input[0].previous_output.vout,
- a_broadcast_txn[2].input[0].previous_output.vout);
- // a_broadcast_txn [1] and [2] should spend the HTLC outputs of the commitment tx
- assert_eq!(remote_txn[0].output[a_broadcast_txn[1].input[0].previous_output.vout as usize].value, 3_000);
- assert_eq!(remote_txn[0].output[a_broadcast_txn[2].input[0].previous_output.vout as usize].value, 4_000);
+ assert_ne!(a_broadcast_txn[0].input[0].previous_output.vout,
+ a_broadcast_txn[1].input[0].previous_output.vout);
+ // a_broadcast_txn [0] and [1] should spend the HTLC outputs of the commitment tx
+ assert_eq!(remote_txn[0].output[a_broadcast_txn[0].input[0].previous_output.vout as usize].value, 3_000);
+ assert_eq!(remote_txn[0].output[a_broadcast_txn[1].input[0].previous_output.vout as usize].value, 4_000);
// Once the HTLC-Timeout transaction confirms, A will no longer consider the HTLC
// "MaybeClaimable", but instead move it to "AwaitingConfirmations".
- mine_transaction(&nodes[0], &a_broadcast_txn[2]);
+ mine_transaction(&nodes[0], &a_broadcast_txn[1]);
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
assert_eq!(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 4_000,
nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances());
expect_payment_failed!(nodes[0], timeout_payment_hash, false);
- test_spendable_output(&nodes[0], &a_broadcast_txn[2]);
+ test_spendable_output(&nodes[0], &a_broadcast_txn[1]);
// Node B will no longer consider the HTLC "contentious" after the HTLC claim transaction
// confirms, and consider it simply "awaiting confirmations". Note that it has to wait for the
// Finally, mine the HTLC timeout transaction that A broadcasted (even though B should be able
// to claim this HTLC with the preimage it knows!). It will remain listed as a claimable HTLC
// until ANTI_REORG_DELAY confirmations on the spend.
- mine_transaction(&nodes[1], &a_broadcast_txn[2]);
+ mine_transaction(&nodes[1], &a_broadcast_txn[1]);
assert_eq!(vec![Balance::ContentiousClaimable {
claimable_amount_satoshis: 4_000,
timeout_height: htlc_cltv_timeout,
check_closed_broadcast!(nodes[1], true);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let bs_htlc_claim_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(bs_htlc_claim_txn.len(), 3);
+ assert_eq!(bs_htlc_claim_txn.len(), 1);
check_spends!(bs_htlc_claim_txn[0], as_txn[0]);
- check_spends!(bs_htlc_claim_txn[1], funding_tx);
- check_spends!(bs_htlc_claim_txn[2], bs_htlc_claim_txn[1]);
// Connect blocks until the HTLCs expire, allowing us to (validly) broadcast the HTLC-Timeout
// transaction.
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let revoked_htlc_success_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(revoked_htlc_success_txn.len(), 2);
+ assert_eq!(revoked_htlc_success_txn.len(), 1);
assert_eq!(revoked_htlc_success_txn[0].input.len(), 1);
assert_eq!(revoked_htlc_success_txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
check_spends!(revoked_htlc_success_txn[0], revoked_local_txn[0]);
- check_spends!(revoked_htlc_success_txn[1], funding_tx);
connect_blocks(&nodes[1], TEST_FINAL_CLTV);
let revoked_htlc_timeout_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
let to_remote_conf_height = nodes[0].best_block_info().1 + ANTI_REORG_DELAY - 1;
let as_commitment_claim_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(as_commitment_claim_txn.len(), 2);
+ assert_eq!(as_commitment_claim_txn.len(), 1);
check_spends!(as_commitment_claim_txn[0], revoked_local_txn[0]);
- check_spends!(as_commitment_claim_txn[1], funding_tx);
// The next two checks have the same balance set for A - even though we confirm a revoked HTLC
// transaction our balance tracking doesn't use the on-chain value so the
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let claim_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(claim_txn.len(), 3);
+ assert_eq!(claim_txn.len(), 1);
check_spends!(claim_txn[0], node_txn[1]);
- check_spends!(claim_txn[1], funding_tx);
- check_spends!(claim_txn[2], claim_txn[1]);
header.prev_blockhash = nodes[0].best_block_hash();
connect_block(&nodes[0], &Block { header, txdata: vec![node_txn[1].clone()]});
check_closed_broadcast!(nodes[2], true); // We should get a BroadcastChannelUpdate (and *only* a BroadcstChannelUpdate)
check_closed_event!(nodes[2], 1, ClosureReason::CommitmentTxConfirmed);
let node_2_commitment_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(node_2_commitment_txn.len(), 3); // ChannelMonitor: 1 offered HTLC-Claim, ChannelManger: 1 local commitment tx, 1 Received HTLC-Claim
- assert_eq!(node_2_commitment_txn[1].output.len(), 2); // to-remote and Received HTLC (to-self is dust)
- check_spends!(node_2_commitment_txn[1], chan_2.3);
- check_spends!(node_2_commitment_txn[2], node_2_commitment_txn[1]);
+ assert_eq!(node_2_commitment_txn.len(), 1); // ChannelMonitor: 1 offered HTLC-Claim
check_spends!(node_2_commitment_txn[0], node_1_commitment_txn[0]);
// Make sure node 1's height is the same as the !local_commitment case
mine_transaction(&nodes[1], &node_2_commitment_txn[0]);
connect_blocks(&nodes[1], TEST_FINAL_CLTV - 1); // Confirm blocks until the HTLC expires
let node_1_commitment_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- assert_eq!(node_1_commitment_txn.len(), 2); // ChannelMonitor: 1 offered HTLC-Timeout, ChannelManger: 1 local commitment tx
- assert_eq!(node_1_commitment_txn[0].output.len(), 2); // to-local and Offered HTLC (to-remote is dust)
- check_spends!(node_1_commitment_txn[0], chan_2.3);
- check_spends!(node_1_commitment_txn[1], node_2_commitment_txn[0]);
+ assert_eq!(node_1_commitment_txn.len(), 1); // ChannelMonitor: 1 offered HTLC-Timeout
+ check_spends!(node_1_commitment_txn[0], node_2_commitment_txn[0]);
// Confirm node 1's HTLC-Timeout on node 1
- mine_transaction(&nodes[1], &node_1_commitment_txn[1]);
+ mine_transaction(&nodes[1], &node_1_commitment_txn[0]);
// ...but return node 2's commitment tx (and claim) in case claim is set and we're preparing to reorg
vec![node_2_commitment_txn.pop().unwrap()]
};
// Verify node A broadcast tx claiming both HTLCs
{
let mut node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
- // ChannelMonitor: claim tx, ChannelManager: local commitment tx + HTLC-Success*2
- assert_eq!(node_txn.len(), 4);
+ // ChannelMonitor: claim tx
+ assert_eq!(node_txn.len(), 1);
check_spends!(node_txn[0], remote_txn[0]);
- check_spends!(node_txn[1], chan.3);
- check_spends!(node_txn[2], node_txn[1]);
- check_spends!(node_txn[3], node_txn[1]);
assert_eq!(node_txn[0].input.len(), 2);
node_txn.clear();
}
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
- // Drop transactions broadcasted in response to the first commitment transaction (we have good
- // test coverage of these things already elsewhere).
- assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0).len(), 1);
- assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0).len(), 1);
-
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
assert!(nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
//! Data structures and encoding for `invoice_request` messages.
//!
-//! An [`InvoiceRequest`] can be either built from a parsed [`Offer`] as an "offer to be paid" or
-//! built directly as an "offer for money" (e.g., refund, ATM withdrawal). In the former case, it is
+//! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is
//! typically constructed by a customer and sent to the merchant who had published the corresponding
-//! offer. In the latter case, an offer doesn't exist as a precursor to the request. Rather the
-//! merchant would typically construct the invoice request and present it to the customer.
+//! offer. The recipient of the request responds with an `Invoice`.
//!
-//! The recipient of the request responds with an `Invoice`.
+//! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a
+//! precursor, see [`Refund`].
+//!
+//! [`Refund`]: crate::offers::refund::Refund
//!
//! ```ignore
//! extern crate bitcoin;
//! let pubkey = PublicKey::from(keys);
//! let mut buffer = Vec::new();
//!
-//! // "offer to be paid" flow
//! "lno1qcp4256ypq"
//! .parse::<Offer>()?
//! .request_invoice(vec![42; 64], pubkey)?
self.contents.amount_msats
}
- /// Features for paying the invoice.
+ /// Features pertaining to requesting an invoice.
pub fn features(&self) -> &InvoiceRequestFeatures {
&self.contents.features
}
#[cfg(test)]
mod tests {
- use super::InvoiceRequest;
+ use super::{InvoiceRequest, InvoiceRequestTlvStreamRef};
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
use core::time::Duration;
use crate::ln::features::InvoiceRequestFeatures;
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
- use crate::offers::merkle::SignError;
- use crate::offers::offer::{Amount, OfferBuilder, Quantity};
+ use crate::offers::merkle::{SignError, SignatureTlvStreamRef};
+ use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
use crate::offers::parse::{ParseError, SemanticError};
+ use crate::offers::payer::PayerTlvStreamRef;
use crate::util::ser::{BigSize, Writeable};
use crate::util::string::PrintableString;
#[test]
fn builds_invoice_request_with_defaults() {
- let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
+ let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
- .build().unwrap();
- let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
- .build().unwrap().sign(payer_sign).unwrap();
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
- let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream) =
- invoice_request.as_tlv_stream();
let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();
assert_eq!(invoice_request.payer_note(), None);
assert!(invoice_request.signature().is_some());
- assert_eq!(payer_tlv_stream.metadata, Some(&vec![1; 32]));
- assert_eq!(offer_tlv_stream.chains, None);
- assert_eq!(offer_tlv_stream.metadata, None);
- assert_eq!(offer_tlv_stream.currency, None);
- assert_eq!(offer_tlv_stream.amount, Some(1000));
- assert_eq!(offer_tlv_stream.description, Some(&String::from("foo")));
- assert_eq!(offer_tlv_stream.features, None);
- assert_eq!(offer_tlv_stream.absolute_expiry, None);
- assert_eq!(offer_tlv_stream.paths, None);
- assert_eq!(offer_tlv_stream.issuer, None);
- assert_eq!(offer_tlv_stream.quantity_max, None);
- assert_eq!(offer_tlv_stream.node_id, Some(&recipient_pubkey()));
- assert_eq!(invoice_request_tlv_stream.chain, None);
- assert_eq!(invoice_request_tlv_stream.amount, None);
- assert_eq!(invoice_request_tlv_stream.features, None);
- assert_eq!(invoice_request_tlv_stream.quantity, None);
- assert_eq!(invoice_request_tlv_stream.payer_id, Some(&payer_pubkey()));
- assert_eq!(invoice_request_tlv_stream.payer_note, None);
- assert!(signature_tlv_stream.signature.is_some());
+ assert_eq!(
+ invoice_request.as_tlv_stream(),
+ (
+ PayerTlvStreamRef { metadata: Some(&vec![1; 32]) },
+ OfferTlvStreamRef {
+ chains: None,
+ metadata: None,
+ currency: None,
+ amount: Some(1000),
+ description: Some(&String::from("foo")),
+ features: None,
+ absolute_expiry: None,
+ paths: None,
+ issuer: None,
+ quantity_max: None,
+ node_id: Some(&recipient_pubkey()),
+ },
+ InvoiceRequestTlvStreamRef {
+ chain: None,
+ amount: None,
+ features: None,
+ quantity: None,
+ payer_id: Some(&payer_pubkey()),
+ payer_note: None,
+ },
+ SignatureTlvStreamRef { signature: invoice_request.signature().as_ref() },
+ ),
+ );
if let Err(e) = InvoiceRequest::try_from(buffer) {
panic!("error parsing invoice request: {:?}", e);
pub mod offer;
pub mod parse;
mod payer;
+pub mod refund;
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
- supported_quantity: Quantity::one(), signing_pubkey: Some(signing_pubkey),
+ supported_quantity: Quantity::one(), signing_pubkey,
};
OfferBuilder { offer }
}
issuer: Option<String>,
paths: Option<Vec<BlindedPath>>,
supported_quantity: Quantity,
- signing_pubkey: Option<PublicKey>,
+ signing_pubkey: PublicKey,
}
impl Offer {
/// The public key used by the recipient to sign invoices.
pub fn signing_pubkey(&self) -> PublicKey {
- self.contents.signing_pubkey.unwrap()
+ self.contents.signing_pubkey
}
/// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
paths: self.paths.as_ref(),
issuer: self.issuer.as_ref(),
quantity_max: self.supported_quantity.to_tlv_record(),
- node_id: self.signing_pubkey.as_ref(),
+ node_id: Some(&self.signing_pubkey),
}
}
}
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};
- if node_id.is_none() {
- return Err(SemanticError::MissingSigningPubkey);
- }
+ let signing_pubkey = match node_id {
+ None => return Err(SemanticError::MissingSigningPubkey),
+ Some(node_id) => node_id,
+ };
Ok(OfferContents {
chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
- supported_quantity, signing_pubkey: node_id,
+ supported_quantity, signing_pubkey,
})
}
}
#[cfg(test)]
mod tests {
- use super::{Amount, Offer, OfferBuilder, Quantity};
+ use super::{Amount, Offer, OfferBuilder, OfferTlvStreamRef, Quantity};
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
#[test]
fn builds_offer_with_defaults() {
let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
- let tlv_stream = offer.as_tlv_stream();
+
let mut buffer = Vec::new();
offer.write(&mut buffer).unwrap();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.signing_pubkey(), pubkey(42));
- assert_eq!(tlv_stream.chains, None);
- assert_eq!(tlv_stream.metadata, None);
- assert_eq!(tlv_stream.currency, None);
- assert_eq!(tlv_stream.amount, None);
- assert_eq!(tlv_stream.description, Some(&String::from("foo")));
- assert_eq!(tlv_stream.features, None);
- assert_eq!(tlv_stream.absolute_expiry, None);
- assert_eq!(tlv_stream.paths, None);
- assert_eq!(tlv_stream.issuer, None);
- assert_eq!(tlv_stream.quantity_max, None);
- assert_eq!(tlv_stream.node_id, Some(&pubkey(42)));
+ assert_eq!(
+ offer.as_tlv_stream(),
+ OfferTlvStreamRef {
+ chains: None,
+ metadata: None,
+ currency: None,
+ amount: None,
+ description: Some(&String::from("foo")),
+ features: None,
+ absolute_expiry: None,
+ paths: None,
+ issuer: None,
+ quantity_max: None,
+ node_id: Some(&pubkey(42)),
+ },
+ );
if let Err(e) = Offer::try_from(buffer) {
panic!("error parsing offer: {:?}", e);
panic!("error parsing offer: {:?}", e);
}
- let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
- builder.offer.signing_pubkey = None;
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.node_id = None;
- let offer = builder.build().unwrap();
- match offer.to_string().parse::<Offer>() {
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey));
AlreadyExpired,
/// The provided chain hash does not correspond to a supported chain.
UnsupportedChain,
+ /// A chain was provided but was not expected.
+ UnexpectedChain,
/// An amount was expected but was missing.
MissingAmount,
/// The amount exceeded the total bitcoin supply.
InvalidAmount,
/// An amount was provided but was not sufficient in value.
InsufficientAmount,
+ /// An amount was provided but was not expected.
+ UnexpectedAmount,
/// A currency was provided that is not supported.
UnsupportedCurrency,
/// A feature was required but is unknown.
UnknownRequiredFeatures,
+ /// Features were provided but were not expected.
+ UnexpectedFeatures,
/// A required description was not provided.
MissingDescription,
/// A signing pubkey was not provided.
MissingSigningPubkey,
+ /// A signing pubkey was provided but was not expected.
+ UnexpectedSigningPubkey,
/// A quantity was expected but was missing.
MissingQuantity,
/// An unsupported quantity was provided.
--- /dev/null
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Data structures and encoding for refunds.
+//!
+//! A [`Refund`] is an "offer for money" and is typically constructed by a merchant and presented
+//! directly to the customer. The recipient responds with an `Invoice` to be paid.
+//!
+//! This is an [`InvoiceRequest`] produced *not* in response to an [`Offer`].
+//!
+//! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+//! [`Offer`]: crate::offers::offer::Offer
+//!
+//! ```ignore
+//! extern crate bitcoin;
+//! extern crate core;
+//! extern crate lightning;
+//!
+//! use core::convert::TryFrom;
+//! use core::time::Duration;
+//!
+//! use bitcoin::network::constants::Network;
+//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+//! use lightning::offers::parse::ParseError;
+//! use lightning::offers::refund::{Refund, RefundBuilder};
+//! use lightning::util::ser::{Readable, Writeable};
+//!
+//! # use lightning::onion_message::BlindedPath;
+//! # #[cfg(feature = "std")]
+//! # use std::time::SystemTime;
+//! #
+//! # fn create_blinded_path() -> BlindedPath { unimplemented!() }
+//! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
+//! #
+//! # #[cfg(feature = "std")]
+//! # fn build() -> Result<(), ParseError> {
+//! let secp_ctx = Secp256k1::new();
+//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
+//! let pubkey = PublicKey::from(keys);
+//!
+//! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
+//! let refund = RefundBuilder::new("coffee, large".to_string(), vec![1; 32], pubkey, 20_000)?
+//! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap())
+//! .issuer("Foo Bar".to_string())
+//! .path(create_blinded_path())
+//! .path(create_another_blinded_path())
+//! .chain(Network::Bitcoin)
+//! .payer_note("refund for order #12345".to_string())
+//! .build()?;
+//!
+//! // Encode as a bech32 string for use in a QR code.
+//! let encoded_refund = refund.to_string();
+//!
+//! // Parse from a bech32 string after scanning from a QR code.
+//! let refund = encoded_refund.parse::<Refund>()?;
+//!
+//! // Encode refund as raw bytes.
+//! let mut bytes = Vec::new();
+//! refund.write(&mut bytes).unwrap();
+//!
+//! // Decode raw bytes into an refund.
+//! let refund = Refund::try_from(bytes)?;
+//! # Ok(())
+//! # }
+//! ```
+
+use bitcoin::blockdata::constants::ChainHash;
+use bitcoin::network::constants::Network;
+use bitcoin::secp256k1::PublicKey;
+use core::convert::TryFrom;
+use core::str::FromStr;
+use core::time::Duration;
+use crate::io;
+use crate::ln::features::InvoiceRequestFeatures;
+use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
+use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
+use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
+use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
+use crate::onion_message::BlindedPath;
+use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
+use crate::util::string::PrintableString;
+
+use crate::prelude::*;
+
+#[cfg(feature = "std")]
+use std::time::SystemTime;
+
+/// Builds a [`Refund`] for the "offer for money" flow.
+///
+/// See [module-level documentation] for usage.
+///
+/// [module-level documentation]: self
+pub struct RefundBuilder {
+ refund: RefundContents,
+}
+
+impl RefundBuilder {
+ /// Creates a new builder for a refund using the [`Refund::payer_id`] for signing invoices. Use
+ /// a different pubkey per refund to avoid correlating refunds.
+ ///
+ /// Additionally, sets the required [`Refund::description`], [`Refund::metadata`], and
+ /// [`Refund::amount_msats`].
+ pub fn new(
+ description: String, metadata: Vec<u8>, payer_id: PublicKey, amount_msats: u64
+ ) -> Result<Self, SemanticError> {
+ if amount_msats > MAX_VALUE_MSAT {
+ return Err(SemanticError::InvalidAmount);
+ }
+
+ let refund = RefundContents {
+ payer: PayerContents(metadata), metadata: None, description, absolute_expiry: None,
+ issuer: None, paths: None, chain: None, amount_msats,
+ features: InvoiceRequestFeatures::empty(), payer_id, payer_note: None,
+ };
+
+ Ok(RefundBuilder { refund })
+ }
+
+ /// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
+ /// already passed is valid and can be checked for using [`Refund::is_expired`].
+ ///
+ /// Successive calls to this method will override the previous setting.
+ pub fn absolute_expiry(mut self, absolute_expiry: Duration) -> Self {
+ self.refund.absolute_expiry = Some(absolute_expiry);
+ self
+ }
+
+ /// Sets the [`Refund::issuer`].
+ ///
+ /// Successive calls to this method will override the previous setting.
+ pub fn issuer(mut self, issuer: String) -> Self {
+ self.refund.issuer = Some(issuer);
+ self
+ }
+
+ /// Adds a blinded path to [`Refund::paths`]. Must include at least one path if only connected
+ /// by private channels or if [`Refund::payer_id`] is not a public node id.
+ ///
+ /// Successive calls to this method will add another blinded path. Caller is responsible for not
+ /// adding duplicate paths.
+ pub fn path(mut self, path: BlindedPath) -> Self {
+ self.refund.paths.get_or_insert_with(Vec::new).push(path);
+ self
+ }
+
+ /// Sets the [`Refund::chain`] of the given [`Network`] for paying an invoice. If not
+ /// called, [`Network::Bitcoin`] is assumed.
+ ///
+ /// Successive calls to this method will override the previous setting.
+ pub fn chain(mut self, network: Network) -> Self {
+ self.refund.chain = Some(ChainHash::using_genesis_block(network));
+ self
+ }
+
+ /// Sets the [`Refund::payer_note`].
+ ///
+ /// Successive calls to this method will override the previous setting.
+ pub fn payer_note(mut self, payer_note: String) -> Self {
+ self.refund.payer_note = Some(payer_note);
+ self
+ }
+
+ /// Builds a [`Refund`] after checking for valid semantics.
+ pub fn build(mut self) -> Result<Refund, SemanticError> {
+ if self.refund.chain() == self.refund.implied_chain() {
+ self.refund.chain = None;
+ }
+
+ let mut bytes = Vec::new();
+ self.refund.write(&mut bytes).unwrap();
+
+ Ok(Refund {
+ bytes,
+ contents: self.refund,
+ })
+ }
+}
+
+#[cfg(test)]
+impl RefundBuilder {
+ fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self {
+ self.refund.features = features;
+ self
+ }
+}
+
+/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`].
+///
+/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
+/// recoup their funds. A refund may be used more generally as an "offer for money", such as with a
+/// bitcoin ATM.
+///
+/// [`Offer`]: crate::offers::offer::Offer
+#[derive(Clone, Debug)]
+pub struct Refund {
+ bytes: Vec<u8>,
+ contents: RefundContents,
+}
+
+/// The contents of a [`Refund`], which may be shared with an `Invoice`.
+#[derive(Clone, Debug)]
+struct RefundContents {
+ payer: PayerContents,
+ // offer fields
+ metadata: Option<Vec<u8>>,
+ description: String,
+ absolute_expiry: Option<Duration>,
+ issuer: Option<String>,
+ paths: Option<Vec<BlindedPath>>,
+ // invoice_request fields
+ chain: Option<ChainHash>,
+ amount_msats: u64,
+ features: InvoiceRequestFeatures,
+ payer_id: PublicKey,
+ payer_note: Option<String>,
+}
+
+impl Refund {
+ /// A complete description of the purpose of the refund. Intended to be displayed to the user
+ /// but with the caveat that it has not been verified in any way.
+ pub fn description(&self) -> PrintableString {
+ PrintableString(&self.contents.description)
+ }
+
+ /// Duration since the Unix epoch when an invoice should no longer be sent.
+ ///
+ /// If `None`, the refund does not expire.
+ pub fn absolute_expiry(&self) -> Option<Duration> {
+ self.contents.absolute_expiry
+ }
+
+ /// Whether the refund has expired.
+ #[cfg(feature = "std")]
+ pub fn is_expired(&self) -> bool {
+ match self.absolute_expiry() {
+ Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+ Ok(elapsed) => elapsed > seconds_from_epoch,
+ Err(_) => false,
+ },
+ None => false,
+ }
+ }
+
+ /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. Intended to be
+ /// displayed to the user but with the caveat that it has not been verified in any way.
+ pub fn issuer(&self) -> Option<PrintableString> {
+ self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str()))
+ }
+
+ /// Paths to the sender originating from publicly reachable nodes. Blinded paths provide sender
+ /// privacy by obfuscating its node id.
+ pub fn paths(&self) -> &[BlindedPath] {
+ self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[])
+ }
+
+ /// An unpredictable series of bytes, typically containing information about the derivation of
+ /// [`payer_id`].
+ ///
+ /// [`payer_id`]: Self::payer_id
+ pub fn metadata(&self) -> &[u8] {
+ &self.contents.payer.0
+ }
+
+ /// A chain that the refund is valid for.
+ pub fn chain(&self) -> ChainHash {
+ self.contents.chain.unwrap_or_else(|| self.contents.implied_chain())
+ }
+
+ /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
+ ///
+ /// [`chain`]: Self::chain
+ pub fn amount_msats(&self) -> u64 {
+ self.contents.amount_msats
+ }
+
+ /// Features pertaining to requesting an invoice.
+ pub fn features(&self) -> &InvoiceRequestFeatures {
+ &self.contents.features
+ }
+
+ /// A possibly transient pubkey used to sign the refund.
+ pub fn payer_id(&self) -> PublicKey {
+ self.contents.payer_id
+ }
+
+ /// Payer provided note to include in the invoice.
+ pub fn payer_note(&self) -> Option<PrintableString> {
+ self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
+ }
+
+ #[cfg(test)]
+ fn as_tlv_stream(&self) -> RefundTlvStreamRef {
+ self.contents.as_tlv_stream()
+ }
+}
+
+impl AsRef<[u8]> for Refund {
+ fn as_ref(&self) -> &[u8] {
+ &self.bytes
+ }
+}
+
+impl RefundContents {
+ fn chain(&self) -> ChainHash {
+ self.chain.unwrap_or_else(|| self.implied_chain())
+ }
+
+ pub fn implied_chain(&self) -> ChainHash {
+ ChainHash::using_genesis_block(Network::Bitcoin)
+ }
+
+ pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
+ let payer = PayerTlvStreamRef {
+ metadata: Some(&self.payer.0),
+ };
+
+ let offer = OfferTlvStreamRef {
+ chains: None,
+ metadata: self.metadata.as_ref(),
+ currency: None,
+ amount: None,
+ description: Some(&self.description),
+ features: None,
+ absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()),
+ paths: self.paths.as_ref(),
+ issuer: self.issuer.as_ref(),
+ quantity_max: None,
+ node_id: None,
+ };
+
+ let features = {
+ if self.features == InvoiceRequestFeatures::empty() { None }
+ else { Some(&self.features) }
+ };
+
+ let invoice_request = InvoiceRequestTlvStreamRef {
+ chain: self.chain.as_ref(),
+ amount: Some(self.amount_msats),
+ features,
+ quantity: None,
+ payer_id: Some(&self.payer_id),
+ payer_note: self.payer_note.as_ref(),
+ };
+
+ (payer, offer, invoice_request)
+ }
+}
+
+impl Writeable for Refund {
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ WithoutLength(&self.bytes).write(writer)
+ }
+}
+
+impl Writeable for RefundContents {
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ self.as_tlv_stream().write(writer)
+ }
+}
+
+type RefundTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
+
+type RefundTlvStreamRef<'a> = (
+ PayerTlvStreamRef<'a>,
+ OfferTlvStreamRef<'a>,
+ InvoiceRequestTlvStreamRef<'a>,
+);
+
+impl SeekReadable for RefundTlvStream {
+ fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
+ let payer = SeekReadable::read(r)?;
+ let offer = SeekReadable::read(r)?;
+ let invoice_request = SeekReadable::read(r)?;
+
+ Ok((payer, offer, invoice_request))
+ }
+}
+
+impl Bech32Encode for Refund {
+ const BECH32_HRP: &'static str = "lnr";
+}
+
+impl FromStr for Refund {
+ type Err = ParseError;
+
+ fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
+ Refund::from_bech32_str(s)
+ }
+}
+
+impl TryFrom<Vec<u8>> for Refund {
+ type Error = ParseError;
+
+ fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+ let refund = ParsedMessage::<RefundTlvStream>::try_from(bytes)?;
+ let ParsedMessage { bytes, tlv_stream } = refund;
+ let contents = RefundContents::try_from(tlv_stream)?;
+
+ Ok(Refund { bytes, contents })
+ }
+}
+
+impl TryFrom<RefundTlvStream> for RefundContents {
+ type Error = SemanticError;
+
+ fn try_from(tlv_stream: RefundTlvStream) -> Result<Self, Self::Error> {
+ let (
+ PayerTlvStream { metadata: payer_metadata },
+ OfferTlvStream {
+ chains, metadata, currency, amount: offer_amount, description,
+ features: offer_features, absolute_expiry, paths, issuer, quantity_max, node_id,
+ },
+ InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note },
+ ) = tlv_stream;
+
+ let payer = match payer_metadata {
+ None => return Err(SemanticError::MissingPayerMetadata),
+ Some(metadata) => PayerContents(metadata),
+ };
+
+ if chains.is_some() {
+ return Err(SemanticError::UnexpectedChain);
+ }
+
+ if currency.is_some() || offer_amount.is_some() {
+ return Err(SemanticError::UnexpectedAmount);
+ }
+
+ let description = match description {
+ None => return Err(SemanticError::MissingDescription),
+ Some(description) => description,
+ };
+
+ if offer_features.is_some() {
+ return Err(SemanticError::UnexpectedFeatures);
+ }
+
+ let absolute_expiry = absolute_expiry.map(Duration::from_secs);
+
+ if quantity_max.is_some() {
+ return Err(SemanticError::UnexpectedQuantity);
+ }
+
+ if node_id.is_some() {
+ return Err(SemanticError::UnexpectedSigningPubkey);
+ }
+
+ let amount_msats = match amount {
+ None => return Err(SemanticError::MissingAmount),
+ Some(amount_msats) if amount_msats > MAX_VALUE_MSAT => {
+ return Err(SemanticError::InvalidAmount);
+ },
+ Some(amount_msats) => amount_msats,
+ };
+
+ let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
+
+ // TODO: Check why this isn't in the spec.
+ if quantity.is_some() {
+ return Err(SemanticError::UnexpectedQuantity);
+ }
+
+ let payer_id = match payer_id {
+ None => return Err(SemanticError::MissingPayerId),
+ Some(payer_id) => payer_id,
+ };
+
+ // TODO: Should metadata be included?
+ Ok(RefundContents {
+ payer, metadata, description, absolute_expiry, issuer, paths, chain, amount_msats,
+ features, payer_id, payer_note,
+ })
+ }
+}
+
+impl core::fmt::Display for Refund {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ self.fmt_bech32_str(f)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Refund, RefundBuilder, RefundTlvStreamRef};
+
+ use bitcoin::blockdata::constants::ChainHash;
+ use bitcoin::network::constants::Network;
+ use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+ use core::convert::TryFrom;
+ use core::time::Duration;
+ use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
+ use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+ use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
+ use crate::offers::offer::OfferTlvStreamRef;
+ use crate::offers::parse::{ParseError, SemanticError};
+ use crate::offers::payer::PayerTlvStreamRef;
+ use crate::onion_message::{BlindedHop, BlindedPath};
+ use crate::util::ser::{BigSize, Writeable};
+ use crate::util::string::PrintableString;
+
+ fn payer_pubkey() -> PublicKey {
+ let secp_ctx = Secp256k1::new();
+ KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()).public_key()
+ }
+
+ fn pubkey(byte: u8) -> PublicKey {
+ let secp_ctx = Secp256k1::new();
+ PublicKey::from_secret_key(&secp_ctx, &privkey(byte))
+ }
+
+ fn privkey(byte: u8) -> SecretKey {
+ SecretKey::from_slice(&[byte; 32]).unwrap()
+ }
+
+ trait ToBytes {
+ fn to_bytes(&self) -> Vec<u8>;
+ }
+
+ impl<'a> ToBytes for RefundTlvStreamRef<'a> {
+ fn to_bytes(&self) -> Vec<u8> {
+ let mut buffer = Vec::new();
+ self.write(&mut buffer).unwrap();
+ buffer
+ }
+ }
+
+ #[test]
+ fn builds_refund_with_defaults() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .build().unwrap();
+
+ let mut buffer = Vec::new();
+ refund.write(&mut buffer).unwrap();
+
+ assert_eq!(refund.bytes, buffer.as_slice());
+ assert_eq!(refund.metadata(), &[1; 32]);
+ assert_eq!(refund.description(), PrintableString("foo"));
+ assert_eq!(refund.absolute_expiry(), None);
+ #[cfg(feature = "std")]
+ assert!(!refund.is_expired());
+ assert_eq!(refund.paths(), &[]);
+ assert_eq!(refund.issuer(), None);
+ assert_eq!(refund.chain(), ChainHash::using_genesis_block(Network::Bitcoin));
+ assert_eq!(refund.amount_msats(), 1000);
+ assert_eq!(refund.features(), &InvoiceRequestFeatures::empty());
+ assert_eq!(refund.payer_id(), payer_pubkey());
+ assert_eq!(refund.payer_note(), None);
+
+ assert_eq!(
+ refund.as_tlv_stream(),
+ (
+ PayerTlvStreamRef { metadata: Some(&vec![1; 32]) },
+ OfferTlvStreamRef {
+ chains: None,
+ metadata: None,
+ currency: None,
+ amount: None,
+ description: Some(&String::from("foo")),
+ features: None,
+ absolute_expiry: None,
+ paths: None,
+ issuer: None,
+ quantity_max: None,
+ node_id: None,
+ },
+ InvoiceRequestTlvStreamRef {
+ chain: None,
+ amount: Some(1000),
+ features: None,
+ quantity: None,
+ payer_id: Some(&payer_pubkey()),
+ payer_note: None,
+ },
+ ),
+ );
+
+ if let Err(e) = Refund::try_from(buffer) {
+ panic!("error parsing refund: {:?}", e);
+ }
+ }
+
+ #[test]
+ fn fails_building_refund_with_invalid_amount() {
+ match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), MAX_VALUE_MSAT + 1) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SemanticError::InvalidAmount),
+ }
+ }
+
+ #[test]
+ fn builds_refund_with_absolute_expiry() {
+ let future_expiry = Duration::from_secs(u64::max_value());
+ let past_expiry = Duration::from_secs(0);
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .absolute_expiry(future_expiry)
+ .build()
+ .unwrap();
+ let (_, tlv_stream, _) = refund.as_tlv_stream();
+ #[cfg(feature = "std")]
+ assert!(!refund.is_expired());
+ assert_eq!(refund.absolute_expiry(), Some(future_expiry));
+ assert_eq!(tlv_stream.absolute_expiry, Some(future_expiry.as_secs()));
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .absolute_expiry(future_expiry)
+ .absolute_expiry(past_expiry)
+ .build()
+ .unwrap();
+ let (_, tlv_stream, _) = refund.as_tlv_stream();
+ #[cfg(feature = "std")]
+ assert!(refund.is_expired());
+ assert_eq!(refund.absolute_expiry(), Some(past_expiry));
+ assert_eq!(tlv_stream.absolute_expiry, Some(past_expiry.as_secs()));
+ }
+
+ #[test]
+ fn builds_refund_with_paths() {
+ let paths = vec![
+ BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
+ BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] },
+ ],
+ },
+ BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
+ BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] },
+ ],
+ },
+ ];
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .path(paths[0].clone())
+ .path(paths[1].clone())
+ .build()
+ .unwrap();
+ let (_, offer_tlv_stream, invoice_request_tlv_stream) = refund.as_tlv_stream();
+ assert_eq!(refund.paths(), paths.as_slice());
+ assert_eq!(refund.payer_id(), pubkey(42));
+ assert_ne!(pubkey(42), pubkey(44));
+ assert_eq!(offer_tlv_stream.paths, Some(&paths));
+ assert_eq!(invoice_request_tlv_stream.payer_id, Some(&pubkey(42)));
+ }
+
+ #[test]
+ fn builds_refund_with_issuer() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .issuer("bar".into())
+ .build()
+ .unwrap();
+ let (_, tlv_stream, _) = refund.as_tlv_stream();
+ assert_eq!(refund.issuer(), Some(PrintableString("bar")));
+ assert_eq!(tlv_stream.issuer, Some(&String::from("bar")));
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .issuer("bar".into())
+ .issuer("baz".into())
+ .build()
+ .unwrap();
+ let (_, tlv_stream, _) = refund.as_tlv_stream();
+ assert_eq!(refund.issuer(), Some(PrintableString("baz")));
+ assert_eq!(tlv_stream.issuer, Some(&String::from("baz")));
+ }
+
+ #[test]
+ fn builds_refund_with_chain() {
+ let mainnet = ChainHash::using_genesis_block(Network::Bitcoin);
+ let testnet = ChainHash::using_genesis_block(Network::Testnet);
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .chain(Network::Bitcoin)
+ .build().unwrap();
+ let (_, _, tlv_stream) = refund.as_tlv_stream();
+ assert_eq!(refund.chain(), mainnet);
+ assert_eq!(tlv_stream.chain, None);
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .chain(Network::Testnet)
+ .build().unwrap();
+ let (_, _, tlv_stream) = refund.as_tlv_stream();
+ assert_eq!(refund.chain(), testnet);
+ assert_eq!(tlv_stream.chain, Some(&testnet));
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .chain(Network::Regtest)
+ .chain(Network::Testnet)
+ .build().unwrap();
+ let (_, _, tlv_stream) = refund.as_tlv_stream();
+ assert_eq!(refund.chain(), testnet);
+ assert_eq!(tlv_stream.chain, Some(&testnet));
+ }
+
+ #[test]
+ fn builds_refund_with_payer_note() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .payer_note("bar".into())
+ .build().unwrap();
+ let (_, _, tlv_stream) = refund.as_tlv_stream();
+ assert_eq!(refund.payer_note(), Some(PrintableString("bar")));
+ assert_eq!(tlv_stream.payer_note, Some(&String::from("bar")));
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .payer_note("bar".into())
+ .payer_note("baz".into())
+ .build().unwrap();
+ let (_, _, tlv_stream) = refund.as_tlv_stream();
+ assert_eq!(refund.payer_note(), Some(PrintableString("baz")));
+ assert_eq!(tlv_stream.payer_note, Some(&String::from("baz")));
+ }
+
+ #[test]
+ fn parses_refund_with_metadata() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .build().unwrap();
+ if let Err(e) = refund.to_string().parse::<Refund>() {
+ panic!("error parsing refund: {:?}", e);
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.0.metadata = None;
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPayerMetadata));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_refund_with_description() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .build().unwrap();
+ if let Err(e) = refund.to_string().parse::<Refund>() {
+ panic!("error parsing refund: {:?}", e);
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.1.description = None;
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingDescription));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_refund_with_amount() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .build().unwrap();
+ if let Err(e) = refund.to_string().parse::<Refund>() {
+ panic!("error parsing refund: {:?}", e);
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.2.amount = None;
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount));
+ },
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.2.amount = Some(MAX_VALUE_MSAT + 1);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidAmount));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_refund_with_payer_id() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .build().unwrap();
+ if let Err(e) = refund.to_string().parse::<Refund>() {
+ panic!("error parsing refund: {:?}", e);
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.2.payer_id = None;
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPayerId));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_refund_with_optional_fields() {
+ let past_expiry = Duration::from_secs(0);
+ let paths = vec![
+ BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
+ BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] },
+ ],
+ },
+ BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
+ BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] },
+ ],
+ },
+ ];
+
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .absolute_expiry(past_expiry)
+ .issuer("bar".into())
+ .path(paths[0].clone())
+ .path(paths[1].clone())
+ .chain(Network::Testnet)
+ .features_unchecked(InvoiceRequestFeatures::unknown())
+ .payer_note("baz".into())
+ .build()
+ .unwrap();
+ match refund.to_string().parse::<Refund>() {
+ Ok(refund) => {
+ assert_eq!(refund.absolute_expiry(), Some(past_expiry));
+ #[cfg(feature = "std")]
+ assert!(refund.is_expired());
+ assert_eq!(refund.paths(), &paths[..]);
+ assert_eq!(refund.issuer(), Some(PrintableString("bar")));
+ assert_eq!(refund.chain(), ChainHash::using_genesis_block(Network::Testnet));
+ assert_eq!(refund.features(), &InvoiceRequestFeatures::unknown());
+ assert_eq!(refund.payer_note(), Some(PrintableString("baz")));
+ },
+ Err(e) => panic!("error parsing refund: {:?}", e),
+ }
+ }
+
+ #[test]
+ fn fails_parsing_refund_with_unexpected_fields() {
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .build().unwrap();
+ if let Err(e) = refund.to_string().parse::<Refund>() {
+ panic!("error parsing refund: {:?}", e);
+ }
+
+ let chains = vec![ChainHash::using_genesis_block(Network::Testnet)];
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.1.chains = Some(&chains);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedChain));
+ },
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.1.currency = Some(&b"USD");
+ tlv_stream.1.amount = Some(1000);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedAmount));
+ },
+ }
+
+ let features = OfferFeatures::unknown();
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.1.features = Some(&features);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedFeatures));
+ },
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.1.quantity_max = Some(10);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedQuantity));
+ },
+ }
+
+ let node_id = payer_pubkey();
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.1.node_id = Some(&node_id);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedSigningPubkey));
+ },
+ }
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.2.quantity = Some(10);
+
+ match Refund::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedQuantity));
+ },
+ }
+ }
+
+ #[test]
+ fn fails_parsing_refund_with_extra_tlv_records() {
+ let secp_ctx = Secp256k1::new();
+ let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
+ let refund = RefundBuilder::new("foo".into(), vec![1; 32], keys.public_key(), 1000).unwrap()
+ .build().unwrap();
+
+ let mut encoded_refund = Vec::new();
+ refund.write(&mut encoded_refund).unwrap();
+ BigSize(1002).write(&mut encoded_refund).unwrap();
+ BigSize(32).write(&mut encoded_refund).unwrap();
+ [42u8; 32].write(&mut encoded_refund).unwrap();
+
+ match Refund::try_from(encoded_refund) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
+ }
+ }
+}
)*
}
+ #[derive(Debug, PartialEq)]
pub(super) struct $nameref<'a> {
$(
pub(super) $field: Option<tlv_record_ref_type!($fieldty)>,