server: remove accountability signal experiment period
What changed, and why it matters
This commit removes a time-limited 'experiment period' for a new Lightning network signaling feature called 'accountable' (previously 'endorsement signal'). Previously, nodes would only relay this signal until a fixed date in 2026, after which they would stop. Now, because the latest protocol proposal says the default should be to send a zero/'unaccountable' value, there is no longer a privacy risk from relaying it, so the experiment end date is removed. The signal is now relayed whenever the feature is enabled, regardless of date. This is a protocol behavior change, not a fix for an exploitable security bug.
No security action required. This is a routine protocol update to align with an evolving BLIP proposal. Operators and reviewers should confirm the change matches the latest BLIP-0004 proposal and that defaulting to 'unaccountable' (zero) values does not introduce unintended privacy or routing behavior in their deployment.
Security signals we found
Removal of time-based feature gating
Change to default privacy behavior of a protocol signal
Renaming of experimental protocol feature
No vulnerability fix or exploit mitigation present
Evidence from the diff
The change removes the AccountabilityExperimentEnd timestamp and all date-based gating for the experimental accountability/endorsement signal. ShouldSetExpAccountability and ShouldFwdExpAccountability now depend only on the NoExpAccountability protocol option. Tests are updated to expect the signal is always present when enabled, rather than conditional on a date window. The release notes rename the feature from ‘experimental endorsement signal’ to ‘accountable’ to match BLIP updates.
Changed components
htlcswitch/link.gorpcserver.goserver.golntest/utils.goitest/lnd_experimental_accountability.goitest/lnd_forward_interceptor_test.godocs/release-notes/release-notes-0.21.0.mdInspect captured patch +17 / −69
diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md
index 4e032a3..7196579 100644
--- a/docs/release-notes/release-notes-0.21.0.md
+++ b/docs/release-notes/release-notes-0.21.0.md
@@ -75,6 +75,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
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index 93bbabb..4dbfa52 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -3346,8 +3346,6 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
func (l *channelLink) experimentalAccountability(
customUpdateAdd record.CustomSet) fn.Option[byte] {
- // Only relay experimental signal if we are within the experiment
- // period.
if !l.cfg.ShouldFwdExpAccountability() {
return fn.None[byte]()
}
diff --git a/itest/lnd_experimental_accountability.go b/itest/lnd_experimental_accountability.go
index a636769..30296e7 100644
--- a/itest/lnd_experimental_accountability.go
+++ b/itest/lnd_experimental_accountability.go
@@ -57,18 +57,12 @@ func testAccountability(ht *lntest.HarnessTest, aliceAccountable bool) {
FeeLimitMsat: math.MaxInt64,
}
- var expectedValue []byte
- hasAccountability := lntest.ExperimentalAccountabilityActive()
-
- if hasAccountability {
- if aliceAccountable {
- expectedValue = []byte{lnwire.ExperimentalAccountable}
- t := uint64(lnwire.ExperimentalAccountableType)
- sendReq.FirstHopCustomRecords = map[uint64][]byte{
- t: expectedValue,
- }
- } else {
- expectedValue = []byte{lnwire.ExperimentalUnaccountable}
+ expectedValue := []byte{lnwire.ExperimentalUnaccountable}
+ if aliceAccountable {
+ expectedValue = []byte{lnwire.ExperimentalAccountable}
+ t := uint64(lnwire.ExperimentalAccountableType)
+ sendReq.FirstHopCustomRecords = map[uint64][]byte{
+ t: expectedValue,
}
}
@@ -76,12 +70,11 @@ func testAccountability(ht *lntest.HarnessTest, aliceAccountable bool) {
// Validate that our signal (positive or zero) propagates until carol
// and then is dropped because she has disabled the feature.
- // When the accountability experiment is not active, no signal is sent.
validateAccountableAndResume(
- ht, bobIntercept, hasAccountability, expectedValue,
+ ht, bobIntercept, true, expectedValue,
)
validateAccountableAndResume(
- ht, carolIntercept, hasAccountability, expectedValue,
+ ht, carolIntercept, true, expectedValue,
)
validateAccountableAndResume(ht, daveIntercept, false, nil)
diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go
index d45d650..f68042d 100644
--- a/itest/lnd_forward_interceptor_test.go
+++ b/itest/lnd_forward_interceptor_test.go
@@ -432,25 +432,14 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
// We should get another notification about the held HTLC.
packet = ht.ReceiveHtlcInterceptor(bobInterceptor)
- // Check the expected number of custom records based on whether the
- // accountability experiment is still active.
- expectedLen := 1
- if lntest.ExperimentalAccountabilityActive() {
- expectedLen = 2
- }
- require.Len(ht, packet.InWireCustomRecords, expectedLen)
+ require.Len(ht, packet.InWireCustomRecords, 2)
require.Equal(ht, lntest.CustomRecordsWithUnaccountable(customRecords),
packet.InWireCustomRecords)
// And now we forward the payment at Carol, expecting only an
- // accountability signal in our incoming custom records (if the experiment
- // is still active).
+ // accountability signal in our incoming custom records.
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
- expectedCarolLen := 0
- if lntest.ExperimentalAccountabilityActive() {
- expectedCarolLen = 1
- }
- require.Len(ht, packet.InWireCustomRecords, expectedCarolLen)
+ require.Len(ht, packet.InWireCustomRecords, 1)
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
IncomingCircuitKey: packet.IncomingCircuitKey,
Action: actionResume,
diff --git a/lntest/utils.go b/lntest/utils.go
index a2c3ea8..9994398 100644
--- a/lntest/utils.go
+++ b/lntest/utils.go
@@ -7,11 +7,9 @@ import (
"os"
"strconv"
"strings"
- "time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
- "github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest/wait"
@@ -290,16 +288,6 @@ func CalcStaticFeeBuffer(c lnrpc.CommitmentType, numHTLCs int) btcutil.Amount {
// tests.
func CustomRecordsWithUnaccountable(
originalRecords lnwire.CustomRecords) map[uint64][]byte {
-
- if !ExperimentalAccountabilityActive() {
- // Return nil if there are no records, to match wire encoding.
- if len(originalRecords) == 0 {
- return nil
- }
-
- return originalRecords.Copy()
- }
-
return originalRecords.MergedCopy(map[uint64][]byte{
uint64(lnwire.ExperimentalAccountableType): {
lnwire.ExperimentalUnaccountable,
@@ -307,12 +295,6 @@ func CustomRecordsWithUnaccountable(
)
}
-// ExperimentalAccountabilityActive returns true if the experimental accountability
-// window is still open.
-func ExperimentalAccountabilityActive() bool {
- return time.Now().Before(lnd.AccountabilityExperimentEnd)
-}
-
// LnrpcOutpointToStr returns a string representation of an lnrpc.OutPoint.
func LnrpcOutpointToStr(outpoint *lnrpc.OutPoint) string {
return fmt.Sprintf("%s:%d", outpoint.TxidStr, outpoint.OutputIndex)
diff --git a/rpcserver.go b/rpcserver.go
index 026b224..29efd15 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -780,13 +780,7 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server,
return nil
},
ShouldSetExpAccountability: func() bool {
- if s.cfg.ProtocolOptions.NoExpAccountability() {
- return false
- }
-
- return clock.NewDefaultClock().Now().Before(
- AccountabilityExperimentEnd,
- )
+ return !s.cfg.ProtocolOptions.NoExpAccountability()
},
}
diff --git a/server.go b/server.go
index ef8abbf..3f7b3e8 100644
--- a/server.go
+++ b/server.go
@@ -139,12 +139,6 @@ var (
// TODO(roasbeef): add command line param to modify.
MaxFundingAmount = funding.MaxBtcFundingAmount
- // AccountabilityExperimentEnd is the time after which nodes should stop
- // propagating experimental accountable signals.
- //
- // Per blip04: January 1, 2026 12:00:00 AM UTC in unix seconds.
- AccountabilityExperimentEnd = time.Unix(1767225600, 0)
-
// ErrGossiperBan is one of the errors that can be returned when we
// attempt to finalize a connection to a remote peer.
ErrGossiperBan = errors.New("gossiper has banned remote's key")
@@ -4444,13 +4438,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
AuxTrafficShaper: s.implCfg.TrafficShaper,
AuxChannelNegotiator: s.implCfg.AuxChannelNegotiator,
ShouldFwdExpAccountability: func() bool {
- if s.cfg.ProtocolOptions.NoExpAccountability() {
- return false
- }
-
- return clock.NewDefaultClock().Now().Before(
- AccountabilityExperimentEnd,
- )
+ return !s.cfg.ProtocolOptions.NoExpAccountability()
},
NoDisconnectOnPongFailure: s.cfg.NoDisconnectOnPongFailure,
}
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.