Skip to content

Commit 3cdd93f

Browse files
committed
lnwire: enforce non-zero timestamp in gossip messages
In this commit, we add extra validation for chan updates and node ann to ensure that we fail to parse instances that have have a zero timestamp. From bolt 7: ``` For channel_update: "MUST set timestamp to greater than 0, AND to greater than any previously-sent channel_update for this short_channel_id." ```
1 parent 013f5c9 commit 3cdd93f

5 files changed

Lines changed: 145 additions & 3 deletions

File tree

lnwire/channel_update.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ func (a *ChannelUpdate1) Decode(r io.Reader, _ uint32) error {
154154
return err
155155
}
156156

157+
// Per BOLT 7, the timestamp MUST be greater than 0. A zero timestamp
158+
// is invalid and indicates a malformed message.
159+
if a.Timestamp == 0 {
160+
return fmt.Errorf("timestamp cannot be zero")
161+
}
162+
157163
// Now check whether the max HTLC field is present and read it if so.
158164
if a.MessageFlags.HasMaxHtlc() {
159165
if err := ReadElements(r, &a.HtlcMaximumMsat); err != nil {

lnwire/channel_update_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package lnwire
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestChannelUpdate1Timestamp tests that ChannelUpdate1 correctly validates
11+
// timestamps during decoding. Per BOLT 7, the timestamp MUST be greater than 0.
12+
func TestChannelUpdate1Timestamp(t *testing.T) {
13+
t.Parallel()
14+
15+
testCases := []struct {
16+
name string
17+
timestamp uint32
18+
valid bool
19+
}{
20+
{
21+
name: "zero timestamp rejected",
22+
timestamp: 0,
23+
valid: false,
24+
},
25+
{
26+
name: "timestamp 1 accepted",
27+
timestamp: 1,
28+
valid: true,
29+
},
30+
{
31+
name: "large timestamp accepted",
32+
timestamp: 1700000000,
33+
valid: true,
34+
},
35+
}
36+
37+
for _, tc := range testCases {
38+
t.Run(tc.name, func(t *testing.T) {
39+
t.Parallel()
40+
41+
cu := &ChannelUpdate1{
42+
Timestamp: tc.timestamp,
43+
MessageFlags: ChanUpdateRequiredMaxHtlc,
44+
}
45+
46+
var buf bytes.Buffer
47+
err := cu.Encode(&buf, 0)
48+
require.NoError(t, err, "failed to encode")
49+
50+
decoded := &ChannelUpdate1{}
51+
err = decoded.Decode(&buf, 0)
52+
53+
if tc.valid {
54+
require.NoError(t, err)
55+
require.Equal(
56+
t, tc.timestamp, decoded.Timestamp,
57+
)
58+
} else {
59+
require.Error(t, err)
60+
require.Contains(
61+
t, err.Error(), "timestamp cannot "+
62+
"be zero",
63+
)
64+
}
65+
})
66+
}
67+
}

lnwire/node_announcement.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ func (a *NodeAnnouncement1) Decode(r io.Reader, _ uint32) error {
131131
return err
132132
}
133133

134+
if a.Timestamp == 0 {
135+
return fmt.Errorf("timestamp cannot be zero")
136+
}
137+
134138
return a.ExtraOpaqueData.ValidateTLV()
135139
}
136140

lnwire/node_announcement_test.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package lnwire
22

3-
import "testing"
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
49

510
// TestNodeAliasValidation tests that the NewNodeAlias method will only accept
611
// valid node announcements.
@@ -40,3 +45,63 @@ func TestNodeAliasValidation(t *testing.T) {
4045
}
4146
}
4247
}
48+
49+
// TestNodeAnnouncement1Timestamp tests that NodeAnnouncement1 correctly
50+
// validates timestamps during decoding. Per BOLT 7, the timestamp MUST be
51+
// greater than any previous node_announcement.
52+
func TestNodeAnnouncement1Timestamp(t *testing.T) {
53+
t.Parallel()
54+
55+
testCases := []struct {
56+
name string
57+
timestamp uint32
58+
valid bool
59+
}{
60+
{
61+
name: "zero timestamp rejected",
62+
timestamp: 0,
63+
valid: false,
64+
},
65+
{
66+
name: "timestamp 1 accepted",
67+
timestamp: 1,
68+
valid: true,
69+
},
70+
{
71+
name: "large timestamp accepted",
72+
timestamp: 1700000000,
73+
valid: true,
74+
},
75+
}
76+
77+
for _, tc := range testCases {
78+
t.Run(tc.name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
na := &NodeAnnouncement1{
82+
Timestamp: tc.timestamp,
83+
Features: NewRawFeatureVector(),
84+
}
85+
86+
var buf bytes.Buffer
87+
err := na.Encode(&buf, 0)
88+
require.NoError(t, err, "failed to encode")
89+
90+
decoded := &NodeAnnouncement1{}
91+
err = decoded.Decode(&buf, 0)
92+
93+
if tc.valid {
94+
require.NoError(t, err)
95+
require.Equal(
96+
t, tc.timestamp, decoded.Timestamp,
97+
)
98+
} else {
99+
require.Error(t, err)
100+
require.Contains(
101+
t, err.Error(), "timestamp cannot "+
102+
"be zero",
103+
)
104+
}
105+
})
106+
}
107+
}

lnwire/test_message.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ func (a *ChannelUpdate1) RandTestMessage(t *rapid.T) Message {
477477
Signature: RandSignature(t),
478478
ChainHash: hash,
479479
ShortChannelID: RandShortChannelID(t),
480-
Timestamp: uint32(rapid.IntRange(0, 0x7FFFFFFF).Draw(
480+
Timestamp: uint32(rapid.IntRange(1, 0x7FFFFFFF).Draw(
481481
t, "timestamp"),
482482
),
483483
MessageFlags: msgFlags,
@@ -1279,7 +1279,7 @@ func (a *NodeAnnouncement1) RandTestMessage(t *rapid.T) Message {
12791279
return &NodeAnnouncement1{
12801280
Signature: RandSignature(t),
12811281
Features: RandFeatureVector(t),
1282-
Timestamp: uint32(rapid.IntRange(0, 0x7FFFFFFF).Draw(
1282+
Timestamp: uint32(rapid.IntRange(1, 0x7FFFFFFF).Draw(
12831283
t, "timestamp"),
12841284
),
12851285
NodeID: nodeID,

0 commit comments

Comments
 (0)