Skip to content

[WIP, POC] Tests for Interactive TX #2541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a7ed97a
Add `InteractiveTxConstructor` state machine`
dunxen Mar 21, 2023
8536621
Check add_output sats amount < 2.1 quadrillion sats
jurvis Apr 21, 2023
8cf4ee7
Add test module for API usage experiment
jurvis Apr 21, 2023
af5133e
Add ascii diagram & remove tx_signatures
dunxen May 7, 2023
7310736
Update API usage
jurvis May 8, 2023
487f279
Wrap InteractiveTxStateMachine in constructor struct
jurvis Jul 14, 2023
46c5f1a
Add abort negotiation
jurvis Jul 14, 2023
2643fce
Abstract mem::take to function
jurvis Jul 16, 2023
6ffc4b3
Exhaustively handle adding/removing inputs and outputs
jurvis Jul 16, 2023
6779d74
Added tx_complete handling to state machine
jurvis Jul 16, 2023
2591320
Add APIs for send/receive tx complete
jurvis Jul 17, 2023
26ce75f
Clean up comments
jurvis Jul 17, 2023
1d78971
Tidy up visibility
jurvis Jul 17, 2023
0dd26b9
[wip] add tests
jurvis Jul 17, 2023
8403569
[wip] more things
jurvis Aug 5, 2023
5342b1a
Make data structure for TxIn/TxOut tuple
jurvis Aug 7, 2023
3ceb99c
MOAR validation
wpaulino Aug 7, 2023
4b9f237
Minor cleanup
wpaulino Aug 8, 2023
9b4f2e5
Add better errors
jurvis Aug 9, 2023
94deca8
Use 21M from channel.rs
jurvis Aug 9, 2023
f10a639
Add more TODOs
jurvis Aug 9, 2023
4d72dfc
Change serial_id in messages to SerialId
jurvis Aug 16, 2023
b82b77d
Accept changes on TheirTxComplete
jurvis Aug 16, 2023
bc2adaf
Merge branch 'jurvis-interact-tx' into interact-tx
optout21 Aug 27, 2023
1358d65
Adaptations, compile fixes
optout21 Aug 27, 2023
3f21de9
ChannelId adaptations
optout21 Aug 27, 2023
de3f4b1
Add unit tests; minor changes (fee substraction)
optout21 Aug 30, 2023
d2f5fe4
Add missing docs (error in CI)
optout21 Aug 30, 2023
deca874
Remove 'use std', compile fix
optout21 Aug 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add InteractiveTxConstructor state machine`
  • Loading branch information
dunxen authored and jurvis committed May 8, 2023
commit a7ed97a08a1dde426186edb9a207f80121bf421b
263 changes: 263 additions & 0 deletions lightning/src/ln/interactivetxs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
// 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.

use std::collections::{HashMap, HashSet};

use bitcoin::{psbt::Psbt, TxIn, Sequence, Transaction, TxOut, OutPoint};

use super::msgs::TxAddInput;

/// The number of received `tx_add_input` messages during a negotiation at which point the
/// negotiation MUST be failed.
const MAX_RECEIVED_TX_ADD_INPUT_COUNT: u16 = 4096;

/// The number of received `tx_add_output` messages during a negotiation at which point the
/// negotiation MUST be failed.
const MAX_RECEIVED_TX_ADD_OUTPUT_COUNT: u16 = 4096;

type SerialId = u64;
trait SerialIdExt {
fn is_valid_for_initiator(&self) -> bool;
}
impl SerialIdExt for SerialId {
fn is_valid_for_initiator(&self) -> bool { self % 2 == 0 }
}

pub(crate) enum InteractiveTxConstructionError {
InputsNotConfirmed,
ReceivedTooManyTxAddInputs,
ReceivedTooManyTxAddOutputs,
IncorrectInputSequenceValue,
IncorrectSerialIdParity,
SerialIdUnknown,
DuplicateSerialId,
PrevTxOutInvalid,
}

// States
// TODO: ASCII state machine
pub(crate) trait AcceptingChanges {}

