Repository navigation
Conversation
Owner
Author
|
Note 🤖 Claude Opus 5.5 responding on behalf of bennyhodl Review against orange-grove's consumers ( The structure-versus-policy split is right, and it would replace about 70 lines of offer checks in
|
…info `get_contract_info_and_announcements` subtracted each enum payout from the total collateral unchecked, so a payout above the total panicked, in release builds too. A numeric payout function with no pieces converted fine and then hit an `expect` in `PayoutFunction::validate`. Both come from the counterparty's offer. The subtraction is now checked and the payout function conversion is a `TryFrom` that rejects an empty function, both returning `InvalidParameters`.
…ming `OfferDlc::validate` mixed the protocol rules with the receiver's timeout bounds and clock, so a party with no timeout policy could not check an offer at all. It is now three checks: - `validate_announcements`: oracle signatures and events, the receiving party's check on oracles it did not choose; - `validate_terms`: the message-only protocol rules; - `validate_timing`: the refund window and maturity. `validate_terms` also covers the offer_dlc MUSTs that went unchecked: standard payout and change scripts, CET and refund locktimes in one unit and in order, unique funding input serial ids, distinct change and fund output serial ids, the fee rate, and the collateral. A zero CET locktime passes against either unit: it does not lock the CETs. The collateral rule moves to `ddk_dlc::util::validate_collateral` and `ContractInput::validate` uses it instead of its own dust constant. It keeps DDK's allowance for zero offer collateral. The manager checks its own offers with `validate_terms`, where it only checked the CET locktime before.
`validate_offer` needed timeout bounds and a clock, so a stateless consumer that only stores or relays offers had to re-implement the structural checks. `validate_offer_structure(offer)` runs the oracle announcement checks, the protocol version, `OfferDlc::validate_terms`, splice input consistency, and the execution information (payouts cover every attestable outcome). It does not decode ordinary funding inputs, and says so. `validate_offer` is now that plus `OfferDlc::validate_timing`. `create_offer` runs the same checks except the announcements, through one private `validate_offer_terms`, instead of its own copies of the fee rate, collateral, serial id, locktime and execution information checks. The offering party chose its oracles; checking announcements is the receiver's job, and creation never did it. Whether one-nonce-per- outcome oracles are supported is still open (#225 item 7), and a test pins that creation accepts them while validation rejects them. `build_context` shares one `execution_infos` helper with validation.
Under fuzztarget every serial id and temporary id came from a freshly seeded RNG, so each draw returned the same value. An offer built with the feature then had equal change and fund output serial ids, which offer validation now rejects. The generator keeps its fixed seed but lives for the thread, so a run stays reproducible and its ids differ.
`create_offer` skipped the oracle announcement check, so it built offers over an enum event with one nonce per outcome that `validate_offer_structure` then rejected. An offer the counterparty refuses is never worth sending, so per-outcome nonces are now refused on both sides: `create_offer` runs `validate_offer_structure` itself, and the manager's `offer_contract` checks each announcement with the same `OracleAnnouncement::validate` before it reserves any wallet input. `OracleEvent::validate` stays the one owner of the one-nonce-per-enum-event rule. The test that recorded the old behaviour is replaced by one that asserts creation refuses such an announcement, and the manager gains the matching test. Zero offer collateral and a zero CET locktime stay allowed. They are valid DLCs (one-sided funding, CETs that are not time-locked); refusing them is application policy. The docs of `validate_offer_structure` now say so, and its test pins the zero CET locktime next to the zero collateral.
bennyhodl
force-pushed
the
benny/225-validate-offer-structure
branch
from
October 15, 2026 18:34
3bcf799 to
4e8beaa
Compare
A party that stores, relays or audits an accept had no single call that checked it; the rules lived inline in ddk's context building, and the manager checked fewer of them. The accept now has one owner per rule, split the way the offer's are. `AcceptDlc::validate_terms` in dlc-messages holds the message-only rules: the accept answers this offer (protocol version, temporary contract id), carries no negotiation fields, has standard payout and change scripts, uses funding input serial ids distinct across both parties, and has a change serial id that differs from the fund output's. It shares the script and serial-id helpers with `OfferDlc::validate_terms`. `ddk::contract::validate_accept` runs `validate_offer_structure`, the accept's terms, then rebuilds the contract and verifies the accepting party's refund and CET adaptor signatures. `sign_accept` signs from that same verified context, so the two cannot drift. ddk's context building and `accept_offer` call `validate_terms` in place of their own checks, and the manager's `verify_accepted_and_sign_contract` calls it too. The collateral rule stays in `build_contract_transactions`: the builder also runs on party parameters that never came in an accept message (the accepting side, before its accept exists), and every path that builds a contract passes through it. The manager now rejects accepts it used to take: one whose protocol version differs from the offer's, one with negotiation fields, one with a non-standard payout or change script, one whose funding input serial id repeats another input's, and one whose change serial id equals the fund output serial id.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Item 8 of #225.
validate_offer(offer, min_timeout_interval, max_timeout_interval, now_unix)bundled structural checks with the accepting party's timeout policy and clock. Lygos re-implemented the structural half. This PR splits them, with one place per check.API
ddk::contract::validate_offer_structure(offer)checks:It doesn't decode ordinary funding inputs, so it checks neither their values nor the spec's SegWit rule. The docs say so.
validate_offer=validate_offer_structure+ the timeout and maturity policy. Its signature is unchanged.OfferDlc::validateis split intovalidate_announcements,validate_termsandvalidate_timing.validateruns all three and keeps its signature.ddk_dlc::util::validate_collateral(offer ≤ total, total ≥ 1000 sats, offer collateral 0 or ≥ 1000 sats) is shared withContractInput::validatein ddk-manager.create_offerdrops its own copies of these checks and runs the shared policy-free checks on the offer it built.Creation doesn't validate announcements
create_offer, and the manager's own outgoing offers, skip announcement validation, as master did. The offering party chose its own oracles; validating announcements is the receiver's check. This also keeps Lygos's offers building: its oracle commits one nonce per enum outcome, whichOracleEvent::validaterejects. Whether DDK supports that convention is item 7 of #225, which is undecided. A test pins the difference.Fixes found along the way
ddk-manager/src/conversion_utils.rssubtracted an enum payout from total collateral unchecked, andexpected a non-empty numeric payout function. A crafted offer panickedaccept_offeron master; this PR would also have exposed that panic throughvalidate_offer. Both cases now returnInvalidParameters. The payout-function conversion isTryFrominstead ofFrom.Behaviour changes
accept_offerand the manager'son_offer_message, are now rejected for:These are protocol requirements in the DLC spec.
cet_locktime = 0passes against either locktime unit (it locks nothing, and an existing test relies on it);create_offernow returnInvalidOffer; they used to returnInvalidFundingInput.Testing
offer_structure_is_validated_without_a_timeout_policy: acreate_offeroffer passes, and nine structural defects are each rejected.validate_offer_adds_the_timeout_policy_to_the_structureonly_the_receiver_checks_the_announcementsunrepresentable_payouts_are_rejected: an enum payout above total and an empty numeric curve return errors from bothvalidate_offer_structureandaccept_offer, with no panic.manager_tests5 andcontract_updater1, all passing.stateless_executionignored tests and 5 of the 80manager_executiontests passed on regtest. The fixes don't touch the funding or settlement path.cargo fmt --check,cargo clippy -- -D warningsandcargo check -p ddk --no-default-featurespass.