Skip to content

feat: hold a spliced successor's funding transaction until its CET locktime - #238

Open
matthewjablack wants to merge 2 commits into
bennyhodl:masterfrom
matthewjablack:feat/successor-fund-locktime
Open

matthewjablack wants to merge 2 commits into
bennyhodl:masterfrom
matthewjablack:feat/successor-fund-locktime

Conversation

@matthewjablack

Copy link
Copy Markdown
Contributor

Summary

Lets a spliced successor contract be signed now and its funding transaction held until a later time.

  • New contract_flags bit FUND_LOCKTIME_AT_CET_FLAG (0x04). A spliced contract (one with a DLC input) whose offer sets it gets a funding transaction with nLockTime = cet_locktime. ddk-dlc already turns a nonzero funding locktime into sequence 0xfffffffe on every funding input, the DLC input included, so nothing below the builder changes.
  • A flag, not an argument. contract_flags already travels on the offer and is stored with the contract, so both parties, and a stored contract rebuilt later, get the same bytes.
  • Old splices are unchanged. A splice without the bit, and any contract without a DLC input, keeps locktime 0. The pinned transaction digests still pass.
  • Bit 1 is skipped on purpose. The stored OfferedContract reader treats a first byte of 0x02/0x03 as a compressed pubkey, so contract_flags of 0x02 or 0x03 would be misread.
  • Unknown bits are now rejected. Offer creation and validation reject any bit ddk gives no meaning to, in both ddk::contract and the Manager. The builder does not reject them, so stored contracts still rebuild.

Manager

The Manager broadcast the funding transaction on sign and returned the error if it was refused. A held successor was rejected as non-final, and the contract it replaced was never marked as closing. Now:

  • On sign, the accepting party stores the signed funding transaction of a held successor in dlc_transactions.fund. It is the only party with every signature.
  • While that transaction is not final, the Manager doesn't broadcast. The contract stays Signed and the parent stays Confirmed.
  • Finality is checked against the chain:
    • a height locktime is final once it is ≤ the tip height;
    • a time locktime is final once it is < the tip's median time past (BIP 113).
  • The periodic check broadcasts the stored transaction once it is final, then pre-closes the parents. This doesn't sign again, so a signer that went offline after signing doesn't block it.
  • Every broadcast error after finality still propagates.

API addition: Blockchain::get_median_time_past is a new required method. EsploraClient reads it from the tip's block info. A default implementation isn't possible: async_trait default bodies need Self: Sync, and the Manager's B::Target doesn't provide it. Any external Blockchain implementor will need to add this method.

Tests

  • Unit, stateless:
    • a flagged splice has lock_time == cet_locktime, every input has sequence 0xfffffffe, both halves of the DLC input verify, and the txid matches the rebuilt transaction;
    • an unflagged splice keeps locktime 0;
    • a flagged contract without a DLC input keeps locktime 0;
    • flags 0x05 round-trip on the wire;
    • unknown flags (0x02, 0x03, 0x06, 0x08, 0x80) are rejected by create_offer and validate_offer.
  • Unit, manager:
    • 0x04/0x05 round-trip through both stored OfferedContract formats;
    • 0x02/0x03 are rejected;
    • boundary cases for is_final_at.
  • Regtest, stateless_execution::held_splice_broadcasts_at_the_cet_locktime: the node refuses the held funding transaction as non-final one block early, accepts the same bytes at the locktime, and the successor settles with a CET.
  • Regtest, manager_execution_tests::held_splice_enum_single_oracle_test (dual-funded successor):
    • both Managers keep the successor Signed and the parent Confirmed until the locktime;
    • the acceptor's stored transaction is fully signed;
    • the acceptor broadcasts at the locktime and both sides pre-close and then close the parent;
    • the mined wtxid equals the stored transaction's;
    • EsploraClient's median time past matches bitcoind's.
  • The existing splice regtest tests and the full unit suite pass.

…cktime

A new contract_flags bit, FUND_LOCKTIME_AT_CET_FLAG (0x04), gives a spliced
contract's funding transaction nLockTime = the offer's CET locktime. ddk-dlc
already turns a nonzero funding locktime into sequence 0xfffffffe on every
funding input, the DLC input included, so a successor can be signed now and
held until then. Contracts without a DLC input, and every splice without the
bit, keep locktime 0 and build the same bytes as before.

The switch is a flag rather than an argument because the flag already travels
on the offer and is stored with the contract: both parties, and a stored
contract rebuilt later, get the same transactions.

Bit 1 is skipped. The stored OfferedContract reader takes a first byte of 0x02
or 0x03 as a compressed pubkey, so contract_flags 0x02/0x03 would be misread.
Offer creation and validation (stateless and Manager) now reject any bit ddk
gives no meaning to.

The Manager broadcast the funding transaction on sign and returned the error
when the network refused it, so a held successor failed as non-final and its
replaced contract was never marked as closing. A held funding transaction the
network refuses now leaves the contract Signed and the replaced contract
Confirmed; the periodic check re-signs it from the stored signatures and
broadcasts it once it is final, then pre-closes the replaced contracts.
… signed

The Manager treated any broadcast error on a funding transaction with a
locktime as a hold, so a final transaction the network refused for a bad
witness or a missing input left the contract Signed and returned Ok. It now
checks finality itself: a height locktime is final once the tip reaches it,
a time locktime once the tip's median time past is beyond it (BIP 113). It
skips the broadcast only while the transaction is not final, and every
broadcast error after that propagates.

Blockchain gains get_median_time_past for that check; EsploraClient reads it
from the tip's block info.

The accepting party also re-signed the held transaction with its wallet on
every periodic check, so a signer that went offline after signing kept the
successor from ever being broadcast. It now stores the signed funding
transaction when it signs a held successor and broadcasts exactly that.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant