Skip to content

lnwire: pull gossip v2 messages in line with BOLT taproot-gossip review updates#10837

Open
ellemouton wants to merge 10 commits into
lightningnetwork:masterfrom
ellemouton:bolt-1059-spec-updates
Open

lnwire: pull gossip v2 messages in line with BOLT taproot-gossip review updates#10837
ellemouton wants to merge 10 commits into
lightningnetwork:masterfrom
ellemouton:bolt-1059-spec-updates

Conversation

@ellemouton
Copy link
Copy Markdown
Collaborator

Summary

This branch pulls the gossip v2 wire messages in lnwire in line with the
review-driven updates on the BOLT taproot-gossip extension (lightning/bolts#1059).

The spec branch backing these changes is currently at
lightning/bolts#1059 (extension BOLT 7-style document
for gossip v2). The accompanying spec PR linked from each commit groups the
changes by reviewer rationale.

Each commit here is a single, independently-reviewable spec delta and is
written so that make unit pkg=lnwire and go build ./... both pass on its
own. make install builds clean at the tip.

Commits

  • lnwire: bump pure-TLV signed range to 0..=239; sig TLVs to type 240
    align with BOLT 12's renumbering. Signed range expands from 0..=159 to
    0..=239 and the signature TLVs in channel_announcement_2,
    channel_update_2 and node_announcement_2 move from type 160 to 240.
  • lnwire: enforce compulsory-field presence on gossip v2 reader
    add an AssertRequiredPresent helper and use it in the three gossip v2
    decoders so messages missing required TLVs are rejected up front.
  • lnwire: reject tor_v3_address with port 0 in node_announcement_2
    extend the existing port-not-zero rule to cover tor v3 addresses too.
  • lnwire: collapse gossip_timestamp_range's two block-height TLVs into
    one
    — replace the split first_block (type 2) / block_height_range
    (type 4) pair with a single BlockHeightRange TLV at type 2 holding
    {u32 first_block_height, tu32 num_blocks}.
  • lnwire: add funding_txid TLV to announcement_signatures_2
    required funding_txid (type 6, sha256-shaped [32]byte) so each
    message names its funding tx explicitly. NewAnnSigs2 and the existing
    channeldb test callers are updated.
  • lnwire: assign inbound-fee TLVs proper types on channel_update_2
    drop the experimental InboundFee at TlvType55555 in favour of two
    defaulted-on-the-wire uint32 records: type 20 inbound_fee_base_msat
    and type 22 inbound_fee_proportional_millionths. Positive-only.
    ChanEdgePolicyFromWire folds the two fields into the existing
    fn.Option[lnwire.Fee] contract (None when both are 0).
  • lnwire: carry two raw musig2 partial sigs in announcement_signatures_2
    — introduce an AnnouncementSigPair value type (node || bitcoin, 64
    bytes) and switch the PartialSignature field to it. The previously
    pre-aggregated 32-byte form is gone; receivers can now verify each half
    with the standard MuSig2 PartialSigVerify routine.
  • graph/db/models: clarify inbound-fee comment in ChanEdgePolicyFromWire
    — tiny follow-up; the inbound-fee TLVs are defaulted on the wire, not
    required.
  • lnwire: encode channel_update_2 direction in short_channel_id via
    sciddir
    — switch the short_channel_id field to BOLT 1's
    sciddir_or_pubkey type constrained to the 9-byte sciddir form
    (<dirbyte><scid>). The separate second_peer TLV at type 8 becomes
    redundant and is removed; IsNode1() now derives from the direction
    byte.

Out of scope (deliberately)

Test plan

  • make unit pkg=lnwire passes
  • make unit pkg=channeldb passes (the only other unit suite that
    builds AnnounceSignatures2 directly)
  • make unit pkg=graph/db passes (touches ChanEdgePolicyFromWire)
  • make unit pkg=discovery passes (gossip 2 consumers)
  • make install builds clean at HEAD
  • CI green

