Skip to content

Commit a1d6ae8

Browse files
committed
sweeptimelockmanual: refactor and implement new ECDH format
We need to implement the new ECDH based revocation root format.
1 parent 9000e82 commit a1d6ae8

File tree

5 files changed

+224
-48
lines changed

5 files changed

+224
-48
lines changed

cmd/chantools/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
const (
2828
defaultAPIURL = "https://blockstream.info/api"
29-
version = "0.9.5"
29+
version = "0.9.6"
3030
na = "n/a"
3131

3232
Commit = ""

cmd/chantools/sweeptimelockmanual.go

Lines changed: 115 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"encoding/hex"
66
"fmt"
7-
87
"github.com/btcsuite/btcd/btcec"
98
"github.com/btcsuite/btcd/chaincfg/chainhash"
109
"github.com/btcsuite/btcd/txscript"
@@ -165,51 +164,17 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
165164
commitPoint *btcec.PublicKey
166165
)
167166
for i := uint32(0); i < maxKeys; i++ {
168-
// The easy part first, let's derive the delay base point.
169-
delayPath := []uint32{
170-
lnd.HardenedKey(uint32(keychain.KeyFamilyDelayBase)), 0,
171-
i,
172-
}
173-
delayPrivKey, err := lnd.PrivKeyFromPath(baseKey, delayPath)
174-
if err != nil {
175-
return err
176-
}
177-
178-
// Get the revocation base point first so we can calculate our
179-
// commit point.
180-
revPath := []uint32{
181-
lnd.HardenedKey(uint32(
182-
keychain.KeyFamilyRevocationRoot,
183-
)), 0, i,
184-
}
185-
revRoot, err := lnd.ShaChainFromPath(baseKey, revPath)
186-
if err != nil {
187-
return err
188-
}
189-
190-
// We now have everything to brute force the lock script. This
191-
// will take a long while as we both have to go through commit
192-
// points and CSV values.
193-
csvTimeout, script, scriptHash, commitPoint, err =
194-
bruteForceDelayPoint(
195-
delayPrivKey.PubKey(), remoteRevPoint, revRoot,
196-
lockScript, maxCsvTimeout,
197-
)
167+
csvTimeout, script, scriptHash, commitPoint, delayDesc, err = tryKey(
168+
baseKey, remoteRevPoint, maxCsvTimeout, lockScript, i,
169+
)
198170

199171
if err == nil {
200-
delayDesc = &keychain.KeyDescriptor{
201-
PubKey: delayPrivKey.PubKey(),
202-
KeyLocator: keychain.KeyLocator{
203-
Family: keychain.KeyFamilyDelayBase,
204-
Index: i,
205-
},
206-
}
172+
log.Infof("Found keys at index %d with CSV timeout %d",
173+
i, csvTimeout)
207174
break
208175
}
209176

210-
if i != 0 && i%20 == 0 {
211-
fmt.Printf("Tried %d of %d keys.", i, maxKeys)
212-
}
177+
log.Infof("Tried %d of %d keys.", i+1, maxKeys)
213178
}
214179

215180
// Did we find what we looked for or did we just exhaust all
@@ -318,6 +283,115 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
318283

319284
}
320285

