lnd/lnwire/onion_message.go
Olaoluwa Osuntokun 9cad57bfce peer: enforce onion message rate limits at ingress
This commit plumbs the combined IngressLimiter (per-peer + global)
through peer.Config and consults it from the readHandler's
*lnwire.OnionMessage case. The decision is factored into a small
allowOnionMessage helper so that the ingress policy is directly
unit-testable without standing up a full Brontide harness. Per-peer is
checked first inside the IngressLimiter: if we consulted the global
limiter first, a peer whose own bucket was already empty would still
get to burn a global token on each attempt, letting a single hostile
peer drain the shared budget and starve legitimate peers.

peer.Config carries a single OnionLimiter field of IngressLimiter type;
the brontide readHandler calls a single AllowN per incoming onion
message and dispatches on sentinel errors via errors.Is for the
first-drop log path. Nil limiter values are treated as "disabled"
throughout, which both preserves the pre-change behavior when onion
messaging is entirely turned off and keeps the brontide test harness
from needing to construct real limiters. Per-peer bucket state is
retained across disconnect at the IngressLimiter layer so a peer
cannot cycle the connection to reset its per-peer allowance.

OnionMessage also gains a WireSize method that computes the
on-the-wire size directly from the in-memory fields (no round-trip
through Encode) so the hot ingress path can charge the right number of
byte tokens without paying for a full serialization.

The accompanying unit tests cover the nil/disabled path, the
per-peer-rejects-first ordering invariant (asserting the global
limiter is not consulted when the per-peer bucket is empty), the
global rejection path, per-peer isolation across distinct pubkeys, and
a small concurrent stress test that asserts every attempt is accounted
for as either accepted or dropped and that the total accepted count
equals the configured burst under -race. A property-based rapid test
on WireSize guards against silent divergence from WriteMessage if the
OnionMessage wire format ever gains a TLV extension.
2026-04-15 13:23:50 -07:00

107 lines
2.9 KiB
Go

package lnwire
import (
"bytes"
"io"
"github.com/btcsuite/btcd/btcec/v2"
)
// OnionMessage is a message that carries an onion-encrypted payload.
// This is used for BOLT12 messages.
type OnionMessage struct {
// PathKey is the route blinding ephemeral pubkey to be used for
// the onion message.
PathKey *btcec.PublicKey
// OnionBlob contains the onion_message_packet, the raw serialized
// Sphinx onion packet (BOLT 4) containing the layered, per-hop
// encrypted payloads and routing instructions used to forward this
// message along its designated path. This blob should be handled in the
// same manner as onion_routing_packet used to route HTLCs, with the
// exception that it uses blinded routes by default.
OnionBlob []byte
}
// NewOnionMessage creates a new OnionMessage.
func NewOnionMessage(pathKey *btcec.PublicKey,
onion []byte) *OnionMessage {
return &OnionMessage{
PathKey: pathKey,
OnionBlob: onion,
}
}
// A compile-time check to ensure OnionMessage implements the Message interface.
var _ Message = (*OnionMessage)(nil)
var _ SizeableMessage = (*OnionMessage)(nil)
// Decode reads the bytes stream and converts it to the object.
func (o *OnionMessage) Decode(r io.Reader, _ uint32) error {
if err := ReadElement(r, &o.PathKey); err != nil {
return err
}
var onionLen uint16
if err := ReadElement(r, &onionLen); err != nil {
return err
}
o.OnionBlob = make([]byte, onionLen)
if err := ReadElement(r, o.OnionBlob); err != nil {
return err
}
return nil
}
// Encode converts object to the bytes stream and write it into the
// write buffer.
func (o *OnionMessage) Encode(w *bytes.Buffer, _ uint32) error {
if err := WritePublicKey(w, o.PathKey); err != nil {
return err
}
onionLen := len(o.OnionBlob)
if err := WriteUint16(w, uint16(onionLen)); err != nil {
return err
}
if err := WriteBytes(w, o.OnionBlob); err != nil {
return err
}
return nil
}
// MsgType returns the integer uniquely identifying this message type on the
// wire.
func (o *OnionMessage) MsgType() MessageType {
return MsgOnionMessage
}
// WireSize returns the on-the-wire size of the message in bytes, including
// the 2-byte message type prefix, the 33-byte compressed path key, the
// 2-byte onion blob length prefix, and the onion blob itself. It computes
// the size directly from the in-memory fields rather than round-tripping
// through Encode, so callers in the hot ingress path — notably the onion
// message rate limiter — can charge the right number of byte tokens
// without paying for a full serialization.
func (o *OnionMessage) WireSize() int {
const (
msgTypeBytes = 2
pathKeyBytes = 33
onionLenBytes = 2
)
return msgTypeBytes + pathKeyBytes + onionLenBytes + len(o.OnionBlob)
}
// SerializedSize returns the serialized size of the message in bytes.
//
// This is part of the lnwire.SizeableMessage interface.
func (o *OnionMessage) SerializedSize() (uint32, error) {
return uint32(o.WireSize()), nil
}