Merge pull request #8159 from carlaKC/7298-1-forwardblindedroutes
[1/3]: Preparatory work for Forwarding Blinded Routes
This commit is contained in:
26 files changed
+1262
-160
No files matched your search
+67
-1
@@ -35,6 +35,10 @@ const (
|
|||||||
// begins to be interpreted as an absolute block height, rather than a
|
// begins to be interpreted as an absolute block height, rather than a
|
||||||
// relative one.
|
// relative one.
|
||||||
AbsoluteThawHeightThreshold uint32 = 500000
|
AbsoluteThawHeightThreshold uint32 = 500000
|
||||||
|
|
||||||
|
// HTLCBlindingPointTLV is the tlv type used for storing blinding
|
||||||
|
// points with HTLCs.
|
||||||
|
HTLCBlindingPointTLV tlv.Type = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -2316,7 +2320,56 @@ type HTLC struct {
|
|||||||
// Note that this extra data is stored inline with the OnionBlob for
|
// Note that this extra data is stored inline with the OnionBlob for
|
||||||
// legacy reasons, see serialization/deserialization functions for
|
// legacy reasons, see serialization/deserialization functions for
|
||||||
// detail.
|
// detail.
|
||||||
ExtraData []byte
|
ExtraData lnwire.ExtraOpaqueData
|
||||||
|
|
||||||
|
// BlindingPoint is an optional blinding point included with the HTLC.
|
||||||
|
//
|
||||||
|
// Note: this field is not a part of on-disk representation of the
|
||||||
|
// HTLC. It is stored in the ExtraData field, which is used to store
|
||||||
|
// a TLV stream of additional information associated with the HTLC.
|
||||||
|
BlindingPoint lnwire.BlindingPointRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeExtraData encodes a TLV stream of extra data to be stored with a
|
||||||
|
// HTLC. It uses the update_add_htlc TLV types, because this is where extra
|
||||||
|
// data is passed with a HTLC. At present blinding points are the only extra
|
||||||
|
// data that we will store, and the function is a no-op if a nil blinding
|
||||||
|
// point is provided.
|
||||||
|
//
|
||||||
|
// This function MUST be called to persist all HTLC values when they are
|
||||||
|
// serialized.
|
||||||
|
func (h *HTLC) serializeExtraData() error {
|
||||||
|
var records []tlv.RecordProducer
|
||||||
|
h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType,
|
||||||
|
*btcec.PublicKey]) {
|
||||||
|
|
||||||
|
records = append(records, &b)
|
||||||
|
})
|
||||||
|
|
||||||
|
return h.ExtraData.PackRecords(records...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserializeExtraData extracts TLVs from the extra data persisted for the
|
||||||
|
// htlc and populates values in the struct accordingly.
|
||||||
|
//
|
||||||
|
// This function MUST be called to populate the struct properly when HTLCs
|
||||||
|
// are deserialized.
|
||||||
|
func (h *HTLC) deserializeExtraData() error {
|
||||||
|
if len(h.ExtraData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
blindingPoint := h.BlindingPoint.Zero()
|
||||||
|
tlvMap, err := h.ExtraData.ExtractRecords(&blindingPoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil {
|
||||||
|
h.BlindingPoint = tlv.SomeRecordT(blindingPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerializeHtlcs writes out the passed set of HTLC's into the passed writer
|
// SerializeHtlcs writes out the passed set of HTLC's into the passed writer
|
||||||
@@ -2340,6 +2393,12 @@ func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, htlc := range htlcs {
|
for _, htlc := range htlcs {
|
||||||
|
// Populate TLV stream for any additional fields contained
|
||||||
|
// in the TLV.
|
||||||
|
if err := htlc.serializeExtraData(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// The onion blob and hltc data are stored as a single var
|
// The onion blob and hltc data are stored as a single var
|
||||||
// bytes blob.
|
// bytes blob.
|
||||||
onionAndExtraData := make(
|
onionAndExtraData := make(
|
||||||
@@ -2425,6 +2484,12 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) {
|
|||||||
onionAndExtraData[lnwire.OnionPacketSize:],
|
onionAndExtraData[lnwire.OnionPacketSize:],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, deserialize any TLVs contained in that extra data
|
||||||
|
// if they are present.
|
||||||
|
if err := htlcs[i].deserializeExtraData(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return htlcs, nil
|
return htlcs, nil
|
||||||
@@ -2440,6 +2505,7 @@ func (h *HTLC) Copy() HTLC {
|
|||||||
}
|
}
|
||||||
copy(clone.Signature[:], h.Signature)
|
copy(clone.Signature[:], h.Signature)
|
||||||
copy(clone.RHash[:], h.RHash[:])
|
copy(clone.RHash[:], h.RHash[:])
|
||||||
|
copy(clone.ExtraData, h.ExtraData)
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-34
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntest/channels"
|
"github.com/lightningnetwork/lnd/lntest/channels"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/shachain"
|
"github.com/lightningnetwork/lnd/shachain"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -651,8 +652,7 @@ func TestChannelStateTransition(t *testing.T) {
|
|||||||
{
|
{
|
||||||
LogIndex: 2,
|
LogIndex: 2,
|
||||||
UpdateMsg: &lnwire.UpdateAddHTLC{
|
UpdateMsg: &lnwire.UpdateAddHTLC{
|
||||||
ChanID: lnwire.ChannelID{1, 2, 3},
|
ChanID: lnwire.ChannelID{1, 2, 3},
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -710,25 +710,22 @@ func TestChannelStateTransition(t *testing.T) {
|
|||||||
wireSig,
|
wireSig,
|
||||||
wireSig,
|
wireSig,
|
||||||
},
|
},
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
},
|
},
|
||||||
LogUpdates: []LogUpdate{
|
LogUpdates: []LogUpdate{
|
||||||
{
|
{
|
||||||
LogIndex: 1,
|
LogIndex: 1,
|
||||||
UpdateMsg: &lnwire.UpdateAddHTLC{
|
UpdateMsg: &lnwire.UpdateAddHTLC{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Amount: lnwire.NewMSatFromSatoshis(100),
|
Amount: lnwire.NewMSatFromSatoshis(100),
|
||||||
Expiry: 25,
|
Expiry: 25,
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
LogIndex: 2,
|
LogIndex: 2,
|
||||||
UpdateMsg: &lnwire.UpdateAddHTLC{
|
UpdateMsg: &lnwire.UpdateAddHTLC{
|
||||||
ID: 2,
|
ID: 2,
|
||||||
Amount: lnwire.NewMSatFromSatoshis(200),
|
Amount: lnwire.NewMSatFromSatoshis(200),
|
||||||
Expiry: 50,
|
Expiry: 50,
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1610,9 +1607,25 @@ func TestHTLCsExtraData(t *testing.T) {
|
|||||||
OnionBlob: lnmock.MockOnion(),
|
OnionBlob: lnmock.MockOnion(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a blinding point to a htlc.
|
||||||
|
blindingPointHTLC := HTLC{
|
||||||
|
Signature: testSig.Serialize(),
|
||||||
|
Incoming: false,
|
||||||
|
Amt: 10,
|
||||||
|
RHash: key,
|
||||||
|
RefundTimeout: 1,
|
||||||
|
OnionBlob: lnmock.MockOnion(),
|
||||||
|
BlindingPoint: tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||||
|
pubKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
htlcs []HTLC
|
htlcs []HTLC
|
||||||
|
blindingIdx int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
// Serialize multiple HLTCs with no extra data to
|
// Serialize multiple HLTCs with no extra data to
|
||||||
@@ -1624,30 +1637,12 @@ func TestHTLCsExtraData(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Some HTLCs with extra data, some without.
|
||||||
name: "mixed extra data",
|
name: "mixed extra data",
|
||||||
htlcs: []HTLC{
|
htlcs: []HTLC{
|
||||||
mockHtlc,
|
mockHtlc,
|
||||||
{
|
blindingPointHTLC,
|
||||||
Signature: testSig.Serialize(),
|
|
||||||
Incoming: false,
|
|
||||||
Amt: 10,
|
|
||||||
RHash: key,
|
|
||||||
RefundTimeout: 1,
|
|
||||||
OnionBlob: lnmock.MockOnion(),
|
|
||||||
ExtraData: []byte{1, 2, 3},
|
|
||||||
},
|
|
||||||
mockHtlc,
|
mockHtlc,
|
||||||
{
|
|
||||||
Signature: testSig.Serialize(),
|
|
||||||
Incoming: false,
|
|
||||||
Amt: 10,
|
|
||||||
RHash: key,
|
|
||||||
RefundTimeout: 1,
|
|
||||||
OnionBlob: lnmock.MockOnion(),
|
|
||||||
ExtraData: bytes.Repeat(
|
|
||||||
[]byte{9}, 999,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1665,7 +1660,15 @@ func TestHTLCsExtraData(t *testing.T) {
|
|||||||
r := bytes.NewReader(b.Bytes())
|
r := bytes.NewReader(b.Bytes())
|
||||||
htlcs, err := DeserializeHtlcs(r)
|
htlcs, err := DeserializeHtlcs(r)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, testCase.htlcs, htlcs)
|
|
||||||
|
require.EqualValues(t, len(testCase.htlcs), len(htlcs))
|
||||||
|
for i, htlc := range htlcs {
|
||||||
|
// We use the extra data field when we
|
||||||
|
// serialize, so we set to nil to be able to
|
||||||
|
// assert on equal for the test.
|
||||||
|
htlc.ExtraData = nil
|
||||||
|
require.Equal(t, testCase.htlcs[i], htlc)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1250,9 +1250,9 @@ func parseBlindedPaymentParameters(ctx *cli.Context) (
|
|||||||
BaseFeeMsat: ctx.Uint64(
|
BaseFeeMsat: ctx.Uint64(
|
||||||
blindedBaseFlag.Name,
|
blindedBaseFlag.Name,
|
||||||
),
|
),
|
||||||
ProportionalFeeMsat: ctx.Uint64(
|
ProportionalFeeRate: uint32(ctx.Uint64(
|
||||||
blindedPPMFlag.Name,
|
blindedPPMFlag.Name,
|
||||||
),
|
)),
|
||||||
TotalCltvDelta: uint32(ctx.Uint64(
|
TotalCltvDelta: uint32(ctx.Uint64(
|
||||||
blindedCLTVFlag.Name,
|
blindedCLTVFlag.Name,
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/queue"
|
"github.com/lightningnetwork/lnd/queue"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// htlcIncomingContestResolver is a ContractResolver that's able to resolve an
|
// htlcIncomingContestResolver is a ContractResolver that's able to resolve an
|
||||||
@@ -520,9 +522,18 @@ func (h *htlcIncomingContestResolver) Supplement(htlc channeldb.HTLC) {
|
|||||||
func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload,
|
func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload,
|
||||||
[]byte, error) {
|
[]byte, error) {
|
||||||
|
|
||||||
|
var blindingPoint *btcec.PublicKey
|
||||||
|
h.htlc.BlindingPoint.WhenSome(
|
||||||
|
func(b tlv.RecordT[lnwire.BlindingPointTlvType,
|
||||||
|
*btcec.PublicKey]) {
|
||||||
|
|
||||||
|
blindingPoint = b.Val
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onionReader := bytes.NewReader(h.htlc.OnionBlob[:])
|
onionReader := bytes.NewReader(h.htlc.OnionBlob[:])
|
||||||
iterator, err := h.OnionProcessor.ReconstructHopIterator(
|
iterator, err := h.OnionProcessor.ReconstructHopIterator(
|
||||||
onionReader, h.htlc.RHash[:],
|
onionReader, h.htlc.RHash[:], blindingPoint,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
@@ -288,8 +289,8 @@ type mockOnionProcessor struct {
|
|||||||
offeredOnionBlob []byte
|
offeredOnionBlob []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *mockOnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
|
func (o *mockOnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
|
||||||
hop.Iterator, error) {
|
blindingPoint *btcec.PublicKey) (hop.Iterator, error) {
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(r)
|
data, err := ioutil.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
@@ -40,7 +41,8 @@ type Registry interface {
|
|||||||
type OnionProcessor interface {
|
type OnionProcessor interface {
|
||||||
// ReconstructHopIterator attempts to decode a valid sphinx packet from
|
// ReconstructHopIterator attempts to decode a valid sphinx packet from
|
||||||
// the passed io.Reader instance.
|
// the passed io.Reader instance.
|
||||||
ReconstructHopIterator(r io.Reader, rHash []byte) (hop.Iterator, error)
|
ReconstructHopIterator(r io.Reader, rHash []byte,
|
||||||
|
blindingKey *btcec.PublicKey) (hop.Iterator, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UtxoSweeper defines the sweep functions that contract court requires.
|
// UtxoSweeper defines the sweep functions that contract court requires.
|
||||||
|
|||||||
@@ -172,6 +172,9 @@
|
|||||||
* When computing a minimum fee for transaction construction, `lnd` [now takes our
|
* When computing a minimum fee for transaction construction, `lnd` [now takes our
|
||||||
bitcoin peers' feefilter values into account](https://github.com/lightningnetwork/lnd/pull/8418).
|
bitcoin peers' feefilter values into account](https://github.com/lightningnetwork/lnd/pull/8418).
|
||||||
|
|
||||||
|
* [Preparatory work](https://github.com/lightningnetwork/lnd/pull/8159) for
|
||||||
|
forwarding of blinded routes was added.
|
||||||
|
|
||||||
## RPC Additions
|
## RPC Additions
|
||||||
|
|
||||||
* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
|
* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
|
||||||
|
|||||||
+23
-51
@@ -147,67 +147,30 @@ func (p *OnionProcessor) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeHopIterator attempts to decode a valid sphinx packet from the passed io.Reader
|
|
||||||
// instance using the rHash as the associated data when checking the relevant
|
|
||||||
// MACs during the decoding process.
|
|
||||||
func (p *OnionProcessor) DecodeHopIterator(r io.Reader, rHash []byte,
|
|
||||||
incomingCltv uint32) (Iterator, lnwire.FailCode) {
|
|
||||||
|
|
||||||
onionPkt := &sphinx.OnionPacket{}
|
|
||||||
if err := onionPkt.Decode(r); err != nil {
|
|
||||||
switch err {
|
|
||||||
case sphinx.ErrInvalidOnionVersion:
|
|
||||||
return nil, lnwire.CodeInvalidOnionVersion
|
|
||||||
case sphinx.ErrInvalidOnionKey:
|
|
||||||
return nil, lnwire.CodeInvalidOnionKey
|
|
||||||
default:
|
|
||||||
log.Errorf("unable to decode onion packet: %v", err)
|
|
||||||
return nil, lnwire.CodeInvalidOnionKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to process the Sphinx packet. We include the payment hash of
|
|
||||||
// the HTLC as it's authenticated within the Sphinx packet itself as
|
|
||||||
// associated data in order to thwart attempts a replay attacks. In the
|
|
||||||
// case of a replay, an attacker is *forced* to use the same payment
|
|
||||||
// hash twice, thereby losing their money entirely.
|
|
||||||
sphinxPacket, err := p.router.ProcessOnionPacket(
|
|
||||||
onionPkt, rHash, incomingCltv,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case sphinx.ErrInvalidOnionVersion:
|
|
||||||
return nil, lnwire.CodeInvalidOnionVersion
|
|
||||||
case sphinx.ErrInvalidOnionHMAC:
|
|
||||||
return nil, lnwire.CodeInvalidOnionHmac
|
|
||||||
case sphinx.ErrInvalidOnionKey:
|
|
||||||
return nil, lnwire.CodeInvalidOnionKey
|
|
||||||
default:
|
|
||||||
log.Errorf("unable to process onion packet: %v", err)
|
|
||||||
return nil, lnwire.CodeInvalidOnionKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeSphinxHopIterator(onionPkt, sphinxPacket), lnwire.CodeNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReconstructHopIterator attempts to decode a valid sphinx packet from the passed io.Reader
|
// ReconstructHopIterator attempts to decode a valid sphinx packet from the passed io.Reader
|
||||||
// instance using the rHash as the associated data when checking the relevant
|
// instance using the rHash as the associated data when checking the relevant
|
||||||
// MACs during the decoding process.
|
// MACs during the decoding process.
|
||||||
func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
|
func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
|
||||||
Iterator, error) {
|
blindingPoint *btcec.PublicKey) (Iterator, error) {
|
||||||
|
|
||||||
onionPkt := &sphinx.OnionPacket{}
|
onionPkt := &sphinx.OnionPacket{}
|
||||||
if err := onionPkt.Decode(r); err != nil {
|
if err := onionPkt.Decode(r); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var opts []sphinx.ProcessOnionOpt
|
||||||
|
if blindingPoint != nil {
|
||||||
|
opts = append(opts, sphinx.WithBlindingPoint(blindingPoint))
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to process the Sphinx packet. We include the payment hash of
|
// Attempt to process the Sphinx packet. We include the payment hash of
|
||||||
// the HTLC as it's authenticated within the Sphinx packet itself as
|
// the HTLC as it's authenticated within the Sphinx packet itself as
|
||||||
// associated data in order to thwart attempts a replay attacks. In the
|
// associated data in order to thwart attempts a replay attacks. In the
|
||||||
// case of a replay, an attacker is *forced* to use the same payment
|
// case of a replay, an attacker is *forced* to use the same payment
|
||||||
// hash twice, thereby losing their money entirely.
|
// hash twice, thereby losing their money entirely.
|
||||||
sphinxPacket, err := p.router.ReconstructOnionPacket(onionPkt, rHash)
|
sphinxPacket, err := p.router.ReconstructOnionPacket(
|
||||||
|
onionPkt, rHash, opts...,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -219,9 +182,11 @@ func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
|
|||||||
// packet, perform sphinx replay detection, and schedule the entry for garbage
|
// packet, perform sphinx replay detection, and schedule the entry for garbage
|
||||||
// collection.
|
// collection.
|
||||||
type DecodeHopIteratorRequest struct {
|
type DecodeHopIteratorRequest struct {
|
||||||
OnionReader io.Reader
|
OnionReader io.Reader
|
||||||
RHash []byte
|
RHash []byte
|
||||||
IncomingCltv uint32
|
IncomingCltv uint32
|
||||||
|
IncomingAmount lnwire.MilliSatoshi
|
||||||
|
BlindingPoint *btcec.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion
|
// DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion
|
||||||
@@ -277,8 +242,15 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte,
|
|||||||
return lnwire.CodeInvalidOnionKey
|
return lnwire.CodeInvalidOnionKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var opts []sphinx.ProcessOnionOpt
|
||||||
|
if req.BlindingPoint != nil {
|
||||||
|
opts = append(opts, sphinx.WithBlindingPoint(
|
||||||
|
req.BlindingPoint,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
err = tx.ProcessOnionPacket(
|
err = tx.ProcessOnionPacket(
|
||||||
seqNum, onionPkt, req.RHash, req.IncomingCltv,
|
seqNum, onionPkt, req.RHash, req.IncomingCltv, opts...,
|
||||||
)
|
)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ const (
|
|||||||
// RequiredViolation indicates that an unknown even type was found in
|
// RequiredViolation indicates that an unknown even type was found in
|
||||||
// the payload that we could not process.
|
// the payload that we could not process.
|
||||||
RequiredViolation
|
RequiredViolation
|
||||||
|
|
||||||
|
// InsufficientViolation indicates that the provided type does
|
||||||
|
// not satisfy constraints.
|
||||||
|
InsufficientViolation
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a human-readable description of the violation as a verb.
|
// String returns a human-readable description of the violation as a verb.
|
||||||
@@ -42,6 +46,9 @@ func (v PayloadViolation) String() string {
|
|||||||
case RequiredViolation:
|
case RequiredViolation:
|
||||||
return "required"
|
return "required"
|
||||||
|
|
||||||
|
case InsufficientViolation:
|
||||||
|
return "insufficient"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "unknown violation"
|
return "unknown violation"
|
||||||
}
|
}
|
||||||
@@ -410,3 +417,70 @@ func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateBlindedRouteData performs the additional validation that is
|
||||||
|
// required for payments that rely on data provided in an encrypted blob to
|
||||||
|
// be forwarded. We enforce the blinded route's maximum expiry height so that
|
||||||
|
// the route "expires" and a malicious party does not have endless opportunity
|
||||||
|
// to probe the blinded route and compare it to updated channel policies in
|
||||||
|
// the network.
|
||||||
|
//
|
||||||
|
// Note that this function only validates blinded route data for forwarding
|
||||||
|
// nodes, as LND does not yet support receiving via a blinded route (which has
|
||||||
|
// different validation rules).
|
||||||
|
func ValidateBlindedRouteData(blindedData *record.BlindedRouteData,
|
||||||
|
incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error {
|
||||||
|
|
||||||
|
// Bolt 04 notes that we should enforce payment constraints _if_ they
|
||||||
|
// are present, so we do not fail if not provided.
|
||||||
|
var err error
|
||||||
|
blindedData.Constraints.WhenSome(
|
||||||
|
func(c tlv.RecordT[tlv.TlvType12, record.PaymentConstraints]) {
|
||||||
|
// MUST fail if the expiry is greater than
|
||||||
|
// max_cltv_expiry.
|
||||||
|
if incomingTimelock > c.Val.MaxCltvExpiry {
|
||||||
|
err = ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: InsufficientViolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MUST fail if the amount is below htlc_minimum_msat.
|
||||||
|
if incomingAmount < c.Val.HtlcMinimumMsat {
|
||||||
|
err = ErrInvalidPayload{
|
||||||
|
Type: record.AmtOnionType,
|
||||||
|
Violation: InsufficientViolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if we don't understand any features (even or odd), because we
|
||||||
|
// expect the features to have been set from our announcement. If the
|
||||||
|
// feature vector TLV is not included, it's interpreted as an empty
|
||||||
|
// vector (no validation required).
|
||||||
|
// expect the features to have been set from our announcement.
|
||||||
|
//
|
||||||
|
// Note that we do not yet check the features that the blinded payment
|
||||||
|
// is using against our own features, because there are currently no
|
||||||
|
// payment-related features that they utilize other than tlv-onion,
|
||||||
|
// which is implicitly supported.
|
||||||
|
blindedData.Features.WhenSome(
|
||||||
|
func(f tlv.RecordT[tlv.TlvType14, lnwire.FeatureVector]) {
|
||||||
|
if f.Val.UnknownFeatures() {
|
||||||
|
err = ErrInvalidPayload{
|
||||||
|
Type: 14,
|
||||||
|
Violation: IncludedViolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -557,3 +557,141 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
|
|||||||
t.Fatalf("invalid custom records")
|
t.Fatalf("invalid custom records")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestValidateBlindedRouteData tests validation of the values provided in a
|
||||||
|
// blinded route.
|
||||||
|
func TestValidateBlindedRouteData(t *testing.T) {
|
||||||
|
scid := lnwire.NewShortChanIDFromInt(1)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data *record.BlindedRouteData
|
||||||
|
incomingAmount lnwire.MilliSatoshi
|
||||||
|
incomingTimelock uint32
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "max cltv expired",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingTimelock: 200,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: hop.InsufficientViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero max cltv",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 0,
|
||||||
|
HtlcMinimumMsat: 10,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 100,
|
||||||
|
incomingTimelock: 10,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: hop.InsufficientViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "amount below minimum",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
HtlcMinimumMsat: 15,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 10,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: record.AmtOnionType,
|
||||||
|
Violation: hop.InsufficientViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid, no features",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
HtlcMinimumMsat: 20,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 40,
|
||||||
|
incomingTimelock: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown features",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
HtlcMinimumMsat: 20,
|
||||||
|
},
|
||||||
|
lnwire.NewFeatureVector(
|
||||||
|
lnwire.NewRawFeatureVector(
|
||||||
|
lnwire.FeatureBit(9999),
|
||||||
|
),
|
||||||
|
lnwire.Features,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
incomingAmount: 40,
|
||||||
|
incomingTimelock: 80,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: 14,
|
||||||
|
Violation: hop.IncludedViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid data",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{
|
||||||
|
CltvExpiryDelta: 10,
|
||||||
|
FeeRate: 10,
|
||||||
|
BaseFee: 100,
|
||||||
|
},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
HtlcMinimumMsat: 20,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 40,
|
||||||
|
incomingTimelock: 80,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range tests {
|
||||||
|
testCase := testCase
|
||||||
|
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
err := hop.ValidateBlindedRouteData(
|
||||||
|
testCase.data, testCase.incomingAmount,
|
||||||
|
testCase.incomingTimelock,
|
||||||
|
)
|
||||||
|
require.Equal(t, testCase.err, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-3
@@ -3160,9 +3160,11 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
|
|||||||
onionReader := bytes.NewReader(pd.OnionBlob)
|
onionReader := bytes.NewReader(pd.OnionBlob)
|
||||||
|
|
||||||
req := hop.DecodeHopIteratorRequest{
|
req := hop.DecodeHopIteratorRequest{
|
||||||
OnionReader: onionReader,
|
OnionReader: onionReader,
|
||||||
RHash: pd.RHash[:],
|
RHash: pd.RHash[:],
|
||||||
IncomingCltv: pd.Timeout,
|
IncomingCltv: pd.Timeout,
|
||||||
|
IncomingAmount: pd.Amount,
|
||||||
|
BlindingPoint: pd.BlindingPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeReqs = append(decodeReqs, req)
|
decodeReqs = append(decodeReqs, req)
|
||||||
|
|||||||
@@ -11992,8 +11992,9 @@ type BlindedPaymentPath struct {
|
|||||||
BlindedPath *BlindedPath `protobuf:"bytes,1,opt,name=blinded_path,json=blindedPath,proto3" json:"blinded_path,omitempty"`
|
BlindedPath *BlindedPath `protobuf:"bytes,1,opt,name=blinded_path,json=blindedPath,proto3" json:"blinded_path,omitempty"`
|
||||||
// The base fee for the blinded path provided, expressed in msat.
|
// The base fee for the blinded path provided, expressed in msat.
|
||||||
BaseFeeMsat uint64 `protobuf:"varint,2,opt,name=base_fee_msat,json=baseFeeMsat,proto3" json:"base_fee_msat,omitempty"`
|
BaseFeeMsat uint64 `protobuf:"varint,2,opt,name=base_fee_msat,json=baseFeeMsat,proto3" json:"base_fee_msat,omitempty"`
|
||||||
// The proportional fee for the blinded path provided, expressed in msat.
|
// The proportional fee for the blinded path provided, expressed in parts
|
||||||
ProportionalFeeMsat uint64 `protobuf:"varint,3,opt,name=proportional_fee_msat,json=proportionalFeeMsat,proto3" json:"proportional_fee_msat,omitempty"`
|
// per million.
|
||||||
|
ProportionalFeeRate uint32 `protobuf:"varint,3,opt,name=proportional_fee_rate,json=proportionalFeeRate,proto3" json:"proportional_fee_rate,omitempty"`
|
||||||
// The total CLTV delta for the blinded path provided, including the
|
// The total CLTV delta for the blinded path provided, including the
|
||||||
// final CLTV delta for the receiving node.
|
// final CLTV delta for the receiving node.
|
||||||
TotalCltvDelta uint32 `protobuf:"varint,4,opt,name=total_cltv_delta,json=totalCltvDelta,proto3" json:"total_cltv_delta,omitempty"`
|
TotalCltvDelta uint32 `protobuf:"varint,4,opt,name=total_cltv_delta,json=totalCltvDelta,proto3" json:"total_cltv_delta,omitempty"`
|
||||||
@@ -12053,9 +12054,9 @@ func (x *BlindedPaymentPath) GetBaseFeeMsat() uint64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *BlindedPaymentPath) GetProportionalFeeMsat() uint64 {
|
func (x *BlindedPaymentPath) GetProportionalFeeRate() uint32 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ProportionalFeeMsat
|
return x.ProportionalFeeRate
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -19611,9 +19612,9 @@ var file_lightning_proto_rawDesc = []byte{
|
|||||||
0x74, 0x68, 0x12, 0x22, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d,
|
0x74, 0x68, 0x12, 0x22, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d,
|
||||||
0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46,
|
0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46,
|
||||||
0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72,
|
0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72,
|
||||||
0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18,
|
0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18,
|
||||||
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f,
|
0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f,
|
||||||
0x6e, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f,
|
0x6e, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f,
|
||||||
0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x04,
|
0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x04,
|
||||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6c, 0x74, 0x76, 0x44,
|
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6c, 0x74, 0x76, 0x44,
|
||||||
0x65, 0x6c, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0d, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x69, 0x6e,
|
0x65, 0x6c, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0d, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x69, 0x6e,
|
||||||
|
|||||||
@@ -3545,8 +3545,11 @@ message BlindedPaymentPath {
|
|||||||
// The base fee for the blinded path provided, expressed in msat.
|
// The base fee for the blinded path provided, expressed in msat.
|
||||||
uint64 base_fee_msat = 2;
|
uint64 base_fee_msat = 2;
|
||||||
|
|
||||||
// The proportional fee for the blinded path provided, expressed in msat.
|
/*
|
||||||
uint64 proportional_fee_msat = 3;
|
The proportional fee for the blinded path provided, expressed in parts
|
||||||
|
per million.
|
||||||
|
*/
|
||||||
|
uint32 proportional_fee_rate = 3;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The total CLTV delta for the blinded path provided, including the
|
The total CLTV delta for the blinded path provided, including the
|
||||||
|
|||||||
@@ -3523,10 +3523,10 @@
|
|||||||
"format": "uint64",
|
"format": "uint64",
|
||||||
"description": "The base fee for the blinded path provided, expressed in msat."
|
"description": "The base fee for the blinded path provided, expressed in msat."
|
||||||
},
|
},
|
||||||
"proportional_fee_msat": {
|
"proportional_fee_rate": {
|
||||||
"type": "string",
|
"type": "integer",
|
||||||
"format": "uint64",
|
"format": "int64",
|
||||||
"description": "The proportional fee for the blinded path provided, expressed in msat."
|
"description": "The proportional fee for the blinded path provided, expressed in parts\nper million."
|
||||||
},
|
},
|
||||||
"total_cltv_delta": {
|
"total_cltv_delta": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|||||||
@@ -487,15 +487,13 @@ func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &routing.BlindedPayment{
|
return &routing.BlindedPayment{
|
||||||
BlindedPath: path,
|
BlindedPath: path,
|
||||||
CltvExpiryDelta: uint16(rpcPayment.TotalCltvDelta),
|
CltvExpiryDelta: uint16(rpcPayment.TotalCltvDelta),
|
||||||
BaseFee: uint32(rpcPayment.BaseFeeMsat),
|
BaseFee: uint32(rpcPayment.BaseFeeMsat),
|
||||||
ProportionalFee: uint32(
|
ProportionalFeeRate: rpcPayment.ProportionalFeeRate,
|
||||||
rpcPayment.ProportionalFeeMsat,
|
HtlcMinimum: rpcPayment.HtlcMinMsat,
|
||||||
),
|
HtlcMaximum: rpcPayment.HtlcMaxMsat,
|
||||||
HtlcMinimum: rpcPayment.HtlcMinMsat,
|
Features: features,
|
||||||
HtlcMaximum: rpcPayment.HtlcMaxMsat,
|
|
||||||
Features: features,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+71
-13
@@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/shachain"
|
"github.com/lightningnetwork/lnd/shachain"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -371,6 +372,12 @@ type PaymentDescriptor struct {
|
|||||||
// isForwarded denotes if an incoming HTLC has been forwarded to any
|
// isForwarded denotes if an incoming HTLC has been forwarded to any
|
||||||
// possible upstream peers in the route.
|
// possible upstream peers in the route.
|
||||||
isForwarded bool
|
isForwarded bool
|
||||||
|
|
||||||
|
// BlindingPoint is an optional ephemeral key used in route blinding.
|
||||||
|
// This value is set for nodes that are relaying payments inside of a
|
||||||
|
// blinded route (ie, not the introduction node) from update_add_htlc's
|
||||||
|
// TLVs.
|
||||||
|
BlindingPoint *btcec.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// PayDescsFromRemoteLogUpdates converts a slice of LogUpdates received from the
|
// PayDescsFromRemoteLogUpdates converts a slice of LogUpdates received from the
|
||||||
@@ -411,6 +418,7 @@ func PayDescsFromRemoteLogUpdates(chanID lnwire.ShortChannelID, height uint64,
|
|||||||
Height: height,
|
Height: height,
|
||||||
Index: uint16(i),
|
Index: uint16(i),
|
||||||
},
|
},
|
||||||
|
BlindingPoint: wireMsg.BlingingPointOrNil(),
|
||||||
}
|
}
|
||||||
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
|
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
|
||||||
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
|
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
|
||||||
@@ -736,6 +744,14 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment {
|
|||||||
Incoming: false,
|
Incoming: false,
|
||||||
}
|
}
|
||||||
copy(h.OnionBlob[:], htlc.OnionBlob)
|
copy(h.OnionBlob[:], htlc.OnionBlob)
|
||||||
|
if htlc.BlindingPoint != nil {
|
||||||
|
h.BlindingPoint = tlv.SomeRecordT(
|
||||||
|
//nolint:lll
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||||
|
htlc.BlindingPoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if ourCommit && htlc.sig != nil {
|
if ourCommit && htlc.sig != nil {
|
||||||
h.Signature = htlc.sig.Serialize()
|
h.Signature = htlc.sig.Serialize()
|
||||||
@@ -760,7 +776,14 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment {
|
|||||||
Incoming: true,
|
Incoming: true,
|
||||||
}
|
}
|
||||||
copy(h.OnionBlob[:], htlc.OnionBlob)
|
copy(h.OnionBlob[:], htlc.OnionBlob)
|
||||||
|
if htlc.BlindingPoint != nil {
|
||||||
|
h.BlindingPoint = tlv.SomeRecordT(
|
||||||
|
//nolint:lll
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||||
|
htlc.BlindingPoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
if ourCommit && htlc.sig != nil {
|
if ourCommit && htlc.sig != nil {
|
||||||
h.Signature = htlc.sig.Serialize()
|
h.Signature = htlc.sig.Serialize()
|
||||||
}
|
}
|
||||||
@@ -859,6 +882,12 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
|
|||||||
theirWitnessScript: theirWitnessScript,
|
theirWitnessScript: theirWitnessScript,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htlc.BlindingPoint.WhenSome(func(b tlv.RecordT[
|
||||||
|
lnwire.BlindingPointTlvType, *btcec.PublicKey]) {
|
||||||
|
|
||||||
|
pd.BlindingPoint = b.Val
|
||||||
|
})
|
||||||
|
|
||||||
return pd, nil
|
return pd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1548,6 +1577,7 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
|
|||||||
HtlcIndex: wireMsg.ID,
|
HtlcIndex: wireMsg.ID,
|
||||||
LogIndex: logUpdate.LogIndex,
|
LogIndex: logUpdate.LogIndex,
|
||||||
addCommitHeightRemote: commitHeight,
|
addCommitHeightRemote: commitHeight,
|
||||||
|
BlindingPoint: wireMsg.BlingingPointOrNil(),
|
||||||
}
|
}
|
||||||
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
|
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
|
||||||
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
|
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
|
||||||
@@ -1745,6 +1775,7 @@ func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpd
|
|||||||
HtlcIndex: wireMsg.ID,
|
HtlcIndex: wireMsg.ID,
|
||||||
LogIndex: logUpdate.LogIndex,
|
LogIndex: logUpdate.LogIndex,
|
||||||
addCommitHeightLocal: commitHeight,
|
addCommitHeightLocal: commitHeight,
|
||||||
|
BlindingPoint: wireMsg.BlingingPointOrNil(),
|
||||||
}
|
}
|
||||||
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
|
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
|
||||||
copy(pd.OnionBlob, wireMsg.OnionBlob[:])
|
copy(pd.OnionBlob, wireMsg.OnionBlob[:])
|
||||||
@@ -3607,6 +3638,14 @@ func (lc *LightningChannel) createCommitDiff(
|
|||||||
PaymentHash: pd.RHash,
|
PaymentHash: pd.RHash,
|
||||||
}
|
}
|
||||||
copy(htlc.OnionBlob[:], pd.OnionBlob)
|
copy(htlc.OnionBlob[:], pd.OnionBlob)
|
||||||
|
if pd.BlindingPoint != nil {
|
||||||
|
htlc.BlindingPoint = tlv.SomeRecordT(
|
||||||
|
//nolint:lll
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||||
|
pd.BlindingPoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
logUpdate.UpdateMsg = htlc
|
logUpdate.UpdateMsg = htlc
|
||||||
|
|
||||||
// Gather any references for circuits opened by this Add
|
// Gather any references for circuits opened by this Add
|
||||||
@@ -3736,12 +3775,21 @@ func (lc *LightningChannel) getUnsignedAckedUpdates() []channeldb.LogUpdate {
|
|||||||
// four messages that it corresponds to.
|
// four messages that it corresponds to.
|
||||||
switch pd.EntryType {
|
switch pd.EntryType {
|
||||||
case Add:
|
case Add:
|
||||||
|
var b lnwire.BlindingPointRecord
|
||||||
|
if pd.BlindingPoint != nil {
|
||||||
|
tlv.SomeRecordT(
|
||||||
|
//nolint:lll
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](pd.BlindingPoint),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
htlc := &lnwire.UpdateAddHTLC{
|
htlc := &lnwire.UpdateAddHTLC{
|
||||||
ChanID: chanID,
|
ChanID: chanID,
|
||||||
ID: pd.HtlcIndex,
|
ID: pd.HtlcIndex,
|
||||||
Amount: pd.Amount,
|
Amount: pd.Amount,
|
||||||
Expiry: pd.Timeout,
|
Expiry: pd.Timeout,
|
||||||
PaymentHash: pd.RHash,
|
PaymentHash: pd.RHash,
|
||||||
|
BlindingPoint: b,
|
||||||
}
|
}
|
||||||
copy(htlc.OnionBlob[:], pd.OnionBlob)
|
copy(htlc.OnionBlob[:], pd.OnionBlob)
|
||||||
logUpdate.UpdateMsg = htlc
|
logUpdate.UpdateMsg = htlc
|
||||||
@@ -5742,6 +5790,14 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
|
|||||||
Expiry: pd.Timeout,
|
Expiry: pd.Timeout,
|
||||||
PaymentHash: pd.RHash,
|
PaymentHash: pd.RHash,
|
||||||
}
|
}
|
||||||
|
if pd.BlindingPoint != nil {
|
||||||
|
htlc.BlindingPoint = tlv.SomeRecordT(
|
||||||
|
//nolint:lll
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||||
|
pd.BlindingPoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
copy(htlc.OnionBlob[:], pd.OnionBlob)
|
copy(htlc.OnionBlob[:], pd.OnionBlob)
|
||||||
logUpdate.UpdateMsg = htlc
|
logUpdate.UpdateMsg = htlc
|
||||||
addUpdates = append(addUpdates, logUpdate)
|
addUpdates = append(addUpdates, logUpdate)
|
||||||
@@ -6079,6 +6135,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
|
|||||||
HtlcIndex: lc.localUpdateLog.htlcCounter,
|
HtlcIndex: lc.localUpdateLog.htlcCounter,
|
||||||
OnionBlob: htlc.OnionBlob[:],
|
OnionBlob: htlc.OnionBlob[:],
|
||||||
OpenCircuitKey: openKey,
|
OpenCircuitKey: openKey,
|
||||||
|
BlindingPoint: htlc.BlingingPointOrNil(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6129,13 +6186,14 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
pd := &PaymentDescriptor{
|
pd := &PaymentDescriptor{
|
||||||
EntryType: Add,
|
EntryType: Add,
|
||||||
RHash: PaymentHash(htlc.PaymentHash),
|
RHash: PaymentHash(htlc.PaymentHash),
|
||||||
Timeout: htlc.Expiry,
|
Timeout: htlc.Expiry,
|
||||||
Amount: htlc.Amount,
|
Amount: htlc.Amount,
|
||||||
LogIndex: lc.remoteUpdateLog.logIndex,
|
LogIndex: lc.remoteUpdateLog.logIndex,
|
||||||
HtlcIndex: lc.remoteUpdateLog.htlcCounter,
|
HtlcIndex: lc.remoteUpdateLog.htlcCounter,
|
||||||
OnionBlob: htlc.OnionBlob[:],
|
OnionBlob: htlc.OnionBlob[:],
|
||||||
|
BlindingPoint: htlc.BlingingPointOrNil(),
|
||||||
}
|
}
|
||||||
|
|
||||||
localACKedIndex := lc.remoteCommitChain.tail().ourMessageIndex
|
localACKedIndex := lc.remoteCommitChain.tail().ourMessageIndex
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3037,7 +3038,6 @@ func TestChanSyncOweCommitment(t *testing.T) {
|
|||||||
Amount: htlcAmt,
|
Amount: htlcAmt,
|
||||||
Expiry: uint32(10),
|
Expiry: uint32(10),
|
||||||
OnionBlob: fakeOnionBlob,
|
OnionBlob: fakeOnionBlob,
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
htlcIndex, err := bobChannel.AddHTLC(h, nil)
|
htlcIndex, err := bobChannel.AddHTLC(h, nil)
|
||||||
@@ -3082,7 +3082,6 @@ func TestChanSyncOweCommitment(t *testing.T) {
|
|||||||
Amount: htlcAmt,
|
Amount: htlcAmt,
|
||||||
Expiry: uint32(10),
|
Expiry: uint32(10),
|
||||||
OnionBlob: fakeOnionBlob,
|
OnionBlob: fakeOnionBlob,
|
||||||
ExtraData: make([]byte, 0),
|
|
||||||
}
|
}
|
||||||
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
|
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
|
||||||
require.NoError(t, err, "unable to add alice's htlc")
|
require.NoError(t, err, "unable to add alice's htlc")
|
||||||
@@ -10421,8 +10420,9 @@ func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC {
|
|||||||
_, err = rand.Read(sig)
|
_, err = rand.Read(sig)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
extra := make([]byte, 1000)
|
blinding, err := pubkeyFromHex(
|
||||||
_, err = rand.Read(extra)
|
"0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d48236c39", //nolint:lll
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return channeldb.HTLC{
|
return channeldb.HTLC{
|
||||||
@@ -10435,7 +10435,10 @@ func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC {
|
|||||||
OnionBlob: onionBlob,
|
OnionBlob: onionBlob,
|
||||||
HtlcIndex: rand.Uint64(),
|
HtlcIndex: rand.Uint64(),
|
||||||
LogIndex: rand.Uint64(),
|
LogIndex: rand.Uint64(),
|
||||||
ExtraData: extra,
|
BlindingPoint: tlv.SomeRecordT(
|
||||||
|
//nolint:lll
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](blinding),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11002,3 +11005,61 @@ func TestEnforceFeeBuffer(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, aliceBalance, expectedAmt)
|
require.Equal(t, aliceBalance, expectedAmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBlindingPointPersistence tests persistence of blinding points attached
|
||||||
|
// to htlcs across restarts.
|
||||||
|
func TestBlindingPointPersistence(t *testing.T) {
|
||||||
|
// Create a test channel which will be used for the duration of this
|
||||||
|
// test. The channel will be funded evenly with Alice having 5 BTC,
|
||||||
|
// and Bob having 5 BTC.
|
||||||
|
aliceChannel, bobChannel, err := CreateTestChannels(
|
||||||
|
t, channeldb.SingleFunderTweaklessBit,
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "unable to create test channels")
|
||||||
|
|
||||||
|
// Send a HTLC from Alice to Bob that has a blinding point populated.
|
||||||
|
htlc, _ := createHTLC(0, 100_000_000)
|
||||||
|
blinding, err := pubkeyFromHex(
|
||||||
|
"0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d48236c39", //nolint:lll
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
htlc.BlindingPoint = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](blinding),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err = aliceChannel.AddHTLC(htlc, nil)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = bobChannel.ReceiveHTLC(htlc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now, Alice will send a new commitment to Bob, which will persist our
|
||||||
|
// pending HTLC to disk.
|
||||||
|
aliceCommit, err := aliceChannel.SignNextCommitment()
|
||||||
|
require.NoError(t, err, "unable to sign commitment")
|
||||||
|
|
||||||
|
// Restart alice to force fetching state from disk.
|
||||||
|
aliceChannel, err = restartChannel(aliceChannel)
|
||||||
|
require.NoError(t, err, "unable to restart alice")
|
||||||
|
|
||||||
|
// Assert that the blinding point is restored from disk.
|
||||||
|
remoteCommit := aliceChannel.remoteCommitChain.tip()
|
||||||
|
require.Len(t, remoteCommit.outgoingHTLCs, 1)
|
||||||
|
require.Equal(t, blinding, remoteCommit.outgoingHTLCs[0].BlindingPoint)
|
||||||
|
|
||||||
|
// Next, update bob's commitment and assert that we can still retrieve
|
||||||
|
// his incoming blinding point after restart.
|
||||||
|
err = bobChannel.ReceiveNewCommitment(aliceCommit.CommitSigs)
|
||||||
|
require.NoError(t, err, "bob unable to receive new commitment")
|
||||||
|
|
||||||
|
_, _, _, err = bobChannel.RevokeCurrentCommitment()
|
||||||
|
require.NoError(t, err, "bob unable to revoke current commitment")
|
||||||
|
|
||||||
|
bobChannel, err = restartChannel(bobChannel)
|
||||||
|
require.NoError(t, err, "unable to restart bob's channel")
|
||||||
|
|
||||||
|
// Assert that Bob is able to recover the blinding point from disk.
|
||||||
|
bobCommit := bobChannel.localCommitChain.tip()
|
||||||
|
require.Len(t, bobCommit.incomingHTLCs, 1)
|
||||||
|
require.Equal(t, blinding, bobCommit.incomingHTLCs[0].BlindingPoint)
|
||||||
|
}
|
||||||
+10
-8
@@ -19,7 +19,7 @@ type ChannelType RawFeatureVector
|
|||||||
// featureBitLen returns the length in bytes of the encoded feature bits.
|
// featureBitLen returns the length in bytes of the encoded feature bits.
|
||||||
func (c ChannelType) featureBitLen() uint64 {
|
func (c ChannelType) featureBitLen() uint64 {
|
||||||
fv := RawFeatureVector(c)
|
fv := RawFeatureVector(c)
|
||||||
return uint64(fv.SerializeSize())
|
return fv.sizeFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record returns a TLV record that can be used to encode/decode the channel
|
// Record returns a TLV record that can be used to encode/decode the channel
|
||||||
@@ -34,25 +34,27 @@ func (c *ChannelType) Record() tlv.Record {
|
|||||||
// channelTypeEncoder is a custom TLV encoder for the ChannelType record.
|
// channelTypeEncoder is a custom TLV encoder for the ChannelType record.
|
||||||
func channelTypeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
|
func channelTypeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||||
if v, ok := val.(*ChannelType); ok {
|
if v, ok := val.(*ChannelType); ok {
|
||||||
// Encode the feature bits as a byte slice without its length
|
|
||||||
// prepended, as that's already taken care of by the TLV record.
|
|
||||||
fv := RawFeatureVector(*v)
|
fv := RawFeatureVector(*v)
|
||||||
return fv.encode(w, fv.SerializeSize(), 8)
|
return rawFeatureEncoder(w, &fv, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tlv.NewTypeForEncodingErr(val, "lnwire.ChannelType")
|
return tlv.NewTypeForEncodingErr(val, "*lnwire.ChannelType")
|
||||||
}
|
}
|
||||||
|
|
||||||
// channelTypeDecoder is a custom TLV decoder for the ChannelType record.
|
// channelTypeDecoder is a custom TLV decoder for the ChannelType record.
|
||||||
func channelTypeDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
|
func channelTypeDecoder(r io.Reader, val interface{}, buf *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
if v, ok := val.(*ChannelType); ok {
|
if v, ok := val.(*ChannelType); ok {
|
||||||
fv := NewRawFeatureVector()
|
fv := NewRawFeatureVector()
|
||||||
if err := fv.decode(r, int(l), 8); err != nil {
|
|
||||||
|
if err := rawFeatureDecoder(r, fv, buf, l); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*v = ChannelType(*fv)
|
*v = ChannelType(*fv)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return tlv.NewTypeForEncodingErr(val, "lnwire.ChannelType")
|
return tlv.NewTypeForEncodingErr(val, "*lnwire.ChannelType")
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -612,6 +614,41 @@ func (fv *RawFeatureVector) decode(r io.Reader, length, width int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sizeFunc returns the length required to encode the feature vector.
|
||||||
|
func (fv *RawFeatureVector) sizeFunc() uint64 {
|
||||||
|
return uint64(fv.SerializeSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record returns a TLV record that can be used to encode/decode raw feature
|
||||||
|
// vectors. Note that the length of the feature vector is not included, because
|
||||||
|
// it is covered by the TLV record's length field.
|
||||||
|
func (fv *RawFeatureVector) Record(recordType tlv.Type) tlv.Record {
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
recordType, fv, fv.sizeFunc, rawFeatureEncoder,
|
||||||
|
rawFeatureDecoder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawFeatureEncoder is a custom TLV encoder for raw feature vectors.
|
||||||
|
func rawFeatureEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
|
||||||
|
if f, ok := val.(*RawFeatureVector); ok {
|
||||||
|
return f.encode(w, f.SerializeSize(), 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "*lnwire.RawFeatureVector")
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawFeatureDecoder is a custom TLV decoder for raw feature vectors.
|
||||||
|
func rawFeatureDecoder(r io.Reader, val interface{}, _ *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
|
if f, ok := val.(*RawFeatureVector); ok {
|
||||||
|
return f.decode(r, int(l), 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "*lnwire.RawFeatureVector")
|
||||||
|
}
|
||||||
|
|
||||||
// FeatureVector represents a set of enabled features. The set stores
|
// FeatureVector represents a set of enabled features. The set stores
|
||||||
// information on enabled flags and metadata about the feature names. A feature
|
// information on enabled flags and metadata about the feature names. A feature
|
||||||
// vector is serializable to a compact byte representation that is included in
|
// vector is serializable to a compact byte representation that is included in
|
||||||
@@ -641,6 +678,50 @@ func EmptyFeatureVector() *FeatureVector {
|
|||||||
return NewFeatureVector(nil, Features)
|
return NewFeatureVector(nil, Features)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record implements the RecordProducer interface for FeatureVector. Note that
|
||||||
|
// it uses a zero-value type is used to produce the record, as we expect this
|
||||||
|
// type value to be overwritten when used in generic TLV record production.
|
||||||
|
// This allows a single Record function to serve in the many different contexts
|
||||||
|
// in which feature vectors are encoded. This record wraps the encoding/
|
||||||
|
// decoding for our raw feature vectors so that we can directly parse fully
|
||||||
|
// formed feature vector types.
|
||||||
|
func (fv *FeatureVector) Record() tlv.Record {
|
||||||
|
return tlv.MakeDynamicRecord(0, fv, fv.sizeFunc,
|
||||||
|
func(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||||
|
if f, ok := val.(*FeatureVector); ok {
|
||||||
|
return rawFeatureEncoder(
|
||||||
|
w, f.RawFeatureVector, buf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForEncodingErr(
|
||||||
|
val, "*lnwire.FeatureVector",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
func(r io.Reader, val interface{}, buf *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
|
if f, ok := val.(*FeatureVector); ok {
|
||||||
|
features := NewFeatureVector(nil, Features)
|
||||||
|
err := rawFeatureDecoder(
|
||||||
|
r, features.RawFeatureVector, buf, l,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*f = *features
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForDecodingErr(
|
||||||
|
val, "*lnwire.FeatureVector", l, l,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// HasFeature returns whether a particular feature is included in the set. The
|
// HasFeature returns whether a particular feature is included in the set. The
|
||||||
// feature can be seen as set either if the bit is set directly OR the queried
|
// feature can be seen as set either if the bit is set directly OR the queried
|
||||||
// bit has the same meaning as its corresponding even/odd bit, which is set
|
// bit has the same meaning as its corresponding even/odd bit, which is set
|
||||||
@@ -678,6 +759,18 @@ func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
|
|||||||
return unknown
|
return unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnknownFeatures returns a boolean if a feature vector contains *any*
|
||||||
|
// unknown features (even if they are odd).
|
||||||
|
func (fv *FeatureVector) UnknownFeatures() bool {
|
||||||
|
for feature := range fv.features {
|
||||||
|
if !fv.IsKnown(feature) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Name returns a string identifier for the feature represented by this bit. If
|
// Name returns a string identifier for the feature represented by this bit. If
|
||||||
// the bit does not represent a known feature, this returns a string indicating
|
// the bit does not represent a known feature, this returns a string indicating
|
||||||
// as such.
|
// as such.
|
||||||
|
|||||||
@@ -1337,6 +1337,42 @@ func TestLightningWireProtocol(t *testing.T) {
|
|||||||
|
|
||||||
v[0] = reflect.ValueOf(req)
|
v[0] = reflect.ValueOf(req)
|
||||||
},
|
},
|
||||||
|
MsgUpdateAddHTLC: func(v []reflect.Value, r *rand.Rand) {
|
||||||
|
req := &UpdateAddHTLC{
|
||||||
|
ID: r.Uint64(),
|
||||||
|
Amount: MilliSatoshi(r.Uint64()),
|
||||||
|
Expiry: r.Uint32(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.Read(req.ChanID[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = r.Read(req.PaymentHash[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = r.Read(req.OnionBlob[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate a blinding point 50% of the time, since not
|
||||||
|
// all update adds will use route blinding.
|
||||||
|
if r.Int31()%2 == 0 {
|
||||||
|
pubkey, err := randPubKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate key: %v",
|
||||||
|
err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.BlindingPoint = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType0](
|
||||||
|
pubkey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
v[0] = reflect.ValueOf(*req)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the above types defined, we'll now generate a slice of
|
// With the above types defined, we'll now generate a slice of
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package lnwire
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnionPacketSize is the size of the serialized Sphinx onion packet included
|
// OnionPacketSize is the size of the serialized Sphinx onion packet included
|
||||||
@@ -11,6 +14,17 @@ import (
|
|||||||
// of per-hop data, and a 32-byte HMAC over the entire packet.
|
// of per-hop data, and a 32-byte HMAC over the entire packet.
|
||||||
const OnionPacketSize = 1366
|
const OnionPacketSize = 1366
|
||||||
|
|
||||||
|
type (
|
||||||
|
// BlindingPointTlvType is the type for ephemeral pubkeys used in
|
||||||
|
// route blinding.
|
||||||
|
BlindingPointTlvType = tlv.TlvType0
|
||||||
|
|
||||||
|
// BlindingPointRecord holds an optional blinding point on update add
|
||||||
|
// htlc.
|
||||||
|
//nolint:lll
|
||||||
|
BlindingPointRecord = tlv.OptionalRecordT[BlindingPointTlvType, *btcec.PublicKey]
|
||||||
|
)
|
||||||
|
|
||||||
// UpdateAddHTLC is the message sent by Alice to Bob when she wishes to add an
|
// UpdateAddHTLC is the message sent by Alice to Bob when she wishes to add an
|
||||||
// HTLC to his remote commitment transaction. In addition to information
|
// HTLC to his remote commitment transaction. In addition to information
|
||||||
// detailing the value, the ID, expiry, and the onion blob is also included
|
// detailing the value, the ID, expiry, and the onion blob is also included
|
||||||
@@ -54,12 +68,29 @@ type UpdateAddHTLC struct {
|
|||||||
// used in the subsequent UpdateAddHTLC message.
|
// used in the subsequent UpdateAddHTLC message.
|
||||||
OnionBlob [OnionPacketSize]byte
|
OnionBlob [OnionPacketSize]byte
|
||||||
|
|
||||||
|
// BlindingPoint is the ephemeral pubkey used to optionally blind the
|
||||||
|
// next hop for this htlc.
|
||||||
|
BlindingPoint BlindingPointRecord
|
||||||
|
|
||||||
// ExtraData is the set of data that was appended to this message to
|
// ExtraData is the set of data that was appended to this message to
|
||||||
// fill out the full maximum transport message size. These fields can
|
// fill out the full maximum transport message size. These fields can
|
||||||
// be used to specify optional data such as custom TLV fields.
|
// be used to specify optional data such as custom TLV fields.
|
||||||
ExtraData ExtraOpaqueData
|
ExtraData ExtraOpaqueData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BlingingPointOrNil returns the blinding point associated with the update, or
|
||||||
|
// nil.
|
||||||
|
func (c *UpdateAddHTLC) BlingingPointOrNil() *btcec.PublicKey {
|
||||||
|
var blindingPoint *btcec.PublicKey
|
||||||
|
c.BlindingPoint.WhenSome(func(b tlv.RecordT[BlindingPointTlvType,
|
||||||
|
*btcec.PublicKey]) {
|
||||||
|
|
||||||
|
blindingPoint = b.Val
|
||||||
|
})
|
||||||
|
|
||||||
|
return blindingPoint
|
||||||
|
}
|
||||||
|
|
||||||
// NewUpdateAddHTLC returns a new empty UpdateAddHTLC message.
|
// NewUpdateAddHTLC returns a new empty UpdateAddHTLC message.
|
||||||
func NewUpdateAddHTLC() *UpdateAddHTLC {
|
func NewUpdateAddHTLC() *UpdateAddHTLC {
|
||||||
return &UpdateAddHTLC{}
|
return &UpdateAddHTLC{}
|
||||||
@@ -74,7 +105,7 @@ var _ Message = (*UpdateAddHTLC)(nil)
|
|||||||
//
|
//
|
||||||
// This is part of the lnwire.Message interface.
|
// This is part of the lnwire.Message interface.
|
||||||
func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error {
|
func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error {
|
||||||
return ReadElements(r,
|
if err := ReadElements(r,
|
||||||
&c.ChanID,
|
&c.ChanID,
|
||||||
&c.ID,
|
&c.ID,
|
||||||
&c.Amount,
|
&c.Amount,
|
||||||
@@ -82,7 +113,27 @@ func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error {
|
|||||||
&c.Expiry,
|
&c.Expiry,
|
||||||
c.OnionBlob[:],
|
c.OnionBlob[:],
|
||||||
&c.ExtraData,
|
&c.ExtraData,
|
||||||
)
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
blindingRecord := c.BlindingPoint.Zero()
|
||||||
|
tlvMap, err := c.ExtraData.ExtractRecords(&blindingRecord)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := tlvMap[c.BlindingPoint.TlvType()]; ok && val == nil {
|
||||||
|
c.BlindingPoint = tlv.SomeRecordT(blindingRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set extra data to nil if we didn't parse anything out of it so that
|
||||||
|
// we can use assert.Equal in tests.
|
||||||
|
if len(tlvMap) == 0 {
|
||||||
|
c.ExtraData = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode serializes the target UpdateAddHTLC into the passed io.Writer
|
// Encode serializes the target UpdateAddHTLC into the passed io.Writer
|
||||||
@@ -114,6 +165,20 @@ func (c *UpdateAddHTLC) Encode(w *bytes.Buffer, pver uint32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only include blinding point in extra data if present.
|
||||||
|
var records []tlv.RecordProducer
|
||||||
|
|
||||||
|
c.BlindingPoint.WhenSome(func(b tlv.RecordT[BlindingPointTlvType,
|
||||||
|
*btcec.PublicKey]) {
|
||||||
|
|
||||||
|
records = append(records, &b)
|
||||||
|
})
|
||||||
|
|
||||||
|
err := EncodeMessageExtraData(&c.ExtraData, records...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return WriteBytes(w, c.ExtraData)
|
return WriteBytes(w, c.ExtraData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -45,6 +45,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/queue"
|
"github.com/lightningnetwork/lnd/queue"
|
||||||
"github.com/lightningnetwork/lnd/subscribe"
|
"github.com/lightningnetwork/lnd/subscribe"
|
||||||
"github.com/lightningnetwork/lnd/ticker"
|
"github.com/lightningnetwork/lnd/ticker"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtclient"
|
"github.com/lightningnetwork/lnd/watchtower/wtclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1985,8 +1986,19 @@ func messageSummary(msg lnwire.Message) string {
|
|||||||
msg.FeeSatoshis)
|
msg.FeeSatoshis)
|
||||||
|
|
||||||
case *lnwire.UpdateAddHTLC:
|
case *lnwire.UpdateAddHTLC:
|
||||||
return fmt.Sprintf("chan_id=%v, id=%v, amt=%v, expiry=%v, hash=%x",
|
var blindingPoint []byte
|
||||||
msg.ChanID, msg.ID, msg.Amount, msg.Expiry, msg.PaymentHash[:])
|
msg.BlindingPoint.WhenSome(
|
||||||
|
func(b tlv.RecordT[lnwire.BlindingPointTlvType,
|
||||||
|
*btcec.PublicKey]) {
|
||||||
|
|
||||||
|
blindingPoint = b.Val.SerializeCompressed()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return fmt.Sprintf("chan_id=%v, id=%v, amt=%v, expiry=%v, "+
|
||||||
|
"hash=%x, blinding_point=%x", msg.ChanID, msg.ID,
|
||||||
|
msg.Amount, msg.Expiry, msg.PaymentHash[:],
|
||||||
|
blindingPoint)
|
||||||
|
|
||||||
case *lnwire.UpdateFailHTLC:
|
case *lnwire.UpdateFailHTLC:
|
||||||
return fmt.Sprintf("chan_id=%v, id=%v, reason=%x", msg.ChanID,
|
return fmt.Sprintf("chan_id=%v, id=%v, reason=%x", msg.ChanID,
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
package record
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlindedRouteData contains the information that is included in a blinded
|
||||||
|
// route encrypted data blob that is created by the recipient to provide
|
||||||
|
// forwarding information.
|
||||||
|
type BlindedRouteData struct {
|
||||||
|
// ShortChannelID is the channel ID of the next hop.
|
||||||
|
ShortChannelID tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID]
|
||||||
|
|
||||||
|
// NextBlindingOverride is a blinding point that should be switched
|
||||||
|
// in for the next hop. This is used to combine two blinded paths into
|
||||||
|
// one (which primarily is used in onion messaging, but in theory
|
||||||
|
// could be used for payments as well).
|
||||||
|
NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey]
|
||||||
|
|
||||||
|
// RelayInfo provides the relay parameters for the hop.
|
||||||
|
RelayInfo tlv.RecordT[tlv.TlvType10, PaymentRelayInfo]
|
||||||
|
|
||||||
|
// Constraints provides the payment relay constraints for the hop.
|
||||||
|
Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints]
|
||||||
|
|
||||||
|
// Features is the set of features the payment requires.
|
||||||
|
Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlindedRouteData creates the data that's provided for hops within a
|
||||||
|
// blinded route.
|
||||||
|
func NewBlindedRouteData(chanID lnwire.ShortChannelID,
|
||||||
|
blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo,
|
||||||
|
constraints *PaymentConstraints,
|
||||||
|
features *lnwire.FeatureVector) *BlindedRouteData {
|
||||||
|
|
||||||
|
info := &BlindedRouteData{
|
||||||
|
ShortChannelID: tlv.NewRecordT[tlv.TlvType2](chanID),
|
||||||
|
RelayInfo: tlv.NewRecordT[tlv.TlvType10](relayInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
if blindingOverride != nil {
|
||||||
|
info.NextBlindingOverride = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType8](blindingOverride))
|
||||||
|
}
|
||||||
|
|
||||||
|
if constraints != nil {
|
||||||
|
info.Constraints = tlv.SomeRecordT(
|
||||||
|
tlv.NewRecordT[tlv.TlvType12](*constraints))
|
||||||
|
}
|
||||||
|
|
||||||
|
if features != nil {
|
||||||
|
info.Features = tlv.SomeRecordT(
|
||||||
|
tlv.NewRecordT[tlv.TlvType14](*features),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBlindedRouteData decodes the data provided within a blinded route.
|
||||||
|
func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
|
||||||
|
var (
|
||||||
|
d BlindedRouteData
|
||||||
|
|
||||||
|
blindingOverride = d.NextBlindingOverride.Zero()
|
||||||
|
constraints = d.Constraints.Zero()
|
||||||
|
features = d.Features.Zero()
|
||||||
|
)
|
||||||
|
|
||||||
|
var tlvRecords lnwire.ExtraOpaqueData
|
||||||
|
if err := lnwire.ReadElements(r, &tlvRecords); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
typeMap, err := tlvRecords.ExtractRecords(
|
||||||
|
&d.ShortChannelID,
|
||||||
|
&blindingOverride, &d.RelayInfo.Val, &constraints,
|
||||||
|
&features,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := typeMap[d.NextBlindingOverride.TlvType()]
|
||||||
|
if ok && val == nil {
|
||||||
|
d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil {
|
||||||
|
d.Constraints = tlv.SomeRecordT(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := typeMap[d.Features.TlvType()]; ok && val == nil {
|
||||||
|
d.Features = tlv.SomeRecordT(features)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBlindedRouteData encodes the blinded route data provided.
|
||||||
|
func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
e lnwire.ExtraOpaqueData
|
||||||
|
recordProducers = make([]tlv.RecordProducer, 0, 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
recordProducers = append(recordProducers, &data.ShortChannelID)
|
||||||
|
|
||||||
|
data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8,
|
||||||
|
*btcec.PublicKey]) {
|
||||||
|
|
||||||
|
recordProducers = append(recordProducers, &pk)
|
||||||
|
})
|
||||||
|
|
||||||
|
recordProducers = append(recordProducers, &data.RelayInfo.Val)
|
||||||
|
|
||||||
|
data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12,
|
||||||
|
PaymentConstraints]) {
|
||||||
|
|
||||||
|
recordProducers = append(recordProducers, &cs)
|
||||||
|
})
|
||||||
|
|
||||||
|
data.Features.WhenSome(func(f tlv.RecordT[tlv.TlvType14,
|
||||||
|
lnwire.FeatureVector]) {
|
||||||
|
|
||||||
|
recordProducers = append(recordProducers, &f)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := e.PackRecords(recordProducers...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentRelayInfo describes the relay policy for a blinded path.
|
||||||
|
type PaymentRelayInfo struct {
|
||||||
|
// CltvExpiryDelta is the expiry delta for the payment.
|
||||||
|
CltvExpiryDelta uint16
|
||||||
|
|
||||||
|
// FeeRate is the fee rate that will be charged per millionth of a
|
||||||
|
// satoshi.
|
||||||
|
FeeRate uint32
|
||||||
|
|
||||||
|
// BaseFee is the per-htlc fee charged.
|
||||||
|
BaseFee uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPaymentRelayRecord creates a tlv.Record that encodes the payment relay
|
||||||
|
// (type 10) type for an encrypted blob payload.
|
||||||
|
func (i *PaymentRelayInfo) Record() tlv.Record {
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
10, &i, func() uint64 {
|
||||||
|
// uint16 + uint32 + tuint32
|
||||||
|
return 2 + 4 + tlv.SizeTUint32(i.BaseFee)
|
||||||
|
}, encodePaymentRelay, decodePaymentRelay,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePaymentRelay(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||||
|
if t, ok := val.(**PaymentRelayInfo); ok {
|
||||||
|
relayInfo := *t
|
||||||
|
|
||||||
|
// Just write our first 6 bytes directly.
|
||||||
|
binary.BigEndian.PutUint16(buf[:2], relayInfo.CltvExpiryDelta)
|
||||||
|
binary.BigEndian.PutUint32(buf[2:6], relayInfo.FeeRate)
|
||||||
|
if _, err := w.Write(buf[0:6]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can safely reuse buf here because we overwrite its
|
||||||
|
// contents.
|
||||||
|
return tlv.ETUint32(w, &relayInfo.BaseFee, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "**hop.PaymentRelayInfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePaymentRelay(r io.Reader, val interface{}, buf *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
|
if t, ok := val.(**PaymentRelayInfo); ok && l <= 10 {
|
||||||
|
scratch := make([]byte, l)
|
||||||
|
|
||||||
|
n, err := io.ReadFull(r, scratch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect at least 6 bytes, because we have 2 bytes for
|
||||||
|
// cltv delta and 4 bytes for fee rate.
|
||||||
|
if n < 6 {
|
||||||
|
return tlv.NewTypeForDecodingErr(val,
|
||||||
|
"*hop.paymentRelayInfo", uint64(n), 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
relayInfo := *t
|
||||||
|
|
||||||
|
relayInfo.CltvExpiryDelta = binary.BigEndian.Uint16(
|
||||||
|
scratch[0:2],
|
||||||
|
)
|
||||||
|
relayInfo.FeeRate = binary.BigEndian.Uint32(scratch[2:6])
|
||||||
|
|
||||||
|
// To be able to re-use the DTUint32 function we create a
|
||||||
|
// buffer with just the bytes holding the variable length u32.
|
||||||
|
// If the base fee is zero, this will be an empty buffer, which
|
||||||
|
// is okay.
|
||||||
|
b := bytes.NewBuffer(scratch[6:])
|
||||||
|
|
||||||
|
return tlv.DTUint32(b, &relayInfo.BaseFee, buf, l-6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForDecodingErr(val, "*hop.paymentRelayInfo", l, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentConstraints is a set of restrictions on a payment.
|
||||||
|
type PaymentConstraints struct {
|
||||||
|
// MaxCltvExpiry is the maximum expiry height for the payment.
|
||||||
|
MaxCltvExpiry uint32
|
||||||
|
|
||||||
|
// HtlcMinimumMsat is the minimum htlc size for the payment.
|
||||||
|
HtlcMinimumMsat lnwire.MilliSatoshi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentConstraints) Record() tlv.Record {
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
12, &p, func() uint64 {
|
||||||
|
// uint32 + tuint64.
|
||||||
|
return 4 + tlv.SizeTUint64(uint64(
|
||||||
|
p.HtlcMinimumMsat,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
encodePaymentConstraints, decodePaymentConstraints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePaymentConstraints(w io.Writer, val interface{},
|
||||||
|
buf *[8]byte) error {
|
||||||
|
|
||||||
|
if c, ok := val.(**PaymentConstraints); ok {
|
||||||
|
constraints := *c
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(buf[:4], constraints.MaxCltvExpiry)
|
||||||
|
if _, err := w.Write(buf[:4]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can safely re-use buf here because we overwrite its
|
||||||
|
// contents.
|
||||||
|
htlcMsat := uint64(constraints.HtlcMinimumMsat)
|
||||||
|
|
||||||
|
return tlv.ETUint64(w, &htlcMsat, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "**PaymentConstraints")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePaymentConstraints(r io.Reader, val interface{}, buf *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
|
if c, ok := val.(**PaymentConstraints); ok && l <= 12 {
|
||||||
|
scratch := make([]byte, l)
|
||||||
|
|
||||||
|
n, err := io.ReadFull(r, scratch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect at least 4 bytes for our uint32.
|
||||||
|
if n < 4 {
|
||||||
|
return tlv.NewTypeForDecodingErr(val,
|
||||||
|
"*paymentConstraints", uint64(n), 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
payConstraints := *c
|
||||||
|
|
||||||
|
payConstraints.MaxCltvExpiry = binary.BigEndian.Uint32(
|
||||||
|
scratch[:4],
|
||||||
|
)
|
||||||
|
|
||||||
|
// This could be empty if our minimum is zero, that's okay.
|
||||||
|
var (
|
||||||
|
b = bytes.NewBuffer(scratch[4:])
|
||||||
|
minHtlc uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
err = tlv.DTUint64(b, &minHtlc, buf, l-4)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payConstraints.HtlcMinimumMsat = lnwire.MilliSatoshi(minHtlc)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForDecodingErr(val, "**PaymentConstraints", l, l)
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package record
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
const pubkeyStr = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"
|
||||||
|
|
||||||
|
func pubkey(t *testing.T) *btcec.PublicKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
nodeBytes, err := hex.DecodeString(pubkeyStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nodePk, err := btcec.ParsePubKey(nodeBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return nodePk
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBlindedDataEncoding tests encoding and decoding of blinded data blobs.
|
||||||
|
// These tests specifically cover cases where the variable length encoded
|
||||||
|
// integers values have different numbers of leading zeros trimmed because
|
||||||
|
// these TLVs are the first composite records with variable length tlvs
|
||||||
|
// (previously, a variable length integer would take up the whole record).
|
||||||
|
func TestBlindedDataEncoding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
baseFee uint32
|
||||||
|
htlcMin lnwire.MilliSatoshi
|
||||||
|
features *lnwire.FeatureVector
|
||||||
|
constraints bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero variable values",
|
||||||
|
baseFee: 0,
|
||||||
|
htlcMin: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zeros trimmed",
|
||||||
|
baseFee: math.MaxUint32 / 2,
|
||||||
|
htlcMin: math.MaxUint64 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no zeros trimmed",
|
||||||
|
baseFee: math.MaxUint32,
|
||||||
|
htlcMin: math.MaxUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil feature vector",
|
||||||
|
features: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-nil, but empty feature vector",
|
||||||
|
features: lnwire.EmptyFeatureVector(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "populated feature vector",
|
||||||
|
features: lnwire.NewFeatureVector(
|
||||||
|
lnwire.NewRawFeatureVector(lnwire.AMPOptional),
|
||||||
|
lnwire.Features,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no payment constraints",
|
||||||
|
constraints: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range tests {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create a standard set of blinded route data, using
|
||||||
|
// the values from our test case for the variable
|
||||||
|
// length encoded values.
|
||||||
|
channelID := lnwire.NewShortChanIDFromInt(1)
|
||||||
|
info := PaymentRelayInfo{
|
||||||
|
FeeRate: 2,
|
||||||
|
CltvExpiryDelta: 3,
|
||||||
|
BaseFee: testCase.baseFee,
|
||||||
|
}
|
||||||
|
|
||||||
|
var constraints *PaymentConstraints
|
||||||
|
if testCase.constraints {
|
||||||
|
constraints = &PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 4,
|
||||||
|
HtlcMinimumMsat: testCase.htlcMin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedData := NewBlindedRouteData(
|
||||||
|
channelID, pubkey(t), info, constraints,
|
||||||
|
testCase.features,
|
||||||
|
)
|
||||||
|
|
||||||
|
encoded, err := EncodeBlindedRouteData(encodedData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
b := bytes.NewBuffer(encoded)
|
||||||
|
decodedData, err := DecodeBlindedRouteData(b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, encodedData, decodedData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBlindedRouteVectors tests encoding/decoding of the test vectors for
|
||||||
|
// blinded route data provided in the specification.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
|
func TestBlindingSpecTestVectors(t *testing.T) {
|
||||||
|
nextBlindingOverrideStr, err := hex.DecodeString("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")
|
||||||
|
require.NoError(t, err)
|
||||||
|
nextBlindingOverride, err := btcec.ParsePubKey(nextBlindingOverrideStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
encoded string
|
||||||
|
expectedPaymentData *BlindedRouteData
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456",
|
||||||
|
expectedPaymentData: NewBlindedRouteData(
|
||||||
|
lnwire.ShortChannelID{
|
||||||
|
BlockHeight: 0,
|
||||||
|
TxIndex: 0,
|
||||||
|
TxPosition: 1729,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
PaymentRelayInfo{
|
||||||
|
CltvExpiryDelta: 36,
|
||||||
|
FeeRate: 150,
|
||||||
|
BaseFee: 10000,
|
||||||
|
},
|
||||||
|
&PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 748005,
|
||||||
|
HtlcMinimumMsat: 1500,
|
||||||
|
},
|
||||||
|
lnwire.NewFeatureVector(
|
||||||
|
lnwire.NewRawFeatureVector(),
|
||||||
|
lnwire.Features,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00",
|
||||||
|
expectedPaymentData: NewBlindedRouteData(
|
||||||
|
lnwire.ShortChannelID{
|
||||||
|
TxPosition: 1105,
|
||||||
|
},
|
||||||
|
nextBlindingOverride,
|
||||||
|
PaymentRelayInfo{
|
||||||
|
CltvExpiryDelta: 48,
|
||||||
|
FeeRate: 100,
|
||||||
|
BaseFee: 500,
|
||||||
|
},
|
||||||
|
&PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 747969,
|
||||||
|
HtlcMinimumMsat: 1500,
|
||||||
|
},
|
||||||
|
lnwire.NewFeatureVector(
|
||||||
|
lnwire.NewRawFeatureVector(),
|
||||||
|
lnwire.Features,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
|
route, err := hex.DecodeString(test.encoded)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
buff := bytes.NewBuffer(route)
|
||||||
|
|
||||||
|
decodedRoute, err := DecodeBlindedRouteData(buff)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(
|
||||||
|
t, test.expectedPaymentData, decodedRoute,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-7
@@ -36,13 +36,13 @@ type BlindedPayment struct {
|
|||||||
// blinded path.
|
// blinded path.
|
||||||
BaseFee uint32
|
BaseFee uint32
|
||||||
|
|
||||||
// ProportionalFee is the aggregated proportional fee for payments
|
// ProportionalFeeRate is the aggregated proportional fee rate for
|
||||||
// made over the blinded path.
|
// payments made over the blinded path.
|
||||||
ProportionalFee uint32
|
ProportionalFeeRate uint32
|
||||||
|
|
||||||
// CltvExpiryDelta is the total expiry delta for the blinded path. Note
|
// CltvExpiryDelta is the total expiry delta for the blinded path. This
|
||||||
// this does not include the final cltv delta for the receiving node
|
// field includes the CLTV for the blinded hops *and* the final cltv
|
||||||
// (which should be provided in an invoice).
|
// delta for the receiver.
|
||||||
CltvExpiryDelta uint16
|
CltvExpiryDelta uint16
|
||||||
|
|
||||||
// HtlcMinimum is the highest HLTC minimum supported along the blinded
|
// HtlcMinimum is the highest HLTC minimum supported along the blinded
|
||||||
@@ -122,7 +122,7 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
|
|||||||
MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum),
|
MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum),
|
||||||
FeeBaseMSat: lnwire.MilliSatoshi(b.BaseFee),
|
FeeBaseMSat: lnwire.MilliSatoshi(b.BaseFee),
|
||||||
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
||||||
b.ProportionalFee,
|
b.ProportionalFeeRate,
|
||||||
),
|
),
|
||||||
ToNodePubKey: func() route.Vertex {
|
ToNodePubKey: func() route.Vertex {
|
||||||
return route.NewVertex(
|
return route.NewVertex(
|
||||||
|
|||||||
@@ -121,12 +121,12 @@ func TestBlindedPaymentToHints(t *testing.T) {
|
|||||||
{},
|
{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BaseFee: baseFee,
|
BaseFee: baseFee,
|
||||||
ProportionalFee: ppmFee,
|
ProportionalFeeRate: ppmFee,
|
||||||
CltvExpiryDelta: cltvDelta,
|
CltvExpiryDelta: cltvDelta,
|
||||||
HtlcMinimum: htlcMin,
|
HtlcMinimum: htlcMin,
|
||||||
HtlcMaximum: htlcMax,
|
HtlcMaximum: htlcMax,
|
||||||
Features: features,
|
Features: features,
|
||||||
}
|
}
|
||||||
require.Nil(t, blindedPayment.toRouteHints())
|
require.Nil(t, blindedPayment.toRouteHints())
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user