smite: add accept_channel oracle - #185
Conversation
ekzyis
left a comment
There was a problem hiding this comment.
I was working on using #186 to verify the upfront_shutdown_script TLV and I saw this PR is related. I like the idea of oracles to localize code to detect protocol invariants! I only took a quick look for now. Left some comments regarding the design.
There was a problem hiding this comment.
I see that this file was already defined, but is there a reason why not update the return type of evaluate from OracleResult to Result<(), Violation>?
The documentation of Violation mentions this:
//! Each [`Violation`] variant names a buggy target behavior (e.g., crashing,
//! hanging, breaking a protocol invariant). This is how the fuzzer reports bugs
//! in the target.
The documentation of oracles.rs also mentions protocol invariants, so it seems like the perfect match to me. I think this would make the evaluate_accept_channel_oracle function obsolete.
Not sure how useful this would be, but we could then even consider oracles to return all violations instead of only the first one.
There was a problem hiding this comment.
I don't have a strong opinion here and can certainly change OracleResult to Result<(), Violation>, though I do prefer a separate block for oracle evaluation and hence would like to keep evaluate_accept_channel_oracle
Not sure how useful this would be, but we could then even consider oracles to return all violations instead of only the first one.
How will this work? Do you mean each oracle failure is only logged, or we store each oracle fail in a Vec and then output it?
There was a problem hiding this comment.
I also think Result<(), Violation> would be simpler.
I would rather not have oracles return multiple violations though -- it's simpler to just return the first one. Realistically I would be surprised (in a bad way) if multiple violations are common, and even then we should be able to find the next violation after the first one is fixed.
| // The opener cannot afford the fee, so the acceptor must not send | ||
| // `funding_signed`. Receiving one is a protocol violation. | ||
| if !state.config.can_opener_afford_feerate(&state.commitment) { | ||
| return Err(Violation::OpenerCannotAffordFee(fs.channel_id)); | ||
| } | ||
|
|
There was a problem hiding this comment.
verify_funding_signed can still throw a Violation. Is the plan to replace verify_funding_signed with an oracle? I expected all Violations to be thrown in oracles.
There was a problem hiding this comment.
Yup, that is originally my next plan to migrate those to oracles as well
| /// Panics if no matching `open_channel` exists. This should be unreachable, as | ||
| /// `evaluate_accept_channel_oracle` reports such messages as a [`Violation`]. |
There was a problem hiding this comment.
Mhh, would be nice if we could couple oracle evaluation ("verification") with receiving or recording.
I think conceptually, verification is closer to receiving. If we would move it there, we would still need to document a possible panic here, but if the only function that receives also verifies, and we use naming conventions like recv_<message> and record_<message> for the functions we call in the matching arm of Executor::execute(), it would be easier to parse the code and have confidence in no panics.
(Or we move verification to recording and then we don't need to panic.)
There was a problem hiding this comment.
I do prefer all three to be separate, we already follow the same idea currently with funding_signed -- receive and then verify instead of clubbing them together (for logging purposes as well)
I can certainly club verify with record, but I actually wanted to keep the oracle evaluation blocks separate as // -- Oracle evaluation --, so we can put all the oracle evaluation there, which is easier to understand. Then we can follow a pattern like recv -> verify oracle -> record
morehouse
left a comment
There was a problem hiding this comment.
My main concern is that we might get false positives for channel types we haven't implemented yet (i.e. taproot, zero-fee commits). Please double check that and add comments explaining why we're safe (if we are).
Otherwise this is great -- I have only minor comments inline.
Also things that we should consider adding in follow-up PRs:
open_channel expected failures
zero_fee_commitments && max_accepted_htlcs > 114zero_fee_commitments && feerate_per_kw != 0- wumbo limit exceeded
- maybe
feerate_per_kw == 0? - maybe
channel_reserve_satoshis >= funding_satoshis - maybe
chain_hash != regtestif we add non-regtest chain hashes to the IR - maybe
announce_channel && option_scid_alias - maybe absurd
dust_limit_satoshisvalues (e.g., more than 10,000 sat?)
accept_channel violations
channel_type != open_channel.channel_typeoption_zeroconf && minimum_depth > 0- invalid
upfront_shutdown_script zero_fee_commitments && max_accepted_htlcs > 114- maybe when any pubkeys match a pubkey from
open_channel - maybe
per_commitment_pointreuse -- requires maintaining a global PCP set. But we'll want to maintain that set anyway for commitment fuzzing - maybe
to_self_delay == 0 - maybe
htlc_minimum_msat > max_htlc_value_in_flight_msat - maybe
htlc_minimum_msat > open_channel.funding_satoshis - maybe
max_accepted_channels == 0
| // Check that the channel type was included. | ||
| let Some(channel_type) = open_channel.tlvs.channel_type.as_deref() else { | ||
| return Some("open_channel does not include a channel_type".to_string()); | ||
| }; |
There was a problem hiding this comment.
I know the spec says the receiver must fail in this case, but I wouldn't be surprised if there's still an implementation that allows the old "implicit" negotiation...
There was a problem hiding this comment.
I think then that's a non-spec-compliant bug, I will add a TODO to check option_channel_type in the negotiated features since it is assumed to be supported
Required when moving fee checks to accept_channel oracle where we wouldn't need to construct the whole channel state just to compute the cost. Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Define `AcceptChannelOracle` and migrate existing `accept_channel` validation into it, consolidating BOLT 2 v1 channel establishment checks: - verify `temporary_channel_id` maps to a sent `open_channel` - validate funding amounts and channel type presence - check the opener can afford the proposed commitment feerate - detect reused `temporary_channel_id`s before `funding_created` The oracle brings together validation that was previously spread across `record_recv_accept_channel()`, `verify_funding_signed()` and implicit checks. Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
5eca139 to
3fea27c
Compare
|
Had to force-push since there were some design changes
I verified this and added comments inline. For the fee and anchor cost calculations, we might be undercalculating the fee for 0FC or taproot channels, but definitely not overcalculating it. So we will not get false positives, though we might miss some invalid cases. In some cases, we might also hit a I think this will also be resolved once I add support for verifying oracles based on the negotiated features in a follow-up PR. |
morehouse
left a comment
There was a problem hiding this comment.
Ready to squash the fixup.
|
|
||
| /// Verifies that the initial commitment can cover its fee and satisfies the | ||
| /// channel reserve requirement, returning the first requirement it breaches, | ||
| /// or None if both are met. |
| } | ||
|
|
||
| /// Verifies the `accept_channel` against the BOLT 2 requirements it must meet, | ||
| /// returning the one it breaches, or `None` if it meets them all. |
| } | ||
|
|
||
| /// Returns the BOLT 2 requirement our `open_channel` breaches, i.e. the reason | ||
| /// its receiver had to fail the channel instead of accepting it, or `None` if |
| /// Returns `Violation` if an oracle invariant is violated. | ||
| fn evaluate(&self, context: &C) -> Result<(), Violation>; | ||
| /// Return the name of this oracle for logging | ||
| fn name(&self) -> &str; |
There was a problem hiding this comment.
Looks like we can delete name entirely.
| const MAX_ACCEPTED_HTLCS_LIMIT: u16 = 483; | ||
| const MIN_DUST_LIMIT_SATOSHIS: u64 = 354; | ||
|
|
||
| /// `AcceptChannelContext` is the context for the `AcceptChannelOracle` |
There was a problem hiding this comment.
Nit
| /// `AcceptChannelContext` is the context for the `AcceptChannelOracle` | |
| /// Context for `AcceptChannelOracle` |
| pub negotiation: Option<&'a PendingChannel>, | ||
| } | ||
|
|
||
| /// `AcceptChannelOracle` checks whether the `open_channel` answered by an |
There was a problem hiding this comment.
Nit
| /// `AcceptChannelOracle` checks whether the `open_channel` answered by an | |
| /// Checks whether the `open_channel` answered by an |
|
|
||
| #[test] | ||
| fn can_opener_afford_feerate_checks() { | ||
| fn opener_balance_after_fee_sat_checks() { |
There was a problem hiding this comment.
This test name no longer matches the API.
Add
AcceptChannelOracleand consolidate all the previously scattered checks relevant to the initial funding flow negotiation into it, including unknowntemporary_channel_id,temporary_channel_idreuse beforefunding_created, cases where the target accepted an invalidopen_channelwe sent, and cases where the target sent anaccept_channelthat does not follow the BOLT 2open_channel <-> accept_channelrequirementsNote: Some checks that depend on negotiated features or
channel_typeare still missing and will be added in a follow-up PR to keep this one focusedAlso, I only added the checks explicitly mentioned in the BOLT 2 and haven't added some obvious checks, such as rejecting
max_accepted_htlcs < 1inopen_channel. I do think those checks are valuable, so I was thinking of adding them in a follow up PR to avoid adding too much here. But let me know if you think all of them (excluding feature-related checks) should be included here.I'm also planning to add BOLT 9 feature flag primitives so feature related logic can be consolidated in one place instead of being scattered across
operation.rs,setup.rs, andAcceptChannelOracle. The remaining feature dependent checks will be added to this oracle once that is in place