diff --git a/channeldb/waitingproof_test.go b/channeldb/waitingproof_test.go index 4b0e444168a..dce8ffb5658 100644 --- a/channeldb/waitingproof_test.go +++ b/channeldb/waitingproof_test.go @@ -108,7 +108,8 @@ func TestWaitingProofV2RoundTrip(t *testing.T) { annSig2 := lnwire.NewAnnSigs2( lnwire.ChannelID{1, 2, 3}, lnwire.NewShortChanIDFromInt(42), - partialSig, + lnwire.NewAnnouncementSigPair(partialSig.Sig, partialSig.Sig), + [32]byte{1, 2, 3}, ) // Generate a deterministic public key for the combined nonce. @@ -207,7 +208,8 @@ func TestWaitingProofV2Store(t *testing.T) { annSig2 := lnwire.NewAnnSigs2( lnwire.ChannelID{5, 6, 7}, lnwire.NewShortChanIDFromInt(100), - partialSig, + lnwire.NewAnnouncementSigPair(partialSig.Sig, partialSig.Sig), + [32]byte{5, 6, 7}, ) proof := NewV2WaitingProof(true, annSig2, pubKey) @@ -253,7 +255,8 @@ func TestWaitingProofCrossVersionKeyIsolation(t *testing.T) { v2AnnSig := lnwire.NewAnnSigs2( lnwire.ChannelID{9, 9, 9}, scid, - partialSig, + lnwire.NewAnnouncementSigPair(partialSig.Sig, partialSig.Sig), + [32]byte{9, 9, 9}, ) v2Proof := NewV2WaitingProof(true, v2AnnSig, pubKey) diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index 45e76051922..21451e0e9cd 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -77,6 +77,24 @@ later in the reservation flow as a funder-balance-dust error; they now surface a clearer, spec-aligned error string up front. +* The gossip v2 wire messages in `lnwire` + ([#10837](https://github.com/lightningnetwork/lnd/pull/10837)) have been + pulled in line with the BOLT taproot-gossip extension + ([lightning/bolts#1059](https://github.com/lightning/bolts/pull/1059)): + signature TLVs move from type 160 to 240 (with the signed TLV range + widening from `0..=159` to `0..=239` to match BOLT 12); each gossip v2 + reader now rejects messages missing any compulsory field; the + port-not-zero rule on `node_announcement_2` now also covers + `tor_v3_address`; `gossip_timestamp_filter`'s two block-height TLVs + collapse into a single `block_height_range` TLV; + `announcement_signatures_2` gains a required `funding_txid` TLV and + now carries two raw MuSig2 partial signatures (64 bytes) rather than a + single pre-aggregated 32-byte value; `channel_update_2`'s experimental + inbound-fee TLV is replaced with two properly typed `tu32` records at + types 20/22, and its `short_channel_id` now uses the `sciddir` form of + BOLT 1's `sciddir_or_pubkey` (the previous `second_peer` TLV is gone, + with direction folded into the leading dir byte). + ## Testing ## Database diff --git a/graph/db/models/channel_edge_policy.go b/graph/db/models/channel_edge_policy.go index 067c7861a7a..8bb7922ef96 100644 --- a/graph/db/models/channel_edge_policy.go +++ b/graph/db/models/channel_edge_policy.go @@ -124,12 +124,27 @@ func ChanEdgePolicyFromWire(scid uint64, }, nil case *lnwire.ChannelUpdate2: + // Inbound fees in gossip v2 are two uint32 TLVs that are + // suppressed on the wire when they take their default value + // of 0 (i.e. no inbound surcharge). Treat the both-zero case + // as "no inbound fee" so the downstream Option semantics + // still hold. + var inboundFee fn.Option[lnwire.Fee] + baseFee := upd.InboundFeeBaseMsat.Val + propFee := upd.InboundFeeProportionalMillionths.Val + if baseFee != 0 || propFee != 0 { + inboundFee = fn.Some(lnwire.Fee{ + BaseFee: int32(baseFee), + FeeRate: int32(propFee), + }) + } + return &ChannelEdgePolicy{ Version: lnwire.GossipVersion2, SigBytes: upd.Signature.Val.ToSignatureBytes(), ChannelID: scid, LastBlockHeight: upd.BlockHeight.Val, - SecondPeer: upd.SecondPeer.IsSome(), + SecondPeer: !upd.IsNode1(), DisableFlags: upd.DisabledFlags.Val, TimeLockDelta: upd.CLTVExpiryDelta.Val, MinHTLC: upd.HTLCMinimumMsat.Val, @@ -140,7 +155,7 @@ func ChanEdgePolicyFromWire(scid uint64, FeeProportionalMillionths: lnwire.MilliSatoshi( upd.FeeProportionalMillionths.Val, ), - InboundFee: upd.InboundFee.ValOpt(), + InboundFee: inboundFee, ExtraSignedFields: upd.ExtraSignedFields, }, nil } diff --git a/lnwire/announcement_signatures_2.go b/lnwire/announcement_signatures_2.go index a2806f1ee2c..101e7af1cbc 100644 --- a/lnwire/announcement_signatures_2.go +++ b/lnwire/announcement_signatures_2.go @@ -25,10 +25,19 @@ type AnnounceSignatures2 struct { // index which pays to the channel. ShortChannelID tlv.RecordT[tlv.TlvType2, ShortChannelID] - // PartialSignature is the combination of the partial Schnorr signature - // created for the node's bitcoin key with the partial signature created - // for the node's node ID key. - PartialSignature tlv.RecordT[tlv.TlvType4, PartialSig] + // PartialSignatures carries the two raw musig2 partial signatures + // produced by the sender (one with its node_id key, one with its + // bitcoin key), concatenated as `node || bitcoin` (64 bytes total). + // The receiver verifies each half independently with the standard + // MuSig2 partial-sig verify routine. + PartialSignatures tlv.RecordT[tlv.TlvType4, AnnouncementSigPair] + + // FundingTxID is the txid of the funding transaction that this + // announcement signature covers. For an initial channel announcement + // this is the original funding transaction; for a spliced channel it + // is the txid of the splice transaction whose splice_locked triggered + // the new round of announcement signing. + FundingTxID tlv.RecordT[tlv.TlvType6, [32]byte] // Any extra fields in the signed range that we do not yet know about, // but we need to keep them for signature validation and to produce a @@ -38,15 +47,19 @@ type AnnounceSignatures2 struct { // NewAnnSigs2 is a constructor for AnnounceSignatures2. func NewAnnSigs2(chanID ChannelID, scid ShortChannelID, - partialSig PartialSig) *AnnounceSignatures2 { + sigs AnnouncementSigPair, + fundingTxID [32]byte) *AnnounceSignatures2 { return &AnnounceSignatures2{ ChannelID: tlv.NewRecordT[tlv.TlvType0, ChannelID](chanID), ShortChannelID: tlv.NewRecordT[tlv.TlvType2, ShortChannelID]( scid, ), - PartialSignature: tlv.NewRecordT[tlv.TlvType4, PartialSig]( - partialSig, + PartialSignatures: tlv.NewRecordT[ + tlv.TlvType4, AnnouncementSigPair, + ](sigs), + FundingTxID: tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte]( + fundingTxID, ), ExtraSignedFields: make(ExtraSignedFields), } @@ -70,7 +83,8 @@ var _ PureTLVMessage = (*AnnounceSignatures2)(nil) // This is part of the lnwire.Message interface. func (a *AnnounceSignatures2) Decode(r io.Reader, _ uint32) error { stream, err := tlv.NewStream(ProduceRecordsSorted( - &a.ChannelID, &a.ShortChannelID, &a.PartialSignature, + &a.ChannelID, &a.ShortChannelID, &a.PartialSignatures, + &a.FundingTxID, )...) if err != nil { return err @@ -81,6 +95,16 @@ func (a *AnnounceSignatures2) Decode(r io.Reader, _ uint32) error { return err } + if err := AssertRequiredPresent( + typeMap, + a.ChannelID.TlvType(), + a.ShortChannelID.TlvType(), + a.PartialSignatures.TlvType(), + a.FundingTxID.TlvType(), + ); err != nil { + return err + } + a.ExtraSignedFields = ExtraSignedFieldsFromTypeMap(typeMap) return nil @@ -124,7 +148,7 @@ func (a *AnnounceSignatures2) SerializedSize() (uint32, error) { func (a *AnnounceSignatures2) AllRecords() []tlv.Record { recordProducers := []tlv.RecordProducer{ &a.ChannelID, &a.ShortChannelID, - &a.PartialSignature, + &a.PartialSignatures, &a.FundingTxID, } recordProducers = append(recordProducers, RecordsAsProducers( diff --git a/lnwire/announcement_signatures_2_test.go b/lnwire/announcement_signatures_2_test.go index 6b945edcfc6..bd1b394e066 100644 --- a/lnwire/announcement_signatures_2_test.go +++ b/lnwire/announcement_signatures_2_test.go @@ -32,9 +32,16 @@ func TestAnnSigs2EncodeDecode(t *testing.T) { 0, 0, 1, 0, 0, 2, 0, 3, // value }...) - // PartialSignature. + // PartialSignatures (node || bitcoin, 64 bytes total). rawBytes = append(rawBytes, []byte{ 0x04, // type + 0x40, // length + }...) + rawBytes = append(rawBytes, make([]byte, 64)...) // value + + // FundingTxID. + rawBytes = append(rawBytes, []byte{ + 0x06, // type 0x20, // length }...) rawBytes = append(rawBytes, make([]byte, 32)...) // value diff --git a/lnwire/channel_announcement_2.go b/lnwire/channel_announcement_2.go index 9227584b6f5..ae509e602da 100644 --- a/lnwire/channel_announcement_2.go +++ b/lnwire/channel_announcement_2.go @@ -61,7 +61,7 @@ type ChannelAnnouncement2 struct { // Signature is a Schnorr signature over serialised signed-range TLV // stream of the message. - Signature tlv.RecordT[tlv.TlvType160, Sig] + Signature tlv.RecordT[tlv.TlvType240, Sig] // Any extra fields in the signed range that we do not yet know about, // but we need to keep them for signature validation and to produce a @@ -166,6 +166,18 @@ func (c *ChannelAnnouncement2) Decode(r io.Reader, _ uint32) error { return err } + if err := AssertRequiredPresent( + typeMap, + c.ShortChannelID.TlvType(), + c.Outpoint.TlvType(), + c.Capacity.TlvType(), + c.NodeID1.TlvType(), + c.NodeID2.TlvType(), + c.Signature.TlvType(), + ); err != nil { + return err + } + // By default, the chain-hash is the bitcoin mainnet genesis block hash. c.ChainHash.Val = *chaincfg.MainNetParams.GenesisHash if _, ok := typeMap[c.ChainHash.TlvType()]; ok { diff --git a/lnwire/channel_announcement_2_test.go b/lnwire/channel_announcement_2_test.go index 3a2ce28a903..14d0ed64b1c 100644 --- a/lnwire/channel_announcement_2_test.go +++ b/lnwire/channel_announcement_2_test.go @@ -75,7 +75,7 @@ func TestChanAnn2EncodeDecode(t *testing.T) { 0x79, 0x79, // value. // Signature. - 0xa0, // type. + 0xf0, // type. 0x40, // length. 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, diff --git a/lnwire/channel_update_2.go b/lnwire/channel_update_2.go index 3fba2470df3..964e3859250 100644 --- a/lnwire/channel_update_2.go +++ b/lnwire/channel_update_2.go @@ -11,10 +11,12 @@ import ( ) const ( - defaultCltvExpiryDelta = uint16(80) - defaultHtlcMinMsat = MilliSatoshi(1) - defaultFeeBaseMsat = uint32(1000) - defaultFeeProportionalMillionths = uint32(1) + defaultCltvExpiryDelta = uint16(80) + defaultHtlcMinMsat = MilliSatoshi(1) + defaultFeeBaseMsat = uint32(1000) + defaultFeeProportionalMillionths = uint32(1) + defaultInboundFeeBaseMsat = uint32(0) + defaultInboundFeeProportionalMillionths = uint32(0) ) // ChannelUpdate2 message is used after taproot channel has been initially @@ -28,8 +30,13 @@ type ChannelUpdate2 struct { // channel globally in a blockchain. ChainHash tlv.RecordT[tlv.TlvType0, chainhash.Hash] - // ShortChannelID is the unique description of the funding transaction. - ShortChannelID tlv.RecordT[tlv.TlvType2, ShortChannelID] + // ShortChannelID identifies the channel and the side of it that sent + // this update. It is BOLT 1's `sciddir_or_pubkey` type constrained to + // the `sciddir` form: 9 wire bytes of ``, where the + // direction byte is `0` for `node_id_1` and `1` for `node_id_2`. The + // previous separate `second_peer` flag TLV at type 8 is no longer + // emitted; its information is now carried by the direction byte. + ShortChannelID tlv.RecordT[tlv.TlvType2, Sciddir] // BlockHeight allows ordering in the case of multiple announcements. We // should ignore the message if block height is not greater than the @@ -43,11 +50,6 @@ type ChannelUpdate2 struct { // disabled. DisabledFlags tlv.RecordT[tlv.TlvType6, ChanUpdateDisableFlags] - // SecondPeer is used to indicate which node the channel node has - // created and signed this message. If this field is present, it was - // node 2 otherwise it was node 1. - SecondPeer tlv.OptionalRecordT[tlv.TlvType8, TrueBoolean] - // CLTVExpiryDelta is the minimum number of blocks this node requires to // be added to the expiry of HTLCs. This is a security parameter // determined by the node operator. This value represents the required @@ -70,14 +72,20 @@ type ChannelUpdate2 struct { // millionth of a satoshi. FeeProportionalMillionths tlv.RecordT[tlv.TlvType18, uint32] - // InboundFee is an optional TLV record that contains the fee - // information for incoming HTLCs. - // TODO(elle): assign normal tlv type? - InboundFee tlv.OptionalRecordT[tlv.TlvType55555, Fee] + // InboundFeeBaseMsat is the base fee (in millisatoshis) added by this + // node for HTLCs forwarded *in* via this channel, regardless of which + // channel they are forwarded out on. Default 0. Positive-only: this + // version of gossip does not support negative inbound fees. + InboundFeeBaseMsat tlv.RecordT[tlv.TlvType20, uint32] + + // InboundFeeProportionalMillionths is the proportional inbound fee (in + // millionths of a satoshi) added by this node per transferred satoshi + // for HTLCs forwarded *in* via this channel. Default 0. Positive-only. + InboundFeeProportionalMillionths tlv.RecordT[tlv.TlvType22, uint32] // Signature is used to validate the announced data and prove the // ownership of node id. - Signature tlv.RecordT[tlv.TlvType160, Sig] + Signature tlv.RecordT[tlv.TlvType240, Sig] // Any extra fields in the signed range that we do not yet know about, // but we need to keep them for signature validation and to produce a @@ -111,16 +119,13 @@ func (c *ChannelUpdate2) Decode(r io.Reader, _ uint32) error { return err } - var ( - chainHash = tlv.ZeroRecordT[tlv.TlvType0, [32]byte]() - secondPeer = tlv.ZeroRecordT[tlv.TlvType8, TrueBoolean]() - inboundFee = tlv.ZeroRecordT[tlv.TlvType55555, Fee]() - ) + chainHash := tlv.ZeroRecordT[tlv.TlvType0, [32]byte]() typeMap, err := tlvRecords.ExtractRecords( &chainHash, &c.ShortChannelID, &c.BlockHeight, &c.DisabledFlags, - &secondPeer, &c.CLTVExpiryDelta, &c.HTLCMinimumMsat, + &c.CLTVExpiryDelta, &c.HTLCMinimumMsat, &c.HTLCMaximumMsat, &c.FeeBaseMsat, - &c.FeeProportionalMillionths, &inboundFee, + &c.FeeProportionalMillionths, + &c.InboundFeeBaseMsat, &c.InboundFeeProportionalMillionths, &c.Signature, ) if err != nil { @@ -128,17 +133,21 @@ func (c *ChannelUpdate2) Decode(r io.Reader, _ uint32) error { } c.Signature.Val.ForceSchnorr() + if err := AssertRequiredPresent( + typeMap, + c.ShortChannelID.TlvType(), + c.BlockHeight.TlvType(), + c.Signature.TlvType(), + ); err != nil { + return err + } + // By default, the chain-hash is the bitcoin mainnet genesis block hash. c.ChainHash.Val = *chaincfg.MainNetParams.GenesisHash if _, ok := typeMap[c.ChainHash.TlvType()]; ok { c.ChainHash.Val = chainHash.Val } - // The presence of the second_peer tlv type indicates "true". - if _, ok := typeMap[c.SecondPeer.TlvType()]; ok { - c.SecondPeer = tlv.SomeRecordT(secondPeer) - } - // If the CLTV expiry delta was not encoded, then set it to the default // value. if _, ok := typeMap[c.CLTVExpiryDelta.TlvType()]; !ok { @@ -162,9 +171,14 @@ func (c *ChannelUpdate2) Decode(r io.Reader, _ uint32) error { c.FeeProportionalMillionths.Val = defaultFeeProportionalMillionths //nolint:ll } - // If the inbound fee was encoded, set it. - if _, ok := typeMap[c.InboundFee.TlvType()]; ok { - c.InboundFee = tlv.SomeRecordT(inboundFee) + // If the inbound base fee was not encoded, default to 0. + if _, ok := typeMap[c.InboundFeeBaseMsat.TlvType()]; !ok { + c.InboundFeeBaseMsat.Val = defaultInboundFeeBaseMsat + } + + // If the inbound proportional fee was not encoded, default to 0. + if _, ok := typeMap[c.InboundFeeProportionalMillionths.TlvType()]; !ok { + c.InboundFeeProportionalMillionths.Val = defaultInboundFeeProportionalMillionths //nolint:ll } c.ExtraSignedFields = ExtraSignedFieldsFromTypeMap(typeMap) @@ -198,11 +212,6 @@ func (c *ChannelUpdate2) AllRecords() []tlv.Record { recordProducers = append(recordProducers, &c.DisabledFlags) } - // We only need to encode the second peer boolean if it is true - c.SecondPeer.WhenSome(func(r tlv.RecordT[tlv.TlvType8, TrueBoolean]) { - recordProducers = append(recordProducers, &r) - }) - // We only encode the cltv expiry delta if it is not equal to the // default. if c.CLTVExpiryDelta.Val != defaultCltvExpiryDelta { @@ -225,9 +234,15 @@ func (c *ChannelUpdate2) AllRecords() []tlv.Record { ) } - c.InboundFee.WhenSome(func(r tlv.RecordT[tlv.TlvType55555, Fee]) { - recordProducers = append(recordProducers, &r) - }) + if c.InboundFeeBaseMsat.Val != defaultInboundFeeBaseMsat { + recordProducers = append(recordProducers, &c.InboundFeeBaseMsat) + } + + if c.InboundFeeProportionalMillionths.Val != defaultInboundFeeProportionalMillionths { //nolint:ll + recordProducers = append( + recordProducers, &c.InboundFeeProportionalMillionths, + ) + } recordProducers = append(recordProducers, RecordsAsProducers( tlv.MapToRecords(c.ExtraSignedFields), @@ -259,19 +274,21 @@ var _ Message = (*ChannelUpdate2)(nil) // lnwire.PureTLVMessage interface. var _ PureTLVMessage = (*ChannelUpdate2)(nil) -// SCID returns the ShortChannelID of the channel that the update applies to. +// SCID returns the ShortChannelID of the channel that the update applies to, +// projecting away the direction byte that the wire encoding carries. // // NOTE: this is part of the ChannelUpdate interface. func (c *ChannelUpdate2) SCID() ShortChannelID { - return c.ShortChannelID.Val + return c.ShortChannelID.Val.ID } // IsNode1 is true if the update was produced by node 1 of the channel peers. -// Node 1 is the node with the lexicographically smaller public key. +// Node 1 is the node with the lexicographically smaller public key, and is +// indicated by a direction byte of 0 in the encoded ShortChannelID. // // NOTE: this is part of the ChannelUpdate interface. func (c *ChannelUpdate2) IsNode1() bool { - return c.SecondPeer.IsNone() + return c.ShortChannelID.Val.IsNode1() } // IsDisabled is true if the update is announcing that the channel should be @@ -338,11 +355,12 @@ func (c *ChannelUpdate2) SetDisabledFlag(disabled bool) { } } -// SetSCID can be used to overwrite the SCID of the update. +// SetSCID can be used to overwrite the SCID of the update, leaving the +// existing direction byte in place. // // NOTE: this is part of the ChannelUpdate interface. func (c *ChannelUpdate2) SetSCID(scid ShortChannelID) { - c.ShortChannelID.Val = scid + c.ShortChannelID.Val.ID = scid } // A compile time check to ensure ChannelUpdate2 implements the diff --git a/lnwire/channel_update_2_test.go b/lnwire/channel_update_2_test.go index 4e771d89f00..189dcc1aebd 100644 --- a/lnwire/channel_update_2_test.go +++ b/lnwire/channel_update_2_test.go @@ -24,10 +24,11 @@ func TestChanUpdate2EncodeDecode(t *testing.T) { 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, - // ShortChannelID record. + // ShortChannelID record (sciddir form: dir byte + 8-byte scid). 0x2, // type. - 0x8, // length. - 0x0, 0x0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x3, // value. + 0x9, // length. + 0x1, // dir byte: node_id_2. + 0x0, 0x0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x3, // scid value. // BlockHeight record. 0x4, // type. @@ -39,10 +40,6 @@ func TestChanUpdate2EncodeDecode(t *testing.T) { 0x1, // length. 0x1, // value. - // SecondPeer record. - 0x8, // type. - 0x0, // length. - // Unknown odd-type TLV record. 0x9, // type. 0x2, // length. @@ -73,13 +70,23 @@ func TestChanUpdate2EncodeDecode(t *testing.T) { 0x4, // length. 0x0, 0x0, 0x1, 0x0, // value. + // InboundFeeBaseMsat record. + 0x14, // type. + 0x4, // length. + 0x0, 0x0, 0x0, 0x5, // value (5). + + // InboundFeeProportionalMillionths record. + 0x16, // type. + 0x4, // length. + 0x0, 0x0, 0x0, 0x3, // value (3). + // Extra Opaque Data - Unknown Record. - 0x14, // type. + 0x18, // type. 0x2, // length. 0x79, 0x79, // value. // Signature. - 0xa0, // type. + 0xf0, // type. 0x40, // length. 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, diff --git a/lnwire/gossip_timestamp_range.go b/lnwire/gossip_timestamp_range.go index 45ff1f939af..c169b35f74b 100644 --- a/lnwire/gossip_timestamp_range.go +++ b/lnwire/gossip_timestamp_range.go @@ -32,16 +32,12 @@ type GossipTimestampRange struct { // Unix timestamps. TimestampRange uint32 - // FirstBlockHeight is the height of earliest announcement message that - // should be sent by the receiver. This is used only for querying - // announcement messages that use block heights as a timestamp. - FirstBlockHeight tlv.OptionalRecordT[tlv.TlvType2, uint32] - - // BlockRange is the horizon beyond FirstBlockHeight that any - // announcement messages should be sent for. The receiving node MUST NOT - // send any announcements that have a timestamp greater than - // FirstBlockHeight + BlockRange. - BlockRange tlv.OptionalRecordT[tlv.TlvType4, uint32] + // BlockHeightRange is the block-height equivalent of (FirstTimestamp, + // TimestampRange) used for gossip messages timestamped by block + // height. The receiving node MUST NOT send any announcements that have + // a block height greater than BlockHeightRange.FirstBlockHeight + + // BlockHeightRange.NumBlocks. + BlockHeightRange tlv.OptionalRecordT[tlv.TlvType2, BlockHeightRange] // ExtraData is the set of data that was appended to this message to // fill out the full maximum transport message size. These fields can @@ -54,6 +50,65 @@ func NewGossipTimestampRange() *GossipTimestampRange { return &GossipTimestampRange{} } +// BlockHeightRange describes a window of blocks that an announcement may have +// been timestamped within. It is serialised as a u32 first_block_height +// followed by a tu32 num_blocks; the tu32 (truncated uint32) drops any +// leading zero bytes from num_blocks to save space on the wire. +type BlockHeightRange struct { + // FirstBlockHeight is the height of the earliest announcement message + // that should be sent by the receiver. + FirstBlockHeight uint32 + + // NumBlocks is the size of the window beyond FirstBlockHeight that + // announcements should be sent for. + NumBlocks uint32 +} + +// Record returns the TLV record used to encode/decode a BlockHeightRange. The +// type number is supplied by the wrapping RecordT. +func (b *BlockHeightRange) Record() tlv.Record { + sizeFunc := func() uint64 { + return 4 + tlv.SizeTUint32(b.NumBlocks) + } + + return tlv.MakeDynamicRecord( + 0, b, sizeFunc, blockHeightRangeEncoder, + blockHeightRangeDecoder, + ) +} + +func blockHeightRangeEncoder(w io.Writer, val interface{}, + buf *[8]byte) error { + + v, ok := val.(*BlockHeightRange) + if !ok { + return tlv.NewTypeForEncodingErr(val, "lnwire.BlockHeightRange") + } + + if err := tlv.EUint32T(w, v.FirstBlockHeight, buf); err != nil { + return err + } + + return tlv.ETUint32T(w, v.NumBlocks, buf) +} + +func blockHeightRangeDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + v, ok := val.(*BlockHeightRange) + if !ok || l < 4 { + return tlv.NewTypeForDecodingErr( + val, "lnwire.BlockHeightRange", l, 4, + ) + } + + if err := tlv.DUint32(r, &v.FirstBlockHeight, buf, 4); err != nil { + return err + } + + return tlv.DTUint32(r, &v.NumBlocks, buf, l-4) +} + // A compile time check to ensure GossipTimestampRange implements the // lnwire.Message interface. var _ Message = (*GossipTimestampRange)(nil) @@ -81,20 +136,14 @@ func (g *GossipTimestampRange) Decode(r io.Reader, _ uint32) error { return err } - var ( - firstBlock = tlv.ZeroRecordT[tlv.TlvType2, uint32]() - blockRange = tlv.ZeroRecordT[tlv.TlvType4, uint32]() - ) - typeMap, err := tlvRecords.ExtractRecords(&firstBlock, &blockRange) + bhRange := tlv.ZeroRecordT[tlv.TlvType2, BlockHeightRange]() + typeMap, err := tlvRecords.ExtractRecords(&bhRange) if err != nil { return err } - if val, ok := typeMap[g.FirstBlockHeight.TlvType()]; ok && val == nil { - g.FirstBlockHeight = tlv.SomeRecordT(firstBlock) - } - if val, ok := typeMap[g.BlockRange.TlvType()]; ok && val == nil { - g.BlockRange = tlv.SomeRecordT(blockRange) + if val, ok := typeMap[g.BlockHeightRange.TlvType()]; ok && val == nil { + g.BlockHeightRange = tlv.SomeRecordT(bhRange) } if len(tlvRecords) != 0 { @@ -121,15 +170,10 @@ func (g *GossipTimestampRange) Encode(w *bytes.Buffer, pver uint32) error { return err } - recordProducers := make([]tlv.RecordProducer, 0, 2) - g.FirstBlockHeight.WhenSome( - func(height tlv.RecordT[tlv.TlvType2, uint32]) { - recordProducers = append(recordProducers, &height) - }, - ) - g.BlockRange.WhenSome( - func(blockRange tlv.RecordT[tlv.TlvType4, uint32]) { - recordProducers = append(recordProducers, &blockRange) + recordProducers := make([]tlv.RecordProducer, 0, 1) + g.BlockHeightRange.WhenSome( + func(bhr tlv.RecordT[tlv.TlvType2, BlockHeightRange]) { + recordProducers = append(recordProducers, &bhr) }, ) err := EncodeMessageExtraData(&g.ExtraData, recordProducers...) diff --git a/lnwire/node_announcement_2.go b/lnwire/node_announcement_2.go index 93a3792a2d8..2b178a881bc 100644 --- a/lnwire/node_announcement_2.go +++ b/lnwire/node_announcement_2.go @@ -59,7 +59,7 @@ type NodeAnnouncement2 struct { // Signature is used to validate the announced data and prove the // ownership of node id. - Signature tlv.RecordT[tlv.TlvType160, Sig] + Signature tlv.RecordT[tlv.TlvType240, Sig] // Any extra fields in the signed range that we do not yet know about, // but we need to keep them for signature validation and to produce a @@ -154,6 +154,16 @@ func (n *NodeAnnouncement2) Decode(r io.Reader, _ uint32) error { return err } + if err := AssertRequiredPresent( + typeMap, + n.Features.TlvType(), + n.BlockHeight.TlvType(), + n.NodeID.TlvType(), + n.Signature.TlvType(), + ); err != nil { + return err + } + if _, ok := typeMap[n.Alias.TlvType()]; ok { n.Alias = tlv.SomeRecordT(alias) } @@ -536,6 +546,11 @@ func (a *TorV3Addrs) Record() tlv.Record { func torV3AddrsEncoder(w io.Writer, val interface{}, _ *[8]byte) error { if v, ok := val.(*TorV3Addrs); ok { for _, addr := range *v { + if addr.Port == 0 { + return fmt.Errorf("tor_v3_address port " + + "must not be 0") + } + encodedHostLen := tor.V3Len - tor.OnionSuffixLen host, err := tor.Base32Encoding.DecodeString( addr.OnionService[:encodedHostLen], @@ -600,6 +615,10 @@ func torV3AddrsDecoder(r io.Reader, val interface{}, _ *[8]byte, } port := int(binary.BigEndian.Uint16(p[:])) + if port == 0 { + return fmt.Errorf("tor_v3_address port " + + "must not be 0") + } addrs = append(addrs, &tor.OnionAddr{ OnionService: onionService, Port: port, diff --git a/lnwire/node_announcement_2_test.go b/lnwire/node_announcement_2_test.go index 0aea6614789..b03677ab15b 100644 --- a/lnwire/node_announcement_2_test.go +++ b/lnwire/node_announcement_2_test.go @@ -80,7 +80,7 @@ func TestNodeAnn2EncodeDecode(t *testing.T) { 0x23, 0x28, // port 9000. // Signature. - 0xa0, // type. + 0xf0, // type. 0x40, // length. 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, diff --git a/lnwire/partial_sig.go b/lnwire/partial_sig.go index d5af8ec9024..582951fdae8 100644 --- a/lnwire/partial_sig.go +++ b/lnwire/partial_sig.go @@ -249,3 +249,88 @@ func MaybePartialSigWithNonce(sig *PartialSigWithNonce, ), ) } + +const ( + // AnnouncementSigPairLen is the wire length of an AnnouncementSigPair: + // two concatenated 32-byte musig2 partial signatures. + AnnouncementSigPairLen = 64 +) + +// AnnouncementSigPair carries the two raw musig2 partial signatures that a +// node emits in announcement_signatures_2 -- one over its node_id key and one +// over its bitcoin key. They are encoded back-to-back as `node || bitcoin` +// for a total of 64 bytes. The BOLT taproot-gossip extension dropped the +// previously pre-aggregated 32-byte form in favour of this layout so that +// receivers can verify each sig with the standard MuSig2 partial-sig verify +// routine. +type AnnouncementSigPair struct { + // Node is the partial signature produced with the node_id key. + Node btcec.ModNScalar + + // Bitcoin is the partial signature produced with the bitcoin key. + Bitcoin btcec.ModNScalar +} + +// NewAnnouncementSigPair creates a new AnnouncementSigPair. +func NewAnnouncementSigPair(node, + bitcoin btcec.ModNScalar) AnnouncementSigPair { + + return AnnouncementSigPair{ + Node: node, + Bitcoin: bitcoin, + } +} + +// Record returns the tlv record for the announcement-sig pair. The wrapping +// RecordT supplies the real type number; the zero passed here is overridden. +func (a *AnnouncementSigPair) Record() tlv.Record { + return tlv.MakeStaticRecord( + 0, a, AnnouncementSigPairLen, + announcementSigPairEncoder, announcementSigPairDecoder, + ) +} + +func announcementSigPairEncoder(w io.Writer, val interface{}, + _ *[8]byte) error { + + v, ok := val.(*AnnouncementSigPair) + if !ok { + return tlv.NewTypeForEncodingErr( + val, "lnwire.AnnouncementSigPair", + ) + } + + nodeBytes := v.Node.Bytes() + if _, err := w.Write(nodeBytes[:]); err != nil { + return err + } + bitcoinBytes := v.Bitcoin.Bytes() + _, err := w.Write(bitcoinBytes[:]) + + return err +} + +func announcementSigPairDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + v, ok := val.(*AnnouncementSigPair) + if !ok || l != AnnouncementSigPairLen { + return tlv.NewTypeForDecodingErr( + val, "lnwire.AnnouncementSigPair", l, + AnnouncementSigPairLen, + ) + } + + var nodeBytes, bitcoinBytes [32]byte + if err := tlv.DBytes32(r, &nodeBytes, buf, 32); err != nil { + return err + } + if err := tlv.DBytes32(r, &bitcoinBytes, buf, 32); err != nil { + return err + } + + v.Node.SetBytes(&nodeBytes) + v.Bitcoin.SetBytes(&bitcoinBytes) + + return nil +} diff --git a/lnwire/pure_tlv.go b/lnwire/pure_tlv.go index 8e6f7bd9fc3..b24164b6908 100644 --- a/lnwire/pure_tlv.go +++ b/lnwire/pure_tlv.go @@ -2,6 +2,7 @@ package lnwire import ( "bytes" + "fmt" "github.com/lightningnetwork/lnd/tlv" ) @@ -10,11 +11,11 @@ const ( // pureTLVUnsignedRangeOneStart defines the start of the first unsigned // TLV range used for pure TLV messages. The range is inclusive of this // number. - pureTLVUnsignedRangeOneStart = 160 + pureTLVUnsignedRangeOneStart = 240 // pureTLVSignedSecondRangeStart defines the start of the second signed // TLV range used for pure TLV messages. The range is inclusive of this - // number. Note that the first range is the inclusive range of 0-159. + // number. Note that the first range is the inclusive range of 0-239. pureTLVSignedSecondRangeStart = 1000000000 // pureTLVUnsignedRangeTwoStart defines the start of the second unsigned @@ -24,7 +25,7 @@ const ( // PureTLVMessage describes an LN message that is a pure TLV stream. If the // message includes a signature, it will sign all the TLV records in the -// inclusive ranges: 0 to 159 and 1000000000 to 2999999999. +// inclusive ranges: 0 to 239 and 1000000000 to 2999999999. type PureTLVMessage interface { // AllRecords returns all the TLV records for the message. This will // include all the records we know about along with any that we don't @@ -66,6 +67,21 @@ func InUnsignedRange(t tlv.Type) bool { t >= pureTLVUnsignedRangeTwoStart } +// AssertRequiredPresent returns an error if any of the given TLV types is +// missing from the parsed type map (as returned by the various +// DecodeWithParsedTypes helpers). It is used to enforce the spec's +// reader-side requirement that compulsory TLVs are present in a received +// pure-TLV message. +func AssertRequiredPresent(typeMap tlv.TypeMap, required ...tlv.Type) error { + for _, t := range required { + if _, ok := typeMap[t]; !ok { + return fmt.Errorf("required TLV type %d missing", t) + } + } + + return nil +} + // ExtraSignedFields is a type that stores a map from TLV types in the signed // range (for PureMessages) to their corresponding serialised values. This type // can be used to keep around data that we don't yet understand but that we need diff --git a/lnwire/pure_tlv_test.go b/lnwire/pure_tlv_test.go index a81a89ecb6d..31fb167b18a 100644 --- a/lnwire/pure_tlv_test.go +++ b/lnwire/pure_tlv_test.go @@ -113,7 +113,7 @@ type MsgV1 struct { Capacity tlv.OptionalRecordT[tlv.TlvType1, MilliSatoshi] // Signature in the unsigned range. - Signature tlv.RecordT[tlv.TlvType160, Sig] + Signature tlv.RecordT[tlv.TlvType240, Sig] // Any extra fields in the signed range that we do not yet know about, // but we need to keep them for signature validation and to produce a @@ -130,7 +130,7 @@ func newMsgV1(nodeKey *btcec.PublicKey, capacity *MilliSatoshi) *MsgV1 { NodeKey: tlv.NewPrimitiveRecord[tlv.TlvType0]( nodeKey, ), - Signature: tlv.NewRecordT[tlv.TlvType160]( + Signature: tlv.NewRecordT[tlv.TlvType240]( testSchnorrSig, ), ExtraSignedFields: make(ExtraSignedFields), @@ -230,11 +230,11 @@ type MsgV2 struct { SecondPeer tlv.OptionalRecordT[tlv.TlvType5, TrueBoolean] // Signature in the unsigned range. - Signature tlv.RecordT[tlv.TlvType160, Sig] + Signature tlv.RecordT[tlv.TlvType240, Sig] // Another field in the unsigned range. An older node can throw this // away. - SPVProof tlv.RecordT[tlv.TlvType161, []byte] + SPVProof tlv.RecordT[tlv.TlvType241, []byte] // A new field in the second signed range. An older node should keep // this since it is part of the serialised message that is signed. @@ -258,10 +258,10 @@ func newMsgV2(nodeKey *btcec.PublicKey, capacity *MilliSatoshi, newMsg := &MsgV2{ NodeKey: tlv.NewPrimitiveRecord[tlv.TlvType0](nodeKey), - SPVProof: tlv.NewPrimitiveRecord[tlv.TlvType161](spvProof), + SPVProof: tlv.NewPrimitiveRecord[tlv.TlvType241](spvProof), Num: tlv.NewPrimitiveRecord[tlv.TlvType1000000000](num), Other: tlv.NewPrimitiveRecord[tlv.TlvType3000000000](num), - Signature: tlv.NewRecordT[tlv.TlvType160]( + Signature: tlv.NewRecordT[tlv.TlvType240]( testSchnorrSig, ), ExtraSignedFields: make(ExtraSignedFields), diff --git a/lnwire/sciddir.go b/lnwire/sciddir.go new file mode 100644 index 00000000000..ba8bf4a2afb --- /dev/null +++ b/lnwire/sciddir.go @@ -0,0 +1,101 @@ +package lnwire + +import ( + "fmt" + "io" + + "github.com/lightningnetwork/lnd/tlv" +) + +// SciddirLen is the wire length of a Sciddir: one direction byte followed by +// the 8-byte short_channel_id. +const SciddirLen = 9 + +// Sciddir is the `sciddir` form of BOLT 1's `sciddir_or_pubkey` type: a +// channel-with-direction identifier carried on the wire as +// `` for 9 bytes total. The leading byte must be +// `0` (refers to node_id_1 of the corresponding channel_announcement_2) or +// `1` (refers to node_id_2). The `pubkey` form of `sciddir_or_pubkey` is +// rejected wherever a Sciddir is used. +type Sciddir struct { + // Direction is the direction byte. `0` means the message comes from + // (or refers to) `node_id_1` of the channel announcement; `1` means + // `node_id_2`. + Direction byte + + // ID is the 8-byte short_channel_id portion. + ID ShortChannelID +} + +// NewSciddir builds a Sciddir from a short_channel_id and an `isSecondPeer` +// flag — true if the message comes from `node_id_2`. +func NewSciddir(scid ShortChannelID, isSecondPeer bool) Sciddir { + var dir byte + if isSecondPeer { + dir = 1 + } + + return Sciddir{ + Direction: dir, + ID: scid, + } +} + +// IsNode1 reports whether the direction byte refers to `node_id_1` (i.e. +// `dirbyte == 0`). +func (s *Sciddir) IsNode1() bool { + return s.Direction == 0 +} + +// Record returns the tlv record used to encode a Sciddir on the wire. The +// wrapping RecordT supplies the actual TLV type number; the zero passed here +// is overridden. +func (s *Sciddir) Record() tlv.Record { + return tlv.MakeStaticRecord( + 0, s, SciddirLen, sciddirEncoder, sciddirDecoder, + ) +} + +func sciddirEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + v, ok := val.(*Sciddir) + if !ok { + return tlv.NewTypeForEncodingErr(val, "lnwire.Sciddir") + } + + if v.Direction != 0 && v.Direction != 1 { + return fmt.Errorf("sciddir direction byte must be 0 or 1, "+ + "got %d", v.Direction) + } + + if _, err := w.Write([]byte{v.Direction}); err != nil { + return err + } + + return EShortChannelID(w, &v.ID, buf) +} + +func sciddirDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + v, ok := val.(*Sciddir) + if !ok || l != SciddirLen { + return tlv.NewTypeForDecodingErr(val, "lnwire.Sciddir", l, + SciddirLen) + } + + var dir [1]byte + if _, err := io.ReadFull(r, dir[:]); err != nil { + return err + } + + // Constrain to the sciddir form. A first byte of anything other than + // 0 or 1 would indicate the pubkey form of sciddir_or_pubkey, which is + // not permitted in this context. + if dir[0] != 0 && dir[0] != 1 { + return fmt.Errorf("expected sciddir form of sciddir_or_pubkey "+ + "(direction byte 0 or 1), got %d", dir[0]) + } + v.Direction = dir[0] + + return DShortChannelID(r, &v.ID, buf, 8) +} diff --git a/lnwire/test_message.go b/lnwire/test_message.go index 8734440a0f2..b4e93ea747c 100644 --- a/lnwire/test_message.go +++ b/lnwire/test_message.go @@ -132,18 +132,25 @@ var _ TestMessage = (*AnnounceSignatures2)(nil) // This is part of the TestMessage interface. func (a *AnnounceSignatures2) RandTestMessage(t *rapid.T) Message { var ( - chanID = RandChannelID(t) - scid = RandShortChannelID(t) - pSig = RandPartialSig(t) + chanID = RandChannelID(t) + scid = RandShortChannelID(t) + nodeSig = RandPartialSig(t) + bitcoinSig = RandPartialSig(t) + fundingTxID = RandChainHash(t) ) + sigs := NewAnnouncementSigPair(nodeSig.Sig, bitcoinSig.Sig) + msg := &AnnounceSignatures2{ ChannelID: tlv.NewRecordT[tlv.TlvType0, ChannelID]( chanID, ), ShortChannelID: tlv.NewRecordT[tlv.TlvType2](scid), - PartialSignature: tlv.NewRecordT[tlv.TlvType4, PartialSig]( - *pSig, + PartialSignatures: tlv.NewRecordT[ + tlv.TlvType4, AnnouncementSigPair, + ](sigs), + FundingTxID: tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte]( + [32]byte(fundingTxID), ), ExtraSignedFields: make(map[uint64][]byte), } @@ -563,13 +570,16 @@ func (c *ChannelUpdate2) RandTestMessage(t *rapid.T) Message { var chainHashObj chainhash.Hash copy(chainHashObj[:], chainHash[:]) + isSecondPeer := rapid.Bool().Draw(t, "isSecondPeer") + sciddir := NewSciddir(shortChanID, isSecondPeer) + //nolint:ll msg := &ChannelUpdate2{ ChainHash: tlv.NewPrimitiveRecord[tlv.TlvType0, chainhash.Hash]( chainHashObj, ), - ShortChannelID: tlv.NewRecordT[tlv.TlvType2, ShortChannelID]( - shortChanID, + ShortChannelID: tlv.NewRecordT[tlv.TlvType2, Sciddir]( + sciddir, ), BlockHeight: tlv.NewPrimitiveRecord[tlv.TlvType4, uint32]( blockHeight, @@ -596,26 +606,21 @@ func (c *ChannelUpdate2) RandTestMessage(t *rapid.T) Message { } if rapid.Bool().Draw(t, "includeInboundFee") { - base := rapid.IntRange(-1000, 1000).Draw(t, "inFeeBase") - rate := rapid.IntRange(-1000, 1000).Draw(t, "inFeeProp") - fee := Fee{ - BaseFee: int32(base), - FeeRate: int32(rate), - } - msg.InboundFee = tlv.SomeRecordT( - tlv.NewRecordT[tlv.TlvType55555](fee), + base := uint32( + rapid.IntRange(1, 0x7FFFFFFF).Draw(t, "inFeeBase"), + ) + rate := uint32( + rapid.IntRange(1, 0x7FFFFFFF).Draw(t, "inFeeProp"), ) + msg.InboundFeeBaseMsat = + tlv.NewPrimitiveRecord[tlv.TlvType20](base) + msg.InboundFeeProportionalMillionths = + tlv.NewPrimitiveRecord[tlv.TlvType22](rate) } msg.Signature.Val = RandSignature(t) msg.Signature.Val.ForceSchnorr() - if rapid.Bool().Draw(t, "isSecondPeer") { - msg.SecondPeer = tlv.SomeRecordT( - tlv.RecordT[tlv.TlvType8, TrueBoolean]{}, - ) - } - return msg } @@ -1283,22 +1288,20 @@ func (g *GossipTimestampRange) RandTestMessage(t *rapid.T) Message { ExtraData: RandExtraOpaqueData(t, nil), } - includeFirstBlockHeight := rapid.Bool().Draw( - t, "includeFirstBlockHeight", + includeBlockHeightRange := rapid.Bool().Draw( + t, "includeBlockHeightRange", ) - includeBlockRange := rapid.Bool().Draw(t, "includeBlockRange") - if includeFirstBlockHeight { + if includeBlockHeightRange { height := rapid.Uint32().Draw(t, "firstBlockHeight") - msg.FirstBlockHeight = tlv.SomeRecordT( - tlv.RecordT[tlv.TlvType2, uint32]{Val: height}, - ) - } - - if includeBlockRange { - blockRange := rapid.Uint32().Draw(t, "blockRange") - msg.BlockRange = tlv.SomeRecordT( - tlv.RecordT[tlv.TlvType4, uint32]{Val: blockRange}, + numBlocks := rapid.Uint32().Draw(t, "numBlocks") + msg.BlockHeightRange = tlv.SomeRecordT( + tlv.RecordT[tlv.TlvType2, BlockHeightRange]{ + Val: BlockHeightRange{ + FirstBlockHeight: height, + NumBlocks: numBlocks, + }, + }, ) }