Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-0.21.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
This applies to both funders and fundees, with the ability to override the
value during channel opening or acceptance.

* Rename [experimental endorsement signal](https://github.com/lightning/blips/blob/a833e7b49f224e1240b5d669e78fa950160f5a06/blip-0004.md)
to [accountable](https://github.com/lightningnetwork/lnd/pull/10367) to match
the latest [proposal](https://github.com/lightning/blips/pull/67).

## RPC Updates

## lncli Updates
Expand Down
2 changes: 1 addition & 1 deletion feature/default_sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.ExperimentalEndorsementOptional: {
lnwire.ExperimentalAccountabilityOptional: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: one mention of endorsement left in comment.

SetNodeAnn: {}, // N
},
lnwire.RbfCoopCloseOptionalStaging: {
Expand Down
12 changes: 6 additions & 6 deletions feature/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ type Config struct {
// NoTaprootOverlay unsets the taproot overlay channel feature bits.
NoTaprootOverlay bool

// NoExperimentalEndorsement unsets any bits that signal support for
// forwarding experimental endorsement.
NoExperimentalEndorsement bool
// NoExperimentalAccountability unsets any bits that signal support for
// forwarding experimental accountability.
NoExperimentalAccountability bool

// NoRbfCoopClose unsets any bits that signal support for using RBF for
// coop close.
Expand Down Expand Up @@ -213,9 +213,9 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
raw.Unset(lnwire.SimpleTaprootOverlayChansOptional)
raw.Unset(lnwire.SimpleTaprootOverlayChansRequired)
}
if cfg.NoExperimentalEndorsement {
raw.Unset(lnwire.ExperimentalEndorsementOptional)
raw.Unset(lnwire.ExperimentalEndorsementRequired)
if cfg.NoExperimentalAccountability {
raw.Unset(lnwire.ExperimentalAccountabilityOptional)
raw.Unset(lnwire.ExperimentalAccountabilityRequired)
}
if cfg.NoRbfCoopClose {
raw.Unset(lnwire.RbfCoopCloseOptionalStaging)
Expand Down
48 changes: 23 additions & 25 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ type ChannelLinkConfig struct {
// restrict the flow of HTLCs and fee updates.
MaxFeeExposure lnwire.MilliSatoshi

// ShouldFwdExpEndorsement is a closure that indicates whether the link
// should forward experimental endorsement signals.
ShouldFwdExpEndorsement func() bool
// ShouldFwdExpAccountability is a closure that indicates whether the
// link should forward experimental accountability signals.
ShouldFwdExpAccountability func() bool

// AuxTrafficShaper is an optional auxiliary traffic shaper that can be
// used to manage the bandwidth of the link.
Expand Down Expand Up @@ -3163,11 +3163,11 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
continue
}

endorseValue := l.experimentalEndorsement(
accountableValue := l.experimentalAccountability(
record.CustomSet(add.CustomRecords),
)
endorseType := uint64(
lnwire.ExperimentalEndorsementType,
accountableType := uint64(
lnwire.ExperimentalAccountableType,
)

switch fwdPkg.State {
Expand All @@ -3191,9 +3191,9 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
BlindingPoint: fwdInfo.NextBlinding,
}

endorseValue.WhenSome(func(e byte) {
accountableValue.WhenSome(func(e byte) {
custRecords := map[uint64][]byte{
endorseType: {e},
accountableType: {e},
}

outgoingAdd.CustomRecords = custRecords
Expand Down Expand Up @@ -3249,9 +3249,9 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
BlindingPoint: fwdInfo.NextBlinding,
}

endorseValue.WhenSome(func(e byte) {
accountableValue.WhenSome(func(e byte) {
addMsg.CustomRecords = map[uint64][]byte{
endorseType: {e},
accountableType: {e},
}
})

Expand Down Expand Up @@ -3340,44 +3340,42 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
l.forwardBatch(reforward, switchPackets...)
}

// experimentalEndorsement returns the value to set for our outgoing
// experimental endorsement field, and a boolean indicating whether it should
// experimentalAccountability returns the value to set for our outgoing
// experimental accountable field, and a boolean indicating whether it should
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this comment is outdated, it does not return a boolean ?

// be populated on the outgoing htlc.
func (l *channelLink) experimentalEndorsement(
func (l *channelLink) experimentalAccountability(
Copy link
Collaborator

Choose a reason for hiding this comment

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

are we ok that basically this check resets the whole CustomRecord froward TLV in case no Accuntability bit is set? Probably not a problem now since it is not used for anything but I think this should be more fine granular in the future ? This seems to remove the old customRecord data and replaces it with the single lnwire.ExperimentalUnaccountable value can you verifiy @gemini-code-assist

Choose a reason for hiding this comment

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

You've accurately identified a potential issue, @ziggie1984. The current implementation of processRemoteAdds (in htlcswitch/link.go, specifically around lines 3194-3199 and 3252-3255) indeed overwrites the entire outgoingAdd.CustomRecords map with only the ExperimentalAccountableType TLV if experimentalAccountability returns a value (which it does if forwarding is enabled, even if the incoming HTLC didn't have the accountability bit set or had an invalid one).

This means any other custom records present in the incoming HTLC would be lost when forwarding. To prevent this, the outgoingAdd.CustomRecords should be initialized with a copy of the incoming add.CustomRecords before adding or updating the ExperimentalAccountableType.

You can use the record.CustomSet.Copy() method to create a mutable copy of the existing custom records. The corrected logic would look something like this:

For lines 3194-3199:

				accountableValue.WhenSome(func(e byte) {
					// Create a mutable copy of the incoming custom records.
					custRecords := add.CustomRecords.Copy()
					custRecords[accountableType] = []byte{e}
					outgoingAdd.CustomRecords = custRecords
				})

And similarly for lines 3252-3255:

			accountableValue.WhenSome(func(e byte) {
				// Create a mutable copy of the incoming custom records.
				custRecords := add.CustomRecords.Copy()
				custRecords[accountableType] = []byte{e}
				addMsg.CustomRecords = custRecords
			})

This ensures that only the ExperimentalAccountableType is modified or added, while preserving any other custom records.

customUpdateAdd record.CustomSet) fn.Option[byte] {

// Only relay experimental signal if we are within the experiment
// period.
if !l.cfg.ShouldFwdExpEndorsement() {
if !l.cfg.ShouldFwdExpAccountability() {
return fn.None[byte]()
}

// If we don't have any custom records or the experimental field is
// not set, just forward a zero value.
if len(customUpdateAdd) == 0 {
return fn.Some[byte](lnwire.ExperimentalUnendorsed)
return fn.Some[byte](lnwire.ExperimentalUnaccountable)
}

t := uint64(lnwire.ExperimentalEndorsementType)
t := uint64(lnwire.ExperimentalAccountableType)
value, set := customUpdateAdd[t]
if !set {
return fn.Some[byte](lnwire.ExperimentalUnendorsed)
return fn.Some[byte](lnwire.ExperimentalUnaccountable)
}

// We expect at least one byte for this field, consider it invalid if
// it has no data and just forward a zero value.
if len(value) == 0 {
return fn.Some[byte](lnwire.ExperimentalUnendorsed)
return fn.Some[byte](lnwire.ExperimentalUnaccountable)
}

// Only forward endorsed if the incoming link is endorsed.
if value[0] == lnwire.ExperimentalEndorsed {
return fn.Some[byte](lnwire.ExperimentalEndorsed)
// Only forward accountable if the incoming link is accountable.
if value[0] == lnwire.ExperimentalAccountable {
return fn.Some[byte](lnwire.ExperimentalAccountable)
}

// Forward as unendorsed otherwise, including cases where we've
// Forward as unaccountable otherwise, including cases where we've
// received an invalid value that uses more than 3 bits of information.
return fn.Some[byte](lnwire.ExperimentalUnendorsed)
return fn.Some[byte](lnwire.ExperimentalUnaccountable)
}

// processExitHop handles an htlc for which this link is the exit hop. It
Expand Down
46 changes: 23 additions & 23 deletions htlcswitch/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2234,18 +2234,18 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt,
PendingCommitTicker: ticker.New(time.Minute),
// Make the BatchSize and Min/MaxUpdateTimeout large enough
// to not trigger commit updates automatically during tests.
BatchSize: 10000,
MinUpdateTimeout: 30 * time.Minute,
MaxUpdateTimeout: 40 * time.Minute,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
ShouldFwdExpEndorsement: func() bool { return true },
BatchSize: 10000,
MinUpdateTimeout: 30 * time.Minute,
MaxUpdateTimeout: 40 * time.Minute,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
ShouldFwdExpAccountability: func() bool { return true },
}

aliceLink := NewChannelLink(aliceCfg, aliceLc.channel)
Expand Down Expand Up @@ -4924,17 +4924,17 @@ func (h *persistentLinkHarness) restartLink(
MinUpdateTimeout: 30 * time.Minute,
MaxUpdateTimeout: 40 * time.Minute,
// Set any hodl flags requested for the new link.
HodlMask: hodl.MaskFromFlags(hodlFlags...),
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: h.hSwitch.cfg.HtlcNotifier,
SyncStates: syncStates,
GetAliases: getAliases,
ShouldFwdExpEndorsement: func() bool { return true },
HodlMask: hodl.MaskFromFlags(hodlFlags...),
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: h.hSwitch.cfg.HtlcNotifier,
SyncStates: syncStates,
GetAliases: getAliases,
ShouldFwdExpAccountability: func() bool { return true },
}

aliceLink := NewChannelLink(aliceCfg, aliceChannel)
Expand Down
42 changes: 21 additions & 21 deletions htlcswitch/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1157,27 +1157,27 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
NotifyContractUpdate: notifyContractUpdate,
ChainEvents: &contractcourt.ChainEventSubscription{},
SyncStates: true,
BatchSize: 10,
BatchTicker: ticker.NewForce(testBatchTimeout),
FwdPkgGCTicker: ticker.NewForce(fwdPkgTimeout),
PendingCommitTicker: ticker.New(2 * time.Minute),
MinUpdateTimeout: minFeeUpdateTimeout,
MaxUpdateTimeout: maxFeeUpdateTimeout,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
OutgoingCltvRejectDelta: 3,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(10 * 1000).FeePerKWeight(),
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: server.htlcSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
ShouldFwdExpEndorsement: func() bool { return true },
NotifyContractUpdate: notifyContractUpdate,
ChainEvents: &contractcourt.ChainEventSubscription{},
SyncStates: true,
BatchSize: 10,
BatchTicker: ticker.NewForce(testBatchTimeout),
FwdPkgGCTicker: ticker.NewForce(fwdPkgTimeout),
PendingCommitTicker: ticker.New(2 * time.Minute),
MinUpdateTimeout: minFeeUpdateTimeout,
MaxUpdateTimeout: maxFeeUpdateTimeout,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
OutgoingCltvRejectDelta: 3,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(10 * 1000).FeePerKWeight(),
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: server.htlcSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
ShouldFwdExpAccountability: func() bool { return true },
},
channel,
)
Expand Down
4 changes: 2 additions & 2 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,8 +700,8 @@ var allTestCases = []*lntest.TestCase{
TestFunc: testDebuglevelShow,
},
{
Name: "experimental endorsement",
TestFunc: testExperimentalEndorsement,
Name: "experimental accountability",
TestFunc: testExperimentalAccountability,
},
{
Name: "quiescence",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ import (
"github.com/stretchr/testify/require"
)

// testExperimentalEndorsement tests setting of positive and negative
// experimental endorsement signals.
func testExperimentalEndorsement(ht *lntest.HarnessTest) {
testEndorsement(ht, true)
testEndorsement(ht, false)
// testExperimentalAccountability tests setting of positive and negative
// experimental accountable signals.
func testExperimentalAccountability(ht *lntest.HarnessTest) {
testAccountability(ht, true)
testAccountability(ht, false)
}

// testEndorsement sets up a 5 hop network and tests propagation of
// experimental endorsement signals.
func testEndorsement(ht *lntest.HarnessTest, aliceEndorse bool) {
// testAccountability sets up a 5 hop network and tests propagation of
// experimental accountable signals.
func testAccountability(ht *lntest.HarnessTest, aliceAccountable bool) {
cfg := node.CfgAnchor
carolCfg := append(
[]string{"--protocol.no-experimental-endorsement"}, cfg...,
[]string{"--protocol.no-experimental-accountability"}, cfg...,
)
cfgs := [][]string{cfg, cfg, carolCfg, cfg, cfg}

Expand Down Expand Up @@ -57,10 +57,10 @@ func testEndorsement(ht *lntest.HarnessTest, aliceEndorse bool) {
FeeLimitMsat: math.MaxInt64,
}

expectedValue := []byte{lnwire.ExperimentalUnendorsed}
if aliceEndorse {
expectedValue = []byte{lnwire.ExperimentalEndorsed}
t := uint64(lnwire.ExperimentalEndorsementType)
expectedValue := []byte{lnwire.ExperimentalUnaccountable}
if aliceAccountable {
expectedValue = []byte{lnwire.ExperimentalAccountable}
t := uint64(lnwire.ExperimentalAccountableType)
sendReq.FirstHopCustomRecords = map[uint64][]byte{
t: expectedValue,
}
Expand All @@ -70,24 +70,24 @@ func testEndorsement(ht *lntest.HarnessTest, aliceEndorse bool) {

// Validate that our signal (positive or zero) propagates until carol
// and then is dropped because she has disabled the feature.
validateEndorsedAndResume(ht, bobIntercept, true, expectedValue)
validateEndorsedAndResume(ht, carolIntercept, true, expectedValue)
validateEndorsedAndResume(ht, daveIntercept, false, nil)
validateAccountableAndResume(ht, bobIntercept, true, expectedValue)
validateAccountableAndResume(ht, carolIntercept, true, expectedValue)
validateAccountableAndResume(ht, daveIntercept, false, nil)

var preimage lntypes.Preimage
copy(preimage[:], invoice.RPreimage)
ht.AssertPaymentStatus(alice, preimage.Hash(), lnrpc.Payment_SUCCEEDED)
}

func validateEndorsedAndResume(ht *lntest.HarnessTest,
interceptor rpc.InterceptorClient, hasEndorsement bool,
func validateAccountableAndResume(ht *lntest.HarnessTest,
interceptor rpc.InterceptorClient, hasAccountable bool,
expectedValue []byte) {

packet := ht.ReceiveHtlcInterceptor(interceptor)

var expectedRecords map[uint64][]byte
if hasEndorsement {
u64Type := uint64(lnwire.ExperimentalEndorsementType)
if hasAccountable {
u64Type := uint64(lnwire.ExperimentalAccountableType)
expectedRecords = map[uint64][]byte{
u64Type: expectedValue,
}
Expand Down
8 changes: 4 additions & 4 deletions itest/lnd_forward_interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
// all intercepted packets. These packets are held to simulate a
// pending payment.
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
require.Equal(ht, lntest.CustomRecordsWithUnendorsed(
require.Equal(ht, lntest.CustomRecordsWithUnaccountable(
customRecords,
), packet.InWireCustomRecords)

Expand Down Expand Up @@ -433,11 +433,11 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
packet = ht.ReceiveHtlcInterceptor(bobInterceptor)

require.Len(ht, packet.InWireCustomRecords, 2)
require.Equal(ht, lntest.CustomRecordsWithUnendorsed(customRecords),
require.Equal(ht, lntest.CustomRecordsWithUnaccountable(customRecords),
packet.InWireCustomRecords)

// And now we forward the payment at Carol, expecting only an
// endorsement signal in our incoming custom records.
// accountable signal in our incoming custom records.
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
require.Len(ht, packet.InWireCustomRecords, 1)
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
Expand All @@ -451,7 +451,7 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
alice, preimage.Hash(), lnrpc.Payment_SUCCEEDED,
func(p *lnrpc.Payment) error {
recordsEqual := reflect.DeepEqual(
lntest.CustomRecordsWithUnendorsed(
lntest.CustomRecordsWithUnaccountable(
sendReq.FirstHopCustomRecords,
), p.FirstHopCustomRecords,
)
Expand Down
Loading
Loading