286+
func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
287+
maxCsvTimeout uint16, lockScript []byte, idx uint32) (int32, []byte,
288+
[]byte, *btcec.PublicKey, *keychain.KeyDescriptor, error) {
289+
290+
// The easy part first, let's derive the delay base point.
291+
delayPath := []uint32{
292+
lnd.HardenedKey(uint32(keychain.KeyFamilyDelayBase)),
293+
0, idx,
294+
}
295+
delayPrivKey, err := lnd.PrivKeyFromPath(baseKey, delayPath)
296+
if err != nil {
297+
return 0, nil, nil, nil, nil, err
298+
}
299+
300+
// Get the revocation base point first, so we can calculate our
301+
// commit point. We start with the old way where the revocation index
302+
// was the same as the other indices.
303+
revPath := []uint32{
304+
lnd.HardenedKey(uint32(
305+
keychain.KeyFamilyRevocationRoot,
306+
)), 0, idx,
307+
}
308+
revRoot, err := lnd.ShaChainFromPath(baseKey, revPath, nil)
309+
if err != nil {
310+
return 0, nil, nil, nil, nil, err
311+
}
312+
313+
// We now have everything to brute force the lock script. This
314+
// will take a long while as we both have to go through commit
315+
// points and CSV values.
316+
csvTimeout, script, scriptHash, commitPoint, err := bruteForceDelayPoint(
317+
delayPrivKey.PubKey(), remoteRevPoint, revRoot, lockScript,
318+
maxCsvTimeout,
319+
)
320+
if err == nil {
321+
return csvTimeout, script, scriptHash, commitPoint,
322+
&keychain.KeyDescriptor{
323+
PubKey: delayPrivKey.PubKey(),
324+
KeyLocator: keychain.KeyLocator{
325+
Family: keychain.KeyFamilyDelayBase,
326+
Index: idx,
327+
},
328+
}, nil
329+
}
330+
331+
// Now let's try with the new format where the index is one larger than
332+
// the other indices.
333+
revPath = []uint32{
334+
lnd.HardenedKey(uint32(
335+
keychain.KeyFamilyRevocationRoot,
336+
)), 0, idx + 1,
337+
}
338+
revRoot2, err := lnd.ShaChainFromPath(baseKey, revPath, nil)
339+
if err != nil {
340+
return 0, nil, nil, nil, nil, err
341+
}
342+
343+
// We now have everything to brute force the lock script. This
344+
// will take a long while as we both have to go through commit
345+
// points and CSV values.
346+
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
347+
delayPrivKey.PubKey(), remoteRevPoint, revRoot2, lockScript,
348+
maxCsvTimeout,
349+
)
350+
if err == nil {
351+
return csvTimeout, script, scriptHash, commitPoint,
352+
&keychain.KeyDescriptor{
353+
PubKey: delayPrivKey.PubKey(),
354+
KeyLocator: keychain.KeyLocator{
355+
Family: keychain.KeyFamilyDelayBase,
356+
Index: idx,
357+
},
358+
}, nil
359+
}
360+
361+
// Now we try the same with the new revocation producer format.
362+
multiSigPath := []uint32{
363+
lnd.HardenedKey(uint32(keychain.KeyFamilyMultiSig)),
364+
0, idx,
365+
}
366+
multiSigPrivKey, err := lnd.PrivKeyFromPath(
367+
baseKey, multiSigPath,
368+
)
369+
370+
revRoot3, err := lnd.ShaChainFromPath(
371+
baseKey, revPath, multiSigPrivKey.PubKey(),
372+
)
373+
if err != nil {
374+
return 0, nil, nil, nil, nil, err
375+
}
376+
377+
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
378+
delayPrivKey.PubKey(), remoteRevPoint, revRoot3, lockScript,
379+
maxCsvTimeout,
380+
)
381+
if err == nil {
382+
return csvTimeout, script, scriptHash, commitPoint,
383+
&keychain.KeyDescriptor{
384+
PubKey: delayPrivKey.PubKey(),
385+
KeyLocator: keychain.KeyLocator{
386+
Family: keychain.KeyFamilyDelayBase,
387+
Index: idx,
388+
},
389+
}, nil
390+
}
391+
392+
return 0, nil, nil, nil, nil, fmt.Errorf("target script not derived")
393+
}
394+
321395
func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
322396
revRoot *shachain.RevocationProducer, lockScript []byte,
323397
maxCsvTimeout uint16) (int32, []byte, []byte, *btcec.PublicKey, error) {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
7+
"github.com/btcsuite/btcd/btcec"
8+
"github.com/btcsuite/btcd/chaincfg"
9+
"github.com/btcsuite/btcutil/hdkeychain"
10+
"github.com/guggero/chantools/lnd"
11+
)
12+
13+
var sweepTimeLockManualCases = []struct {
14+
baseKey string
15+
keyIndex uint32
16+
timeLockAddr string
17+
remoteRevPubKey string
18+
}{{
19+
// New format with ECDH revocation root.
20+
baseKey: "tprv8dgoXnQWBN4CGGceRYMW495kWcrUZKZVFwMmbzpduFp1D4pi" +
21+
"3B2t37zTG5Fx66XWPDQYi3Q5vqDgmmZ5ffrqZ9H4s2EhJu9WaJjY3SKaWDK",
22+
keyIndex: 7,
23+
timeLockAddr: "bcrt1qf9zv4qtxh27c954rhlzg4tx58xh0vgssuu0csrlep0jdnv" +
24+
"lx9xesmcl5qx",
25+
remoteRevPubKey: "03235261ed5aaaf9fec0e91d5e1a4d17f1a2c7442f1c43806d32" +
26+
"c9bd34abd002a3",
27+
}, {
28+
// Old format with plain private key as revocation root.
29+
baseKey: "tprv8dgoXnQWBN4CGGceRYMW495kWcrUZKZVFwMmbzpduFp1D4pi" +
30+
"3B2t37zTG5Fx66XWPDQYi3Q5vqDgmmZ5ffrqZ9H4s2EhJu9WaJjY3SKaWDK",
31+
keyIndex: 6,
32+
timeLockAddr: "bcrt1qa5rrlswxefc870k7rsza5hhqd37uytczldjk5t0vzd95u9" +
33+
"hs8xlsfdc3zf",
34+
remoteRevPubKey: "03e82cdf164ce5aba253890e066129f134ca8d7e072ce5ad55c7" +
35+
"21b9a13545ee04",
36+
}}
37+
38+
func TestSweepTimeLockManual(t *testing.T) {
39+
for _, tc := range sweepTimeLockManualCases {
40+
// First, we need to parse the lock addr and make sure we can
41+
// brute force the script with the information we have. If not,
42+
// we can't continue anyway.
43+
lockScript, err := lnd.GetP2WSHScript(
44+
tc.timeLockAddr, &chaincfg.RegressionNetParams,
45+
)
46+
if err != nil {
47+
t.Fatalf("invalid time lock addr: %v", err)
48+
}
49+
50+
baseKey, err := hdkeychain.NewKeyFromString(tc.baseKey)
51+
if err != nil {
52+
t.Fatalf("couldn't derive base key: %v", err)
53+
}
54+
55+
revPubKeyBytes, _ := hex.DecodeString(tc.remoteRevPubKey)
56+
revPubKey, _ := btcec.ParsePubKey(revPubKeyBytes, btcec.S256())
57+
58+
_, _, _, _, _, err = tryKey(
59+
baseKey, revPubKey, defaultCsvLimit, lockScript,
60+
tc.keyIndex,
61+
)
62+
if err != nil {
63+
t.Fatalf("couldn't derive key: %v", err)
64+
}
65+
}
66+
}