/// We are currently in the process of negotiating the transaction.
pub(crate) struct Negotiating;
/// We have sent a `tx_complete` message and are awaiting the counterparty's.
pub(crate) struct OurTxComplete;
/// We have received a `tx_complete` message and the counterparty is awaiting ours.
pub(crate) struct TheirTxComplete;
/// We have exchanged consecutive `tx_complete` messages with the counterparty and the transaction
/// negotiation is complete.
pub(crate) struct NegotiationComplete;
/// We have sent a `tx_signatures` message and the counterparty is awaiting ours.
pub(crate) struct OurTxSignatures;
/// We have received a `tx_signatures` message from the counterparty
pub(crate) struct TheirTxSignatures;
/// The negotiation has failed and cannot be continued.
pub(crate) struct NegotiationFailed {
error: InteractiveTxConstructionError,
}

// TODO: Add RBF negotiation

impl AcceptingChanges for Negotiating {}
impl AcceptingChanges for OurTxComplete {}

struct NegotiationContext {
channel_id: [u8; 32],
require_confirmed_inputs: bool,
holder_is_initiator: bool,
received_tx_add_input_count: u16,
received_tx_add_output_count: u16,
inputs: HashMap<u64, TxIn>,
prevtx_outpoints: HashSet<OutPoint>,
outputs: HashMap<u64, TxOut>,
base_tx: Transaction,
}

pub(crate) struct InteractiveTxConstructor<S> {
inner: Box<NegotiationContext>,
state: S,
}

impl InteractiveTxConstructor<Negotiating> {
fn new(channel_id: [u8; 32], require_confirmed_inputs: bool, is_initiator: bool, base_tx: Transaction) -> Self {
Self {
inner: Box::new(NegotiationContext {
channel_id,
require_confirmed_inputs,
holder_is_initiator: is_initiator,
received_tx_add_input_count: 0,
received_tx_add_output_count: 0,
inputs: HashMap::new(),
prevtx_outpoints: HashSet::new(),
outputs: HashMap::new(),
base_tx,
}),
state: Negotiating,
}
}
}

