lnd/lnwire/intro_node.go
bitromortac 9113a53337 lnwire: add bounded introNode BlindedPath codec
Introduce the canonical lnwire.BlindedPath / BlindedPaths codec with a
sealed IntroductionNode sum-type covering both the BOLT 4 pubkey and
sciddir variants. The codec gates every variable-length subfield against
an io.LimitedReader. It fails closed on the encoder side so invalid
input never hits the wire.

This commit is a pure addition: no existing caller changes. Subsequent
commits migrate OnionMessagePayload and the bolt12 message structs to
consume the new codec.
2026-06-03 12:11:29 +02:00

181 lines
4.5 KiB
Go

package lnwire
import (
"bytes"
"fmt"
"io"
"github.com/btcsuite/btcd/btcec/v2"
)
// IntroductionNode is the sealed sum-type for a blinded path's introduction
// node. {0x02, 0x03} → PubkeyIntro; {0x00, 0x01} → SciddirIntro. The unexported
// method seals the variant set so foreign packages cannot satisfy the interface
// with an unrecognised wire form.
type IntroductionNode interface {
isIntroductionNode()
encodedLen() uint64
encode(w io.Writer) error
// validate checks that the discriminator byte is valid for the variant.
validate() error
// Bytes returns the wire-format encoding of the introduction node for
// callers that need it outside an io.Writer (RPC surfaces).
Bytes() []byte
}
// PubkeyIntro is the 33-byte compressed-pubkey variant. The SEC1 parity byte
// (0x02 or 0x03) doubles as the wire discriminator. Use the constructor to
// ensure the non-nil, on-curve invariant is upheld.
type PubkeyIntro struct {
Pubkey *btcec.PublicKey
}
// NewPubkeyIntro constructs the pubkey introduction-node variant, establishing
// the non-nil, on-curve invariant at construction time so callers receive an
// error up front rather than relying on every method to re-guard a nil key.
func NewPubkeyIntro(pubkey *btcec.PublicKey) (PubkeyIntro, error) {
p := PubkeyIntro{Pubkey: pubkey}
if err := p.validate(); err != nil {
return PubkeyIntro{}, err
}
return p, nil
}
// SciddirIntro is the 9-byte sciddir variant. Direction is the wire
// discriminator; SCID is the 8-byte short channel ID. Use the constructor to
// ensure the direction is valid at construction time.
type SciddirIntro struct {
Direction byte
SCID [scidLen]byte
}
// NewSciddirIntro constructs the sciddir introduction-node variant, rejecting
// an invalid direction discriminator at construction time.
func NewSciddirIntro(direction byte, scid [scidLen]byte) (SciddirIntro, error) {
s := SciddirIntro{Direction: direction, SCID: scid}
if err := s.validate(); err != nil {
return SciddirIntro{}, err
}
return s, nil
}
var (
_ IntroductionNode = PubkeyIntro{}
_ IntroductionNode = SciddirIntro{}
)
// decodeIntroductionNode reads the discriminator byte and dispatches to the
// matching variant.
func decodeIntroductionNode(r io.Reader,
buf *[8]byte) (IntroductionNode, error) {
if _, err := io.ReadFull(r, buf[:1]); err != nil {
return nil, fmt.Errorf("read intro node type: %w", err)
}
disc := buf[0]
switch disc {
case 0x00, 0x01:
var scid [scidLen]byte
if _, err := io.ReadFull(r, scid[:]); err != nil {
return nil, fmt.Errorf("read sciddir: %w", err)
}
return NewSciddirIntro(disc, scid)
case 0x02, 0x03:
var b [pubKeyLen]byte
b[0] = disc
if _, err := io.ReadFull(r, b[1:]); err != nil {
return nil, fmt.Errorf("read intro pubkey: %w", err)
}
pub, err := btcec.ParsePubKey(b[:])
if err != nil {
return nil, fmt.Errorf("%w: %w",
ErrInvalidIntroNode, err)
}
return NewPubkeyIntro(pub)
default:
return nil, fmt.Errorf("%w: 0x%02x", ErrInvalidIntroNode, disc)
}
}
func (PubkeyIntro) isIntroductionNode() {}
func (p PubkeyIntro) encodedLen() uint64 { return pubKeyLen }
func (p PubkeyIntro) encode(w io.Writer) error {
if p.Pubkey == nil {
return fmt.Errorf("nil intro pubkey")
}
_, err := w.Write(p.Pubkey.SerializeCompressed())
return err
}
func (p PubkeyIntro) validate() error {
if p.Pubkey == nil {
return fmt.Errorf("%w: nil pubkey", ErrInvalidIntroNode)
}
if !p.Pubkey.IsOnCurve() {
return fmt.Errorf("%w: pubkey not on curve",
ErrInvalidIntroNode)
}
return nil
}
// Bytes returns the wire-format encoding of the pubkey variant.
func (p PubkeyIntro) Bytes() []byte {
var buf bytes.Buffer
buf.Grow(pubKeyLen)
// We ignore errors because we have validated that the pubkey is non nil
// at construction time.
_ = p.encode(&buf)
return buf.Bytes()
}
func (SciddirIntro) isIntroductionNode() {}
func (s SciddirIntro) encodedLen() uint64 { return sciddirLen }
func (s SciddirIntro) encode(w io.Writer) error {
if _, err := w.Write([]byte{s.Direction}); err != nil {
return err
}
_, err := w.Write(s.SCID[:])
return err
}
func (s SciddirIntro) validate() error {
switch s.Direction {
case 0x00, 0x01:
return nil
}
return fmt.Errorf("%w: 0x%02x", ErrInvalidIntroNode, s.Direction)
}
// Bytes returns the wire-format encoding of the sciddir variant.
func (s SciddirIntro) Bytes() []byte {
var buf bytes.Buffer
buf.Grow(sciddirLen)
// We ignore the error because encode only writes the fixed-size
// direction and SCID to an in-memory buffer, which cannot fail.
_ = s.encode(&buf)
return buf.Bytes()
}