lnd/hdkeychain.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,36 @@ func PrivKeyFromPath(extendedKey *hdkeychain.ExtendedKey,
121121
return privKey, nil
122122
}
123123

124-
func ShaChainFromPath(extendedKey *hdkeychain.ExtendedKey,
125-
path []uint32) (*shachain.RevocationProducer, error) {
124+
func ShaChainFromPath(extendedKey *hdkeychain.ExtendedKey, path []uint32,
125+
multiSigPubKey *btcec.PublicKey) (*shachain.RevocationProducer, error) {
126126

127127
privKey, err := PrivKeyFromPath(extendedKey, path)
128128
if err != nil {
129129
return nil, err
130130
}
131-
revRoot, err := chainhash.NewHash(privKey.Serialize())
131+
132+
// This is the legacy way where we just used the private key as the
133+
// revocation root directly.
134+
if multiSigPubKey == nil {
135+
revRoot, err := chainhash.NewHash(privKey.Serialize())
136+
if err != nil {
137+
return nil, fmt.Errorf("could not create revocation "+
138+
"root hash: %v", err)
139+
}
140+
return shachain.NewRevocationProducer(*revRoot), nil
141+
}
142+
143+
// Perform an ECDH operation between the private key described in
144+
// nextRevocationKeyDesc and our public multisig key. The result will be
145+
// used to seed the revocation producer.
146+
revRoot, err := ECDH(privKey, multiSigPubKey)
132147
if err != nil {
133-
return nil, fmt.Errorf("could not create revocation root "+
134-
"hash: %v", err)
148+
return nil, err
135149
}
136-
return shachain.NewRevocationProducer(*revRoot), nil
150+
151+
// Once we have the root, we can then generate our shachain producer
152+
// and from that generate the per-commitment point.
153+
return shachain.NewRevocationProducer(revRoot), nil
137154
}
138155

139156
func IdentityPath(params *chaincfg.Params) string {

lnd/signer.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lnd
22

33
import (
4+
"crypto/sha256"
45
"fmt"
56

67
"github.com/btcsuite/btcd/btcec"
@@ -115,3 +116,21 @@ func maybeTweakPrivKey(signDesc *input.SignDescriptor,
115116
}
116117
return privKey
117118
}
119+
120+
// ECDH performs a scalar multiplication (ECDH-like operation) between the
121+
// target private key and remote public key. The output returned will be
122+
// the sha256 of the resulting shared point serialized in compressed format. If
123+
// k is our private key, and P is the public key, we perform the following
124+
// operation:
125+
//
126+
// sx := k*P s := sha256(sx.SerializeCompressed())
127+
func ECDH(privKey *btcec.PrivateKey, pub *btcec.PublicKey) ([32]byte, error) {
128+
s := &btcec.PublicKey{}
129+
x, y := btcec.S256().ScalarMult(pub.X, pub.Y, privKey.D.Bytes())
130+
s.X = x
131+
s.Y = y
132+
133+
h := sha256.Sum256(s.SerializeCompressed())
134+
135+
return h, nil
136+
}

0 commit comments

Comments
 (0)