The taproot-gossip BOLT extension was updated to align with BOLT 12,
which long since moved its signature TLVs to type 240 and widened the
signed TLV range from 0..=159 to 0..=239. Mirror that here:

  * pureTLVUnsignedRangeOneStart: 160 -> 240
  * channel_announcement_2.Signature: TlvType160 -> TlvType240
  * channel_update_2.Signature: TlvType160 -> TlvType240
  * node_announcement_2.Signature: TlvType160 -> TlvType240

Update the pure-TLV test fixtures and the test message types
correspondingly.
The BOLT taproot-gossip spec now requires each gossip v2 reader to
reject messages that are missing any of their compulsory fields with
a warning/close/ignore action. lnwire's three gossip v2 Decode
implementations silently zero-valued the missing TLVs, which made
later validation harder.

Add a small AssertRequiredPresent helper to pure_tlv.go and call it
from each Decode after DecodeWithParsedTypesP2P / ExtractRecords:

  * channel_announcement_2: short_channel_id, outpoint, capacity,
    node_id_1, node_id_2, signature.
  * channel_update_2: short_channel_id, block_height, signature.
  * node_announcement_2: features, block_height, node_id, signature.
The BOLT taproot-gossip spec was tightened so that the
port-not-zero rule applies to every advertised address type,
including tor_v3_address. Mirror that in the lnwire
encoder/decoder for tor v3 addresses in node_announcement_2:
fail encoding if the sender tries to emit a tor v3 address with
port 0, and fail decoding if a received message contains one.
The taproot-gossip BOLT extension swapped the two-TLV
(first_block @ type 2, block_height_range @ type 4) layout in
gossip_timestamp_range for a single TLV at type 2 holding both
fields: u32 first_block_height and tu32 num_blocks. The two fields
are always set together, and the truncated u32 num_blocks saves a
few bytes on the wire.

Replace the FirstBlockHeight (type 2, u32) and BlockRange (type 4,
u32) optional records on GossipTimestampRange with a single
BlockHeightRange (type 2) optional record holding a new
BlockHeightRange struct. The struct's Record() method uses
MakeDynamicRecord with EUint32T+ETUint32T for the encoder and
DUint32+DTUint32 for the decoder.

The property-test factory in test_message.go is updated to draw a
single optional BlockHeightRange instead of two independent fields.
The taproot-gossip BOLT extension added a required funding_txid TLV
(type 6, sha256) to announcement_signatures_2 so each message names
its funding transaction explicitly. For an initial channel open this
is the original funding tx; for a spliced channel it is the txid of
the splice transaction that triggered the new round of announcement
signing. Using funding_txid directly (rather than inferring it via
short_channel_id) is what makes splice announcements unambiguous
when multiple candidate funding transactions may exist at different
points.

Add the field on AnnounceSignatures2 (encoded as [32]byte, to follow
the same primitive-encoding pattern channel_announcement_2 uses for
its chain hash), thread it through NewAnnSigs2, AllRecords and the
Decode presence assertion, and update the test fixtures + the rapid
property-test factory to set it.

The two channeldb waitingproof tests that build AnnounceSignatures2
via NewAnnSigs2 are updated to pass a placeholder funding_txid.
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request updates the lnwire gossip v2 implementation to align with the latest specifications from the BOLT taproot-gossip extension. The changes focus on protocol consistency, improved message validation, and refining data structures for better interoperability and compliance with the evolving gossip v2 standard.

