Skip to content

Commit 5aef6ab

Browse files
nsacfromknecht
authored andcommitted
sphinx: replay protection and garbage collector
This commit introduces persistent replay protection against prior shared secrets from prior HTLC's. A new data structure DecayedLog was created that stores all shared secrets in boltdb and it persists on startup. Additionally, it comes with a set of tests that assert its persistence guarantees. DecayedLog adheres to the newly created PersistLog interface. DecayedLog also comes with a garbage collector that removes expired shared secrets from the back-end boltdb.
1 parent dbb6dc0 commit 5aef6ab

File tree

9 files changed

+702
-47
lines changed

9 files changed

+702
-47
lines changed

bench_test.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/roasbeef/btcd/btcec"
8+
"github.com/lightningnetwork/lightning-onion/persistlog"
89
)
910

1011
var (
@@ -55,7 +56,14 @@ func BenchmarkPathPacketConstruction(b *testing.B) {
5556

5657
func BenchmarkProcessPacket(b *testing.B) {
5758
b.StopTimer()
58-
path, _, sphinxPacket, err := newTestRoute(1)
59+
60+
// Create the DecayedLog object
61+
d := persistlog.DecayedLog{}
62+
if err := d.Start(); err != nil {
63+
b.Fatalf("unable to start channeldb")
64+
}
65+
66+
path, _, sphinxPacket, err := newTestRoute(1, d)
5967
if err != nil {
6068
b.Fatalf("unable to create test route: %v", err)
6169
}
@@ -65,15 +73,10 @@ func BenchmarkProcessPacket(b *testing.B) {
6573
var (
6674
pkt *ProcessedPacket
6775
)
68-
for i := 0; i < b.N; i++ {
69-
pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil)
70-
if err != nil {
71-
b.Fatalf("unable to process packet: %v", err)
72-
}
7376

74-
b.StopTimer()
75-
path[0].seenSecrets = make(map[[sharedSecretSize]byte]struct{})
76-
b.StartTimer()
77+
pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil)
78+
if err != nil {
79+
b.Fatalf("unable to process packet: %v", err)
7780
}
7881

7982
p = pkt

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func main() {
7676
}
7777

7878
privkey, _ := btcec.PrivKeyFromBytes(btcec.S256(), binKey)
79-
s := sphinx.NewRouter(privkey, &chaincfg.TestNet3Params)
79+
s := sphinx.NewRouter(privkey, &chaincfg.TestNet3Params, nil)
8080

8181
var packet sphinx.OnionPacket
8282
err = packet.Decode(bytes.NewBuffer(binMsg))

glide.lock

Lines changed: 12 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package: github.com/lightningnetwork/lightning-onion
22
import:
3+
- package: github.com/boltdb/bolt
4+
version: ^1.2.1
35
- package: github.com/aead/chacha20
46
version: d31a916ded42d1640b9d89a26f8abd53cc96790c
7+
- package: github.com/lightningnetwork/lnd
8+
subpackages:
9+
- chainntnfs
10+
- channeldb
511
- package: github.com/roasbeef/btcd
612
subpackages:
713
- btcec
814
- chaincfg
15+
- wire
916
- package: github.com/roasbeef/btcutil
1017
- package: golang.org/x/crypto
1118
subpackages:

persistlog/decayedlog.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package persistlog
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/binary"
6+
"fmt"
7+
"github.com/boltdb/bolt"
8+
"github.com/lightningnetwork/lnd/chainntnfs"
9+
"github.com/lightningnetwork/lnd/channeldb"
10+
"sync"
11+
)
12+
13+
const (
14+
// defaultDbDirectory is the default directory where our decayed log
15+
// will store our (sharedHash, CLTV expiry height) key-value pairs.
16+
defaultDbDirectory = "sharedsecret"
17+
18+
// sharedHashSize is the size in bytes of the keys we will be storing
19+
// in the DecayedLog. It represents the first 20 bytes of a truncated
20+
// sha-256 hash of a secret generated by ECDH.
21+
sharedHashSize = 20
22+
23+
// sharedSecretSize is the size in bytes of the shared secrets.
24+
sharedSecretSize = 32
25+
)
26+
27+
var (
28+
// sharedHashBucket is a bucket which houses all the first sharedHashSize
29+
// bytes of a received HTLC's hashed shared secret and the HTLC's
30+
// expiry block height.
31+
sharedHashBucket = []byte("shared-hash")
32+
)
33+
34+
// DecayedLog implements the PersistLog interface. It stores the first
35+
// sharedHashSize bytes of a sha256-hashed shared secret along with a node's
36+
// CLTV value. It is a decaying log meaning there will be a garbage collector
37+
// to collect entries which are expired according to their stored CLTV value
38+
// and the current block height. DecayedLog wraps channeldb for simplicity, but
39+
// must batch writes to the database to decrease write contention.
40+
type DecayedLog struct {
41+
db *channeldb.DB
42+
wg sync.WaitGroup
43+
quit chan (struct{})
44+
Notifier chainntnfs.ChainNotifier
45+
}
46+
47+
// garbageCollector deletes entries from sharedHashBucket whose expiry height
48+
// has already past. This function MUST be run as a goroutine.
49+
func (d *DecayedLog) garbageCollector() error {
50+
defer d.wg.Done()
51+
52+
epochClient, err := d.Notifier.RegisterBlockEpochNtfn()
53+
if err != nil {
54+
return fmt.Errorf("Unable to register for epoch "+
55+
"notification: %v", err)
56+
}
57+
defer epochClient.Cancel()
58+
59+
outer:
60+
for {
61+
select {
62+
case epoch, ok := <-epochClient.Epochs:
63+
if !ok {
64+
return fmt.Errorf("Epoch client shutting " +
65+
"down")
66+
}
67+
68+
var expiredCltv [][]byte
69+
err := d.db.View(func(tx *bolt.Tx) error {
70+
// Grab the shared hash bucket
71+
sharedHashes := tx.Bucket(sharedHashBucket)
72+
if sharedHashes == nil {
73+
return fmt.Errorf("sharedHashBucket " +
74+
"is nil")
75+
}
76+
77+
sharedHashes.ForEach(func(k, v []byte) error {
78+
cltv := uint32(binary.BigEndian.Uint32(v))
79+
if uint32(epoch.Height) > cltv {
80+
// Store expired hash in array
81+
expiredCltv = append(expiredCltv, k)
82+
}
83+
return nil
84+
})
85+
86+
return nil
87+
})
88+
if err != nil {
89+
return fmt.Errorf("Error viewing channeldb: "+
90+
"%v", err)
91+
}
92+
93+
// Delete every item in array
94+
for _, hash := range expiredCltv {
95+
err = d.Delete(hash)
96+
if err != nil {
97+
return fmt.Errorf("Unable to delete"+
98+
"expired secret: %v", err)
99+
}
100+
}
101+
102+
case <-d.quit:
103+
break outer
104+
}
105+
}
106+
107+
return nil
108+
}
109+
110+
// A compile time check to see if DecayedLog adheres to the PersistLog
111+
// interface.
112+
var _ PersistLog = (*DecayedLog)(nil)
113+
114+
// HashSharedSecret Sha-256 hashes the shared secret and returns the first
115+
// sharedHashSize bytes of the hash.
116+
func HashSharedSecret(sharedSecret [sharedSecretSize]byte) [sharedHashSize]byte {
117+
// Sha256 hash of sharedSecret
118+
h := sha256.New()
119+
h.Write(sharedSecret[:])
120+
121+
var sharedHash [sharedHashSize]byte
122+
123+
// Copy bytes to sharedHash
124+
copy(sharedHash[:], h.Sum(nil)[:sharedHashSize])
125+
return sharedHash
126+
}
127+
128+
// Delete removes a <shared secret hash, CLTV value> key-pair from the
129+
// sharedHashBucket.
130+
func (d *DecayedLog) Delete(hash []byte) error {
131+
return d.db.Update(func(tx *bolt.Tx) error {
132+
sharedHashes, err := tx.CreateBucketIfNotExists(sharedHashBucket)
133+
if err != nil {
134+
return fmt.Errorf("Unable to created sharedHashes bucket:"+
135+
" %v", err)
136+
}
137+
138+
return sharedHashes.Delete(hash)
139+
})
140+
}
141+
142+
// Get retrieves the CLTV value of a processed HTLC given the first 20 bytes
143+
// of the Sha-256 hash of the shared secret used during sphinx processing.
144+
func (d *DecayedLog) Get(hash []byte) (
145+
uint32, error) {
146+
var value uint32
147+
148+
err := d.db.View(func(tx *bolt.Tx) error {
149+
// Grab the shared hash bucket which stores the mapping from
150+
// truncated sha-256 hashes of shared secrets to CLTV values.
151+
sharedHashes := tx.Bucket(sharedHashBucket)
152+
if sharedHashes == nil {
153+
return fmt.Errorf("sharedHashes is nil, could " +
154+
"not retrieve CLTV value")
155+
}
156+
157+
// If the sharedHash is found, we use it to find the associated
158+
// CLTV in the sharedHashBucket.
159+
valueBytes := sharedHashes.Get(hash)
160+
if valueBytes == nil {
161+
return nil
162+
}
163+
164+
value = uint32(binary.BigEndian.Uint32(valueBytes))
165+
166+
return nil
167+
})
168+
if err != nil {
169+
return value, err
170+
}
171+
172+
return value, nil
173+
}
174+
175+
// Put stores a <shared secret hash, CLTV value> key-pair into the
176+
// sharedHashBucket.
177+
func (d *DecayedLog) Put(hash []byte,
178+
value uint32) error {
179+
return d.db.Batch(func(tx *bolt.Tx) error {
180+
var scratch [4]byte
181+
182+
sharedHashes, err := tx.CreateBucketIfNotExists(sharedHashBucket)
183+
if err != nil {
184+
return fmt.Errorf("Unable to create bucket sharedHashes:"+
185+
" %v", err)
186+
}
187+
188+
// Store value into scratch
189+
binary.BigEndian.PutUint32(scratch[:], value)
190+
191+
return sharedHashes.Put(hash, scratch[:])
192+
})
193+
}
194+
195+
// Start opens the database we will be using to store hashed shared secrets.
196+
// It also starts the garbage collector in a goroutine to remove stale
197+
// database entries.
198+
func (d *DecayedLog) Start() error {
199+
// Create the quit channel
200+
d.quit = make(chan struct{})
201+
202+
// Open the channeldb for use.
203+
var err error
204+
if d.db, err = channeldb.Open(defaultDbDirectory); err != nil {
205+
return fmt.Errorf("Could not open channeldb: %v", err)
206+
}
207+
208+
err = d.db.Update(func(tx *bolt.Tx) error {
209+
_, err := tx.CreateBucketIfNotExists(sharedHashBucket)
210+
if err != nil {
211+
return fmt.Errorf("Unable to create bucket sharedHashes:"+
212+
" %v", err)
213+
}
214+
return nil
215+
})
216+
if err != nil {
217+
return fmt.Errorf("Could not create sharedHashes")
218+
}
219+
220+
// Start garbage collector.
221+
if d.Notifier != nil {
222+
d.wg.Add(1)
223+
go d.garbageCollector()
224+
}
225+
226+
return nil
227+
}
228+
229+
// Stop halts the garbage collector and closes channeldb.
230+
func (d *DecayedLog) Stop() {
231+
// Stop garbage collector.
232+
close(d.quit)
233+
234+
// Close channeldb.
235+
d.db.Close()
236+
}

0 commit comments

Comments
 (0)