impl<S> InteractiveTxConstructor<S>
where S: AcceptingChanges {
fn fail_negotiation(self, error: InteractiveTxConstructionError) ->
Result<InteractiveTxConstructor<Negotiating>, InteractiveTxConstructor<NegotiationFailed>> {
Err(InteractiveTxConstructor { inner: self.inner, state: NegotiationFailed { error } })
}

fn receive_tx_add_input(mut self, serial_id: SerialId, msg: TxAddInput, confirmed: bool) ->
Result<InteractiveTxConstructor<Negotiating>, InteractiveTxConstructor<NegotiationFailed>> {
// - TODO: MUST fail the negotiation if:
// - `prevtx` is not a valid transaction
if !self.is_valid_counterparty_serial_id(serial_id) {
// The receiving node:
// - MUST fail the negotiation if:
// - the `serial_id` has the wrong parity
return self.fail_negotiation(InteractiveTxConstructionError::IncorrectSerialIdParity);
}

if msg.sequence >= 0xFFFFFFFE {
// The receiving node:
// - MUST fail the negotiation if:
// - `sequence` is set to `0xFFFFFFFE` or `0xFFFFFFFF`
return self.fail_negotiation(InteractiveTxConstructionError::IncorrectInputSequenceValue);
}

if self.inner.require_confirmed_inputs && !confirmed {
return self.fail_negotiation(InteractiveTxConstructionError::InputsNotConfirmed);
}

if let Some(tx_out) = msg.prevtx.output.get(msg.prevtx_out as usize) {
if !tx_out.script_pubkey.is_witness_program() {
// The receiving node:
// - MUST fail the negotiation if:
// - the `scriptPubKey` is not a witness program
return self.fail_negotiation(InteractiveTxConstructionError::PrevTxOutInvalid);
} else if !self.inner.prevtx_outpoints.insert(OutPoint { txid: msg.prevtx.txid(), vout: msg.prevtx_out }) {
// The receiving node:
// - MUST fail the negotiation if:
// - the `prevtx` and `prevtx_vout` are identical to a previously added
// (and not removed) input's
return self.fail_negotiation(InteractiveTxConstructionError::PrevTxOutInvalid);
}
} else {
// The receiving node:
// - MUST fail the negotiation if:
// - `prevtx_vout` is greater or equal to the number of outputs on `prevtx`
return self.fail_negotiation(InteractiveTxConstructionError::PrevTxOutInvalid);
}

self.inner.received_tx_add_input_count += 1;
if self.inner.received_tx_add_input_count > MAX_RECEIVED_TX_ADD_INPUT_COUNT {
// The receiving node:
// - MUST fail the negotiation if:
// - if has received 4096 `tx_add_input` messages during this negotiation
return self.fail_negotiation(InteractiveTxConstructionError::ReceivedTooManyTxAddInputs);
}

if let None = self.inner.inputs.insert(serial_id, TxIn {
previous_output: OutPoint { txid: msg.prevtx.txid(), vout: msg.prevtx_out },
sequence: Sequence(msg.sequence),
..Default::default()
}) {
Ok(InteractiveTxConstructor { inner: self.inner, state: Negotiating {} })
} else {
// The receiving node:
// - MUST fail the negotiation if:
// - the `serial_id` is already included in the transaction
self.fail_negotiation(InteractiveTxConstructionError::DuplicateSerialId)
}
}

fn receive_tx_remove_input(mut self, serial_id: SerialId) ->
Result<InteractiveTxConstructor<Negotiating>, InteractiveTxConstructor<NegotiationFailed>> {
if !self.is_valid_counterparty_serial_id(serial_id) {
return self.fail_negotiation(InteractiveTxConstructionError::IncorrectSerialIdParity);
}

if let Some(input) = self.inner.inputs.remove(&serial_id) {
self.inner.prevtx_outpoints.remove(&input.previous_output);
Ok(InteractiveTxConstructor { inner: self.inner, state: Negotiating {} })
} else {
// The receiving node:
// - MUST fail the negotiation if:
// - the input or output identified by the `serial_id` was not added by the sender
// - the `serial_id` does not correspond to a currently added input
self.fail_negotiation(InteractiveTxConstructionError::SerialIdUnknown)
}
}

fn receive_tx_add_output(mut self, serial_id: u64, output: TxOut) ->
Result<InteractiveTxConstructor<Negotiating>, InteractiveTxConstructor<NegotiationFailed>> {
self.inner.received_tx_add_output_count += 1;
if self.inner.received_tx_add_output_count > MAX_RECEIVED_TX_ADD_OUTPUT_COUNT {
// The receiving node:
// - MUST fail the negotiation if:
// - if has received 4096 `tx_add_output` messages during this negotiation
return self.fail_negotiation(InteractiveTxConstructionError::ReceivedTooManyTxAddOutputs);
}

if let None = self.inner.outputs.insert(serial_id, output) {
Ok(InteractiveTxConstructor { inner: self.inner, state: Negotiating {} })
} else {
// The receiving node:
// - MUST fail the negotiation if:
// - the `serial_id` is already included in the transaction
self.fail_negotiation(InteractiveTxConstructionError::DuplicateSerialId)
}
}


fn send_tx_add_input(mut self, serial_id: u64, input: TxIn) -> InteractiveTxConstructor<Negotiating> {
self.inner.inputs.insert(serial_id, input);
InteractiveTxConstructor { inner: self.inner, state: Negotiating {} }
}

pub(crate) fn send_tx_add_output(mut self, serial_id: u64, output: TxOut) -> InteractiveTxConstructor<Negotiating> {
self.inner.outputs.insert(serial_id, output);
InteractiveTxConstructor { inner: self.inner, state: Negotiating {} }
}

pub(crate) fn send_tx_abort(mut self) -> InteractiveTxConstructor<NegotiationFailed> {
todo!();
}

pub(crate) fn receive_tx_abort(mut self) -> InteractiveTxConstructor<NegotiationFailed> {
todo!();
}

fn is_valid_counterparty_serial_id(&self, serial_id: SerialId) -> bool {
// A received `SerialId`'s parity must match the role of the counterparty.
self.inner.holder_is_initiator == !serial_id.is_valid_for_initiator()
}
}

impl InteractiveTxConstructor<TheirTxComplete> {
fn send_tx_complete(self) -> InteractiveTxConstructor<NegotiationComplete> {
InteractiveTxConstructor {
inner: self.inner,
state: NegotiationComplete {}
}
}
}

impl InteractiveTxConstructor<OurTxComplete> {
fn receive_tx_complete(self) -> InteractiveTxConstructor<NegotiationComplete> {
InteractiveTxConstructor {
inner: self.inner,
state: NegotiationComplete {}
}
}
}

impl InteractiveTxConstructor<NegotiationComplete> {
fn get_psbt(&self) -> Result<Psbt, InteractiveTxConstructionError> {
// Build PSBT from inputs & outputs
todo!();
}
}