Highlights

  • Gossip v2 Protocol Alignment: Updated wire messages to align with BOLT taproot-gossip review updates, including renumbering signed ranges to 0..=239 and moving signature TLVs to type 240.
  • Improved Validation: Introduced an AssertRequiredPresent helper to ensure compulsory TLVs are present in gossip v2 messages and added validation to reject tor_v3_address with port 0.
  • Data Structure Refinements: Collapsed block-height TLVs in gossip_timestamp_range, added funding_txid to announcement_signatures_2, and introduced Sciddir to encode channel direction directly in short_channel_id.
  • MuSig2 Updates: Refactored announcement_signatures_2 to carry two raw MuSig2 partial signatures instead of a pre-aggregated form, allowing for independent verification.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label May 23, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Taproot gossip implementation to match the latest BOLT specifications. Major changes include transitioning AnnounceSignatures2 to use AnnouncementSigPair and FundingTxID, adopting the Sciddir format in ChannelUpdate2 to replace the SecondPeer flag, and splitting inbound fees into distinct base and proportional TLV records. Additionally, the signature TLV type was moved to 240, and mandatory TLV validation was added. Review feedback highlighted a potential integer overflow when casting uint32 inbound fees to int32 in the database models. Furthermore, several new encoder/decoder functions and exported constructors require more comprehensive documentation to comply with the repository's style guide.

