Repository navigation
v2.0.0-rc.9
Pre-releaseDLC DevKit 2.0.0-rc.9
This release adds payout script overrides, which let an offer send an outcome's payout to a third party, such as a liquidator in a loan. It fixes settlement of contracts with more than one contract info. It also removes native-tls/OpenSSL from the workspace, so the esplora client verifies TLS with rustls and no longer trusts compromised eMudhra intermediate CAs. Funding, CET and refund transaction construction now live in one builder, and pinned fixtures confirm that every existing contract shape builds the same transactions as before.
All workspace crates are released at 2.0.0-rc.9: ddk, ddk-manager, ddk-dlc, ddk-messages, ddk-trie, ddk-payouts, ddk-node and kormir.
⚠️ Breaking Changes
- Offer TLV records are now allowlisted, and none are allowed by default (dbecd57). The manager rejects an offer received or committed with a TLV record whose type the application hasn't allowed. This includes the application records that offers have been able to carry since rc.4. If your application sends or accepts such records, list their types with
Manager::with_allowed_offer_tlv_typesor theddkbuilder'sset_allowed_offer_tlv_types. The library applies every CET record it knows no matter which party wrote it, and a record can redirect what the accepting party is paid. Your application still has to check the records on an offer before it accepts it. _with_fee_ruleconstructors inddk-dlctake the longest payout script length for the accepting party's output (dbecd57). Funding reserves the CET fee for that output at this length, so an override script that is longer than the accepting party's own script is still covered.- Duplicate CET record types are rejected (dbecd57). An offer that carries the same CET record type twice is invalid, because this crate reads the first copy and node-dlc reads the last.
🔒 Security Fixes
-
Esplora TLS now uses rustls instead of native-tls (c29872c).
bdk_esplora'sasync-httpsfeature enabled reqwest 0.12default-tls, which is native-tls: OpenSSL with the system root store on Linux. The keys of eight eMudhra intermediate CAs are compromised, and those CAs chain to roots that system stores still ship ("AAA Certificate Services", "emSign ECC Root CA - G3"). Anyone could mint a certificate that the esplora client accepted and feed a node fake chain state.ddknow usesasync-https-rustls, with these updates:rustls-webpki0.103.16 rejects paths through the distrusted SPKIs regardless of the root store.webpki-roots/webpki-root-certs1.0.10 drop the affected trust.rustls0.23.46 fixes RUSTSEC-2026-0346 through -0349, which are now on the esplora path.
Downstream crates couldn't fix this themselves: Cargo features are additive, and reqwest 0.12 picks native-tls whenever
default-tlsis on, even whenrustls-tlsis also enabled. -
An accept with overflowing collateral no longer crashes the node (095caff). The transaction builder added the two parties' collateral with
Amount's+, which panics on overflow. The accepting party chooses its own collateral, so an accept ofu64::MAXagainst any offer took the node down. The sum is now checked, and an overflow is rejected as an invalid accept, the same as collateral that doesn't add up to the total. -
No more OpenSSL in the lockfile (dadc888).
testenvnow downloads embedded Postgres throughpostgresql_embedded'srustlsfeature. This was the last path to OpenSSL, through reqwest 0.12 viapostgresql_archive.Cargo.lockno longer containsnative-tls,openssl,openssl-sysorhyper-tls. -
Dependency advisories resolved for
cargo audit:quinn-proto0.11.15 → 0.11.19 fixes RUSTSEC-2026-0338, -0339, -0340, -0341 and -0342 (b5d6414, #234). It only entered the lockfile through reqwest's optional http3 support, which nothing in the workspace enables.bitcoind/electrsd0.41 → 0.43.2 (38d67be). bitcoind'sdownloadbuild script no longer depends onzip0.6.6 (RUSTSEC-2026-0367, -0368, -0369).negentropy0.5.0 → 0.5.2, pulled in throughnostr(RUSTSEC-2026-0365) (38d67be).
✨ New Features
Payout script overrides (#224)
-
PayoutScriptOverridesTLV record inddk-messages(64c1492). An offer can name outcomes whose CETs pay a script in the accepting party's place: either an outcome of an enum descriptor, or a range of values of a numeric descriptor. The type number is 65005, agreed with node-dlc. It is odd, so a peer that doesn't know the record passes it through and then fails signature verification, which is the safe failure. The encoding is pinned byte for byte for node-dlc to match. -
contract::cet_recordsextension point inddk-manager(dbecd57). ACetRecordis a TLV record on the offer with two methods:validateruns when the offer arrives, before any accept exists.applyrewrites the built CETs once both parties are known.
CET_RECORDSis the registry of known record decoders, listed in the order they apply. To add a record type, define it inddk-messages, implementCetRecord, and add it to the registry. The module documents what a record may change and the fee it must stay within. The builder applies records once for every path, and both the manager and the stateless module validate them when an offer arrives. -
How
PayoutScriptOverrideschanges the CETs (dbecd57). For each CET an override covers, the output that would have paid the accepting party pays the override script instead, in every contract info the override matches.- An enum override covers its outcome's CET.
- A numeric override covers every CET whose values fall in its range. When oracles use different numbers of digits, the last CET also settles every value above the curve that oracles with more digits can attest, so its range extends to the largest of those.
- CETs are rebuilt with
create_cetrather than patched by matching scripts, so the offering party's output is kept even when both parties pay out to the same script. - These are invalid offers:
- an override that covers no CET
- an empty script
- a range that starts after it ends
- a range that covers only part of a CET's values
- two overrides covering the same CET
- an override covering a CET whose accepting-party payout is below the dust limit, which would silently pay the script nothing
-
Override scripts may be longer than the accepting party's script (dbecd57), for example a liquidator's taproot address with a P2WPKH lender. A
CetRecordreports the longest script it pays in the accepting party's place. Funding then reserves the CET fee for the accepting party's output at the longer of that script and the party's own. Whoever pays for that output covers the difference: the accepting party when both parties fund, or the offering party when it funds the whole contract. -
ddk-dlc::DUST_LIMITis now public (dbecd57). The manager's contract input uses it instead of keeping its own copy.
🐛 Bug Fixes
- Contracts with several contract infos can now settle on any of them (08d6a01). A contract's CETs for all its contract infos are stored in one list, one info after another, and each contract info numbers its CETs from zero. The manager used those per-info indexes against the whole list in three places: when the offering party signed adaptor signatures, when the accepting party verified them, and when either party picked the CET to settle with. Both sides made the same mistake, so the signatures verified, but a contract could only ever settle on its first contract info. Otherwise the CET didn't match the decrypted signature, or the first contract info's CET was broadcast in its place. Each of these sites now works on the contract info's own slice of CETs. To support this, the contract info types now implement
PartialEq. MemoryOraclereports an event it hasn't signed as an error (08d6a01). It used to return an attestation with no signatures. That attestation validated, so the periodic check stopped at the first contract info. It now returns an error, as a networked oracle does.- Payout override tests settle on an overridden outcome (8fad012). The tests used to attest a random outcome, so half the runs settled on a CET the override didn't touch. They now attest an overridden outcome, settle it from each side, and check that the CET pays the whole collateral to the third party.
🔧 Other Notable Changes
- One transaction builder (79dfdde). Accepting an offer, signing an accept and the stateless contract module each built the funding transaction, CETs and refund transaction separately. That was three copies of logic both parties must agree on byte for byte.
contract::transactions::build_contract_transactionsis now the only builder. It returns the transactions together with each contract info's CET range. It also owns the checks the copies made or skipped: no contract info, no CET, and collaterals that don't add up to the offer's total. ItsBuildErrorkeeps these validation failures separate from construction failures, so the stateless module still reports an invalid offer or invalid accept rather than a generic DLC error. - Pinned contract transaction fixtures (8cae29a). Transaction digests are replayed from pinned offer and accept messages for each contract shape: enum and numerical, dual and single funded, disjoint, spliced in, and with the refund-to-accepter flag. They confirm that the builder refactor changes no transaction of an existing contract.
- Broader execution tests (08d6a01, dbecd57):
- Disjoint contracts whose first contract info never attests are settled from both sides, both by hand and by the periodic check, with the second contract info as either enum or numerical.
- Every close path checks that the broadcast CET belongs to the attested contract info.
- Override tests cover enum and numeric settlement through both the stateless module and the manager, the taproot-for-P2WPKH fee case, and each rejection.
⬆️ Upgrading
- Stored contracts need no migration (08d6a01). The per-contract-info CET slices are worked out from each contract info's payout count, and adaptor signature offsets from its adaptor info. Nothing new is serialized.
- Existing contracts build the same transactions (79dfdde, 8cae29a). For every contract info after the first, the CETs keep a locktime of zero and their copy of the first CET's input, which is how every stored disjoint contract was built. The pinned fixtures hold the new builder to this.
- Offers without CET records are unchanged on the wire and on chain (dbecd57). An offer without records builds and funds exactly what it did in rc.8.
- Peers that don't support payout overrides (64c1492). Record type 65005 is odd, so an older peer or a node-dlc version without support passes it through and then fails signature verification instead of signing CETs that differ from yours.
- Contracts with more than one contract info (08d6a01). The fix changes which CET each adaptor signature is made and checked against. Upgrade both parties before offering or accepting new contracts with more than one contract info.
- Applications that put their own TLV records on offers (dbecd57) must allow those record types with
Manager::with_allowed_offer_tlv_typesorset_allowed_offer_tlv_types. Otherwise those offers are rejected.
📦 Installation
Add DDK to your Cargo.toml:
[dependencies]
ddk = "2.0.0-rc.9"Other workspace crates can be added directly at the same version, for example:
[dependencies]
ddk-manager = "2.0.0-rc.9"
ddk-messages = "2.0.0-rc.9"
ddk-dlc = "2.0.0-rc.9"