smite: fuzz the simple taproot channel funding flow - #210
Open
erickcestari wants to merge 16 commits into
Open
Conversation
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>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Contributor
Author
|
I'll make this PR draft for now since it can be refactored using the Features bit primitives of #192 |
erickcestari
marked this pull request as draft
August 19, 2026 13:40
Simple taproot channels carry a 66-byte MuSig2 public nonce and a 98-byte partial signature paired with the nonce that produced it. Both are opaque byte containers rather than parsed points. Decoding must not reject a malformed nonce: a target that sends one is violating the spec, and smite reports that as a finding instead of failing to decode the message.
Key aggregation with the BIP 86 taproot tweak, nonce derivation, partial signing and partial signature verification, for the taproot funding output and commitment signatures. This is the only module that talks to the musig2 crate. That crate pulls its own secp256k1 version, distinct from the one bitcoin re-exports, so keys cross the boundary as serialized bytes and the rest of smite only ever sees bitcoin::secp256k1 types. Nonces are derived deterministically from the signer's funding pubkey and a per-use context rather than from the OS RNG. Seeding from public data would be unacceptable in a real node, but smite runs under Nyx snapshots where a crash must replay from the same input, and it holds throwaway keys and no funds. It also lets the first nonce be derived in open_channel, where only the funding pubkey is in scope. Key aggregation is checked against the funding test vector in bolt-simple-taproot.md.
next_local_nonce (type 4) on open_channel, accept_channel and channel_ready, and partial_signature_with_nonce (type 2) on funding_created and funding_signed. funding_created and funding_signed had no TLV stream at all, so trailing bytes were silently dropped; they now decode one. Both TLV types are even, so each message that accepts one must whitelist it as known, and channel_ready's stream had to move off the reject-all-even path. Nothing emits these yet. The executor and oracle changes here only keep their fixtures compiling against the new fields.
erickcestari
force-pushed
the
simple-taproot-channels
branch
from
August 20, 2026 13:47
d59e0b2 to
97a57a2
Compare
The commitment format follows from the bits set in the negotiated channel_type. Name that mapping in two predicates: is_simple_taproot on ChannelConfig, which the funding output is about to need as well, and has_anchor_outputs for the commitment itself. has_anchor_outputs is separate from the option_anchors bit because simple taproot channels carry anchors without setting the anchor bits. The commitment tests build their channel_type from Features rather than raw byte vectors while here.
A simple taproot channel funds a single P2TR output keyed on the MuSig2 aggregate of both funding pubkeys, instead of a 2-of-2 P2WSH. build_funding_transaction and matches_funding_output take the channel_type and pick the format from it. The aggregate key is already BIP 86 tweaked by key aggregation, so the output assumes it is tweaked rather than tweaking again, which would produce a key neither peer can sign for. Deriving the key can fail in principle, so funding construction gains an error type rather than panicking on fuzzer-chosen keys. Nothing selects taproot yet: the IR does not carry the channel type into CreateFundingTransaction, so the executor still asks for P2WSH. The output script is checked against the funding test vector in bolt-simple-taproot.md.
Every output becomes P2TR: to_local and to_remote commit to a tapscript tree under the NUMS point so the script path is always taken and the keys inside are revealed on chain, and the anchors key on the party's main output key rather than the funding key, which MuSig2 no longer reveals. The commitment spends the funding output through the taproot key path, so the digest is the BIP 341 one over the funding prevout rather than BIP 143 over a witness script, and the signature becomes a MuSig2 partial signature. ChannelState carries the nonce we gave the counterparty to sign against, so the incoming funding_signed can be verified. Taproot commitments weigh 968 rather than the 1124 of segwit v0 anchors, which changes the fee both peers must agree on. The scripts, tapscript root and output keys are checked against the test vectors in bolt-simple-taproot.md, and a partial signature is round-tripped between both sides of the same session.
The funding output format follows from the negotiated channel type, so the operation that builds the funding transaction has to know it. It becomes a fifth input rather than being inferred, keeping the dependency visible in the program listing and leaving every mutator working unchanged. It must match the type given to BuildOpenChannel; when a mutator makes them disagree the target simply never sees the funding output it negotiated. This changes the operation's arity, and the executor asserts arity exactly, so existing corpora must be regenerated.
open_channel publishes the MuSig2 nonce the peer signs our first commitment against, funding_created zeroes the fixed signature field and carries a partial signature instead, funding_signed is verified against the nonce we published, and channel_ready publishes a fresh nonce to replace the one the funding flow consumed. Each nonce gets a distinct derivation context, since reusing one across two signing sessions would leak the funding key. channel_flags is left exactly as the program specifies, including the announce_channel bit that taproot channels must not set, so mutators can still exercise how targets reject a public taproot channel. A missing or malformed peer nonce is likewise left to the oracles rather than treated as an executor error: it means the peer did not sign what we asked it to.
A target that accepts an open_channel negotiating a taproot channel without a usable MuSig2 nonce, or one that announces the channel, has skipped a check the spec requires it to make. So has one that answers without publishing a nonce of its own. Also drops a stale note about the commitment fee: the initial commitment check now uses the 968 taproot weight, since CommitmentCost derives the weight from the channel type.
Taproot channels cannot be gossiped, so a randomly chosen channel_flags leaves the happy path unreachable for a third of the channel types the funding flow generator picks from. Clear the announce_channel bit for those types. Mutators can still flip it afterwards, which is what exercises how targets reject a public taproot channel.
lnd v0.21.1-beta gates option_simple_taproot behind --protocol.simple-taproot-chans. Without it lnd clears bits 80/81 from its init and answers a taproot channel_type with "requested channel type not supported", so the taproot funding flow can never be exercised. Note that lnd master inverted this to an opt-out --protocol.no-taproot-chans, so this flag will need revisiting when the pinned version moves.
erickcestari
force-pushed
the
simple-taproot-channels
branch
from
August 20, 2026 14:23
97a57a2 to
9feb173
Compare
erickcestari
marked this pull request as ready for review
August 20, 2026 14:23
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.
Adds
option_simple_taprootsupport, from the wire types up to driving a fulltaproot funding flow against a target and asserting what comes back.
The IR could already generate a taproot
channel_type, but the funding outputwas always P2WSH, the commitment always segwit v0, and the MuSig2 TLVs did not
exist, so a target negotiating one never saw a message it could act on.
Opaque byte containers: a malformed nonce must decode, so smite reports it as a
finding instead of failing to parse the message.
partial signing/verification). Sole owner of the
musig2crate, which pulls itsown
secp256k1, so keys cross that boundary as bytes.next_local_nonce(4) onopen_channel/accept_channel/channel_ready,partial_signature_with_nonce(2) onfunding_created/funding_signed. The latter two had no TLV stream at all, silently droppingtrailing bytes.
tweaked by aggregation, so it is not tweaked again.
to_local/to_remoteunder the NUMS point,anchors keyed on the main output key since MuSig2 no longer reveals the funding
key. Spends via the key path, so BIP 341 digest and a partial signature. Weight
968, not 1124, which changes the fee both peers must agree on.
open_channel, partial signature infunding_created(fixed signature field zeroed),funding_signedverifiedagainst the published nonce, fresh nonce in
channel_ready. Distinct derivationcontext per nonce, since reuse across sessions leaks the funding key.
accept_channelflags a target that accepts without a usable nonce,answers without publishing one, or announces the channel.
Coverage report Eclair:
eclair-output-cov-html.tar.gz