Comment thread graph/db/models/channel_edge_policy.go Outdated
Comment on lines +137 to +140
BaseFee: int32(upd.InboundFeeBaseMsat.Val),
FeeRate: int32(
upd.InboundFeeProportionalMillionths.Val,
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cast from uint32 to int32 for inbound fees is potentially unsafe. While the PR description mentions these are "positive-only", a uint32 value exceeding math.MaxInt32 (approx. 2.1 billion) will result in a negative value when cast to int32. Given that inbound_fee_base_msat is in millisatoshis, a value of 2.1 million satoshis is plausible for a base fee. We should ensure the value fits within the destination type's range or use a larger type if lnwire.Fee allows it.

Comment on lines +80 to +81
func blockHeightRangeEncoder(w io.Writer, val interface{},
buf *[8]byte) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing function comment for blockHeightRangeEncoder.

Suggested change
func blockHeightRangeEncoder(w io.Writer, val interface{},
buf *[8]byte) error {
// blockHeightRangeEncoder is a custom TLV encoder for the BlockHeightRange
// type.
func blockHeightRangeEncoder(w io.Writer, val interface{},
buf *[8]byte) error {
References
  1. Every function must be commented with its purpose and assumptions, beginning with the function name. (link)

Comment on lines +95 to +96
func blockHeightRangeDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing function comment for blockHeightRangeDecoder.

Suggested change
func blockHeightRangeDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
// blockHeightRangeDecoder is a custom TLV decoder for the BlockHeightRange
// type.
func blockHeightRangeDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
References
  1. Every function must be commented with its purpose and assumptions, beginning with the function name. (link)

Comment thread lnwire/sciddir.go
)
}

func sciddirEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing function comment for sciddirEncoder.

Suggested change
func sciddirEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
// sciddirEncoder is a custom TLV encoder for the Sciddir type.
func sciddirEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
References
  1. Every function must be commented with its purpose and assumptions, beginning with the function name. (link)

Comment thread lnwire/sciddir.go
Comment on lines +76 to +77
func sciddirDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing function comment for sciddirDecoder.

Suggested change
func sciddirDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
// sciddirDecoder is a custom TLV decoder for the Sciddir type.
func sciddirDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
References
  1. Every function must be commented with its purpose and assumptions, beginning with the function name. (link)

Comment thread lnwire/partial_sig.go Outdated
Comment on lines +274 to +275
// NewAnnouncementSigPair creates a new AnnouncementSigPair.
func NewAnnouncementSigPair(node, bitcoin btcec.ModNScalar) AnnouncementSigPair {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment for NewAnnouncementSigPair is sparse. Per the LND Style Guide, exported functions require detailed comments for the caller.

// NewAnnouncementSigPair creates a new AnnouncementSigPair from the provided
// node and bitcoin partial signatures.
func NewAnnouncementSigPair(node, bitcoin btcec.ModNScalar) AnnouncementSigPair {
References
  1. Exported functions require detailed comments for the caller. (link)

Comment thread lnwire/partial_sig.go
Comment on lines +291 to +292
func announcementSigPairEncoder(w io.Writer, val interface{},
_ *[8]byte) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing function comment for announcementSigPairEncoder.

Suggested change
func announcementSigPairEncoder(w io.Writer, val interface{},
_ *[8]byte) error {
// announcementSigPairEncoder is a custom TLV encoder for the
// AnnouncementSigPair type.
func announcementSigPairEncoder(w io.Writer, val interface{},
_ *[8]byte) error {
References
  1. Every function must be commented with its purpose and assumptions, beginning with the function name. (link)

Comment thread lnwire/partial_sig.go
Comment on lines +311 to +312
func announcementSigPairDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing function comment for announcementSigPairDecoder.

Suggested change
func announcementSigPairDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
// announcementSigPairDecoder is a custom TLV decoder for the
// AnnouncementSigPair type.
func announcementSigPairDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
References
  1. Every function must be commented with its purpose and assumptions, beginning with the function name. (link)

@github-actions
Copy link
Copy Markdown

🔴 PR Severity: CRITICAL

Automated classification | 10 files | 584 lines changed (excl. tests)

🔴 Critical (9 files) — lnwire/*
  • lnwire/announcement_signatures_2.go - Lightning wire protocol: announcement signatures message
  • lnwire/channel_announcement_2.go - Lightning wire protocol: channel announcement message
  • lnwire/channel_update_2.go - Lightning wire protocol: channel update message (106 lines changed)
  • lnwire/gossip_timestamp_range.go - Lightning wire protocol: gossip timestamp range message (102 lines changed)
  • lnwire/node_announcement_2.go - Lightning wire protocol: node announcement message
  • lnwire/partial_sig.go - Lightning wire protocol: partial signature handling (83 additions)
  • lnwire/pure_tlv.go - Lightning wire protocol: TLV encoding
  • lnwire/sciddir.go - Lightning wire protocol: SCID direction (new file, 100 additions)
  • lnwire/test_message.go - Lightning wire protocol: test message helper (in lnwire package)
🟠 High (1 file) — graph/*
  • graph/db/models/channel_edge_policy.go - Network graph: channel edge policy model

Analysis

This PR modifies multiple files in the lnwire package, which implements the core Lightning Network wire protocol messages. Changes span announcement signatures, channel announcements, channel updates, gossip timestamp range, node announcements, partial signatures, TLV encoding, and adds a new sciddir.go for SCID direction handling. The lnwire package is CRITICAL as it defines fundamental protocol message types used throughout the daemon.

A line-count bump also applies: 584 non-test lines changed exceeds the 500-line threshold. The base classification is already CRITICAL so no further escalation occurs.

The breadth of changes across multiple lnwire v2 gossip message types suggests this is part of the gossip v2 / Taproot Channels initiative. Protocol-level serialization and deserialization changes require careful review for correctness and compatibility.


To override, add a severity-override-{critical,high,medium,low} label.
<!-- pr-severity-bot -->

The taproot-gossip BOLT extension assigned the experimental
inbound-fee field on channel_update_2 a real TLV layout: two
separate uint32 records, type 20 (inbound_fee_base_msat) and type 22
(inbound_fee_proportional_millionths), both positive-only with a
default of 0. Pull the implementation in line:

- Replace the experimental InboundFee OptionalRecordT[TlvType55555,
  Fee] (which had a long-standing TODO to assign a real type) with
  two required uint32 RecordTs at types 20 and 22.
- Suppress on encode when 0 and default-fill on decode, matching how
  the surrounding fee/htlc fields behave.
- Update ChanEdgePolicyFromWire for ChannelUpdate2 to fold the two
  uint32 values into the existing fn.Option[lnwire.Fee] downstream
  contract: emit None when both are 0, Some otherwise. The Fee
  struct itself still uses int32 for the legacy ChannelUpdate1 case;
  ChannelUpdate2 inbound fees can only be non-negative so the
  uint32->int32 widening is safe in practice (and a v2 sender can't
  encode a negative fee anyway).
- Update the channel_update_2 test fixture to carry valid type-20
  and type-22 records, and move the previously-unknown extra TLV out
  of slot 20 to slot 24.
- Update the rapid property factory to draw the two new uint32
  fields from a non-zero range when including an inbound fee.
The taproot-gossip BOLT extension dropped the previously pre-aggregated
32-byte partial signature on announcement_signatures_2 in favour of
emitting both raw musig2 partial sigs back-to-back -- one for the
node_id key and one for the bitcoin key -- so the receiver can verify
each half with the standard MuSig2 PartialSigVerify routine instead
of the custom verifier the old layout required.

Introduce an AnnouncementSigPair value type in partial_sig.go that
encodes as `node || bitcoin` for a fixed 64 bytes, with a static-
record builder via tlv.MakeStaticRecord. Swap announcement_signatures_2's
PartialSignature (TlvType4, PartialSig) field for a new
PartialSignatures (TlvType4, AnnouncementSigPair) field; update
NewAnnSigs2 to take the pair; update the hardcoded test fixture
(length 0x20 -> 0x40, 32 bytes -> 64 bytes of zero padding); update
the rapid property factory to draw two independent partial sigs;
and update the three channeldb waitingproof tests that build
AnnounceSignatures2 directly.

The existing 32-byte PartialSig type stays in place for the
co-operative close flow and other call-sites that don't carry both
sigs at once.
The channel_update_2 inbound-fee TLVs are not "required" -- they
follow the same defaulted-on-the-wire pattern as fee_base_msat and
friends, where the field is always present in the Go struct (a
uint32 with the default-fill applied on decode) and suppressed from
the wire when it equals the default of 0. Reword the comment to
avoid implying that a sender MUST emit them.

No code change.
…ddir

The taproot-gossip BOLT extension switched
channel_update_2.short_channel_id from a plain 8-byte SCID to BOLT 1's
sciddir_or_pubkey type constrained to the sciddir form: a 9-byte
<dirbyte><scid> encoding where the direction byte is 0 for node_id_1
and 1 for node_id_2. With the direction now part of the scid itself,
the previously-separate type-8 second_peer flag TLV is fully
redundant and is removed.

Add a Sciddir value type in lnwire/sciddir.go with a 9-byte static
record (custom encoder/decoder that rejects any direction byte other
than 0 or 1, so we can never accidentally accept the pubkey form of
sciddir_or_pubkey here).

In ChannelUpdate2:

  * ShortChannelID now wraps a Sciddir instead of a ShortChannelID.
  * The SecondPeer OptionalRecordT and all its Decode/AllRecords
    wiring is removed.
  * IsNode1() now derives from the dir byte (`Direction == 0`).
  * SCID() projects the 8-byte scid portion so the ChannelUpdate
    interface stays unchanged for callers.
  * SetSCID() updates the scid portion while leaving the existing
    dir byte in place.

ChanEdgePolicyFromWire now derives ChannelEdgePolicy.SecondPeer from
!upd.IsNode1() instead of the old upd.SecondPeer.IsSome() lookup.
The downstream SecondPeer field on ChannelEdgePolicy stays the same
shape since it is also used by ChannelUpdate1.

The channel_update_2 test fixture now carries a 9-byte sciddir at
type 2 and no longer has a type-8 SecondPeer record. The rapid
property factory draws a single bool for the direction byte instead
of separately drawing an isSecondPeer flag for the (now removed)
second_peer field.
…proot-gossip

The branch behind this PR pulls the lnwire gossip v2 messages in line
with the review-driven updates on the BOLT taproot-gossip extension
(lightning/bolts#1059). Record the change in the 0.22.0 release notes
under "BOLT Spec Updates" so the PR check is satisfied and downstream
implementors get a heads-up about the wire-format shifts.
@ellemouton ellemouton force-pushed the bolt-1059-spec-updates branch from 834c03d to 64acef0 Compare May 24, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

severity-critical Requires expert review - security/consensus critical

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant