Skip to content

Commit e9cb480

Browse files
committed
path: add blind route and decrypt data funcs
This commit introduces a new `BlindedPath` struct which holds all the information defining a blinded route. The commit also includes the addition of various helper methods to blind and unblind node IDs and hop data. Test vectors from the spec are also added.
1 parent 6afc43f commit e9cb480

File tree

4 files changed

+552
-11
lines changed

4 files changed

+552
-11
lines changed

crypto.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/aead/chacha20"
1111
"github.com/btcsuite/btcd/btcec/v2"
1212
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
13+
"golang.org/x/crypto/chacha20poly1305"
1314
)
1415

1516
const (
@@ -19,6 +20,10 @@ const (
1920
HMACSize = 32
2021
)
2122

23+
// chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305
24+
// encryption and decryption.
25+
var chaChaPolyZeroNonce [chacha20poly1305.NonceSize]byte
26+
2227
// Hash256 is a statically sized, 32-byte array, typically containing
2328
// the output of a SHA256 hash.
2429
type Hash256 [sha256.Size]byte
@@ -61,8 +66,8 @@ func (p *PrivKeyECDH) PubKey() *btcec.PublicKey {
6166
// k is our private key, and P is the public key, we perform the following
6267
// operation:
6368
//
64-
// sx := k*P
65-
// s := sha256(sx.SerializeCompressed())
69+
// sx := k*P
70+
// s := sha256(sx.SerializeCompressed())
6671
//
6772
// NOTE: This is part of the SingleKeyECDH interface.
6873
func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) {
@@ -199,6 +204,31 @@ func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey {
199204
return priv.PubKey()
200205
}
201206

207+
// chacha20polyEncrypt initialises the ChaCha20Poly1305 algorithm with the given
208+
// key and uses it to encrypt the passed message. This uses an all-zero nonce as
209+
// required by the route-blinding spec.
210+
func chacha20polyEncrypt(key, plainTxt []byte) ([]byte, error) {
211+
aead, err := chacha20poly1305.New(key)
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
return aead.Seal(plainTxt[:0], chaChaPolyZeroNonce[:], plainTxt, nil),
217+
nil
218+
}
219+
220+
// chacha20polyDecrypt initialises the ChaCha20Poly1305 algorithm with the given
221+
// key and uses it to decrypt the passed cipher text. This uses an all-zero
222+
// nonce as required by the route-blinding spec.
223+
func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) {
224+
aead, err := chacha20poly1305.New(key)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
return aead.Open(cipherTxt[:0], chaChaPolyZeroNonce[:], cipherTxt, nil)
230+
}
231+
202232
// sharedSecretGenerator is an interface that abstracts away exactly *how* the
203233
// shared secret for each hop is generated.
204234
//

path.go

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
package sphinx
22

33
import (
4+
"errors"
5+
"fmt"
6+
47
"github.com/btcsuite/btcd/btcec/v2"
58
)
69

7-
// NumMaxHops is the maximum path length. There is a maximum of 1300 bytes in
8-
// the routing info block. Legacy hop payloads are always 65 bytes, while tlv
9-
// payloads are at least 47 bytes (tlvlen 1, amt 2, timelock 2, nextchan 10,
10-
// hmac 32) for the intermediate hops and 37 bytes (tlvlen 1, amt 2, timelock 2,
11-
// hmac 32) for the exit hop. The maximum path length can therefore only be
12-
// reached by using tlv payloads only. With that, the maximum number of
13-
// intermediate hops is: Floor((1300 - 37) / 47) = 26. Including the exit hop,
14-
// the maximum path length is 27 hops.
15-
const NumMaxHops = 27
10+
const (
11+
// NumMaxHops is the maximum path length. There is a maximum of 1300
12+
// bytes in the routing info block. Legacy hop payloads are always 65
13+
// bytes, while tlv payloads are at least 47 bytes (tlvlen 1, amt 2,
14+
// timelock 2, nextchan 10, hmac 32) for the intermediate hops and 37
15+
// bytes (tlvlen 1, amt 2, timelock 2, hmac 32) for the exit hop. The
16+
// maximum path length can therefore only be reached by using tlv
17+
// payloads only. With that, the maximum number of intermediate hops
18+
// is: Floor((1300 - 37) / 47) = 26. Including the exit hop, the
19+
// maximum path length is 27 hops.
20+
NumMaxHops = 27
21+
22+
routeBlindingHMACKey = "blinded_node_id"
23+
)
1624

1725
// PaymentPath represents a series of hops within the Lightning Network
1826
// starting at a sender and terminating at a receiver. Each hop contains a set
@@ -93,3 +101,158 @@ func (p *PaymentPath) TotalPayloadSize() int {
93101

94102
return totalSize
95103
}
104+
105+
// BlindedPath represents all the data that the creator of a blinded path must
106+
// transmit to the builder of route that will send to this path.
107+
type BlindedPath struct {
108+
// IntroductionPoint is the real node ID of the first hop in the blinded
109+
// path. The sender should be able to find this node in the network
110+
// graph and route to it.
111+
IntroductionPoint *btcec.PublicKey
112+
113+
// BlindingPoint is the first ephemeral blinding point. This is the
114+
// point that the introduction node will use in order to create a shared
115+
// secret with the builder of the blinded route. This point will need
116+
// to be communicated to the introduction node by the sender in some
117+
// way.
118+
BlindingPoint *btcec.PublicKey
119+
120+
// BlindedHops is a list of ordered BlindedHopInfo. Each entry
121+
// represents a hop in the blinded path along with the encrypted data to
122+
// be sent to that node. Note that the first entry in the list
123+
// represents the introduction point of the path and so the node ID of
124+
// this point does not strictly need to be transmitted to the sender
125+
// since they will be able to derive the point using the BlindingPoint.
126+
BlindedHops []*BlindedHopInfo
127+
}
128+
129+
// BlindedHopInfo represents a blinded node pub key along with the encrypted
130+
// data for a node in a blinded route.
131+
type BlindedHopInfo struct {
132+
// BlindedNodePub is the blinded public key of the node in the blinded
133+
// route.
134+
BlindedNodePub *btcec.PublicKey
135+
136+
// CipherText is the encrypted payload to be transported to the hop in
137+
// the blinded route.
138+
CipherText []byte
139+
}
140+
141+
// HopInfo represents a real node pub key along with the plaintext data for a
142+
// node in a blinded route.
143+
type HopInfo struct {
144+
// NodePub is the real public key of the node in the blinded route.
145+
NodePub *btcec.PublicKey
146+
147+
// PlainText is the un-encrypted payload to be transported to the hop
148+
// the blinded route.
149+
PlainText []byte
150+
}
151+
152+
// Encrypt uses the given sharedSecret to blind the public key of the node and
153+
// encrypt the payload and returns the resulting BlindedHopInfo.
154+
func (i *HopInfo) Encrypt(sharedSecret Hash256) (*BlindedHopInfo, error) {
155+
blindedData, err := encryptBlindedHopData(sharedSecret, i.PlainText)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
return &BlindedHopInfo{
161+
BlindedNodePub: blindNodeID(sharedSecret, i.NodePub),
162+
CipherText: blindedData,
163+
}, nil
164+
}
165+
166+
// BuildBlindedPath creates a new BlindedPath from a session key along with a
167+
// list of HopInfo representing the nodes in the blinded path. The first hop in
168+
// paymentPath is expected to be the introduction node.
169+
func BuildBlindedPath(sessionKey *btcec.PrivateKey,
170+
paymentPath []*HopInfo) (*BlindedPath, error) {
171+
172+
if len(paymentPath) < 1 {
173+
return nil, errors.New("at least 1 hop is required to create " +
174+
"a blinded path")
175+
}
176+
177+
bp := &BlindedPath{
178+
IntroductionPoint: paymentPath[0].NodePub,
179+
BlindingPoint: sessionKey.PubKey(),
180+
BlindedHops: make([]*BlindedHopInfo, len(paymentPath)),
181+
}
182+
183+
keys := make([]*btcec.PublicKey, len(paymentPath))
184+
for i, p := range paymentPath {
185+
keys[i] = p.NodePub
186+
}
187+
188+
hopSharedSecrets, err := generateSharedSecrets(keys, sessionKey)
189+
if err != nil {
190+
return nil, fmt.Errorf("error generating shared secret: %v",
191+
err)
192+
}
193+
194+
for i, hop := range paymentPath {
195+
blindedInfo, err := hop.Encrypt(hopSharedSecrets[i])
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
bp.BlindedHops[i] = blindedInfo
201+
}
202+
203+
return bp, nil
204+
}
205+
206+
// blindNodeID blinds the given public key using the provided shared secret.
207+
func blindNodeID(sharedSecret Hash256,
208+
pubKey *btcec.PublicKey) *btcec.PublicKey {
209+
210+
blindingFactorBytes := generateKey(routeBlindingHMACKey, &sharedSecret)
211+
212+
var blindingFactor btcec.ModNScalar
213+
blindingFactor.SetBytes(&blindingFactorBytes)
214+
215+
return blindGroupElement(pubKey, blindingFactor)
216+
}
217+
218+
// encryptBlindedHopData blinds/encrypts the given plain text data using the
219+
// provided shared secret.
220+
func encryptBlindedHopData(sharedSecret Hash256, plainTxt []byte) ([]byte,
221+
error) {
222+
223+
rhoKey := generateKey("rho", &sharedSecret)
224+
225+
return chacha20polyEncrypt(rhoKey[:], plainTxt)
226+
}
227+
228+
// decryptBlindedHopData decrypts the data encrypted by the creator of the
229+
// blinded route.
230+
func decryptBlindedHopData(privKey SingleKeyECDH, ephemPub *btcec.PublicKey,
231+
encryptedData []byte) ([]byte, error) {
232+
233+
ss, err := privKey.ECDH(ephemPub)
234+
if err != nil {
235+
return nil, err
236+
}
237+
238+
ssHash := Hash256(ss)
239+
rho := generateKey("rho", &ssHash)
240+
241+
return chacha20polyDecrypt(rho[:], encryptedData)
242+
}
243+
244+
// NextEphemeral computes the next ephemeral key given the current ephemeral
245+
// key and this node's private key.
246+
func NextEphemeral(privKey SingleKeyECDH,
247+
ephemPub *btcec.PublicKey) (*btcec.PublicKey, error) {
248+
249+
ss, err := privKey.ECDH(ephemPub)
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
blindingFactor := computeBlindingFactor(ephemPub, ss[:])
255+
nextEphem := blindGroupElement(ephemPub, blindingFactor)
256+
257+
return nextEphem, nil
258+
}

0 commit comments

Comments
 (0)