Skip to content

Commit e926602

Browse files
committed
sphinx: adds batched processing of onion pkts via Tx
1 parent d1efa2d commit e926602

File tree

1 file changed

+153
-53
lines changed

1 file changed

+153
-53
lines changed

sphinx.go

Lines changed: 153 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"math"
1212
"math/big"
1313

14-
"github.com/Crypt-iQ/lightning-onion/persistlog"
1514
"github.com/aead/chacha20"
1615
"github.com/lightningnetwork/lnd/chainntnfs"
1716
"github.com/roasbeef/btcd/btcec"
@@ -70,6 +69,10 @@ const (
7069
baseVersion = 0
7170
)
7271

72+
// Hash256 is a statically sized, 32-byte array, typically containing
73+
// the output of a SHA256 hash.
74+
type Hash256 [sha256.Size]byte
75+
7376
var (
7477
// paddingBytes are the padding bytes used to fill out the remainder of the
7578
// unused portion of the per-hop payload.
@@ -207,14 +210,14 @@ func (hd *HopData) Decode(r io.Reader) error {
207210
// generateSharedSecrets by the given nodes pubkeys, generates the shared
208211
// secrets.
209212
func generateSharedSecrets(paymentPath []*btcec.PublicKey,
210-
sessionKey *btcec.PrivateKey) [][sha256.Size]byte {
213+
sessionKey *btcec.PrivateKey) []Hash256 {
211214
// Each hop performs ECDH with our ephemeral key pair to arrive at a
212215
// shared secret. Additionally, each hop randomizes the group element
213216
// for the next hop by multiplying it by the blinding factor. This way
214217
// we only need to transmit a single group element, and hops can't link
215218
// a session back to us if they have several nodes in the path.
216219
numHops := len(paymentPath)
217-
hopSharedSecrets := make([][sha256.Size]byte, numHops)
220+
hopSharedSecrets := make([]Hash256, numHops)
218221

219222
// Compute the triplet for the first hop outside of the main loop.
220223
// Within the loop each new triplet will be computed recursively based
@@ -301,8 +304,8 @@ func NewOnionPacket(paymentPath []*btcec.PublicKey, sessionKey *btcec.PrivateKey
301304
// We'll derive the two keys we need for each hop in order to:
302305
// generate our stream cipher bytes for the mixHeader, and
303306
// calculate the MAC over the entire constructed packet.
304-
rhoKey := generateKey("rho", hopSharedSecrets[i])
305-
muKey := generateKey("mu", hopSharedSecrets[i])
307+
rhoKey := generateKey("rho", &hopSharedSecrets[i])
308+
muKey := generateKey("mu", &hopSharedSecrets[i])
306309

307310
// The HMAC for the final hop is simply zeroes. This allows the
308311
// last hop to recognize that it is the destination for a
@@ -379,13 +382,13 @@ func rightShift(slice []byte, num int) {
379382
// "filler" bytes produced by this function at the last hop. Using this
380383
// methodology, the size of the field stays constant at each hop.
381384
func generateHeaderPadding(key string, numHops int, hopSize int,
382-
sharedSecrets [][sharedSecretSize]byte) []byte {
385+
sharedSecrets []Hash256) []byte {
383386

384387
filler := make([]byte, (numHops-1)*hopSize)
385388
for i := 1; i < numHops; i++ {
386389
totalFillerSize := ((NumMaxHops - i) + 1) * hopSize
387390

388-
streamKey := generateKey(key, sharedSecrets[i-1])
391+
streamKey := generateKey(key, &sharedSecrets[i-1])
389392
streamBytes := generateCipherStream(streamKey, numStreamBytes)
390393

391394
xor(filler, filler, streamBytes[totalFillerSize:totalFillerSize+i*hopSize])
@@ -486,7 +489,7 @@ func xor(dst, a, b []byte) int {
486489
// construction/processing based off of the denoted keyType. Within Sphinx
487490
// various keys are used within the same onion packet for padding generation,
488491
// MAC generation, and encryption/decryption.
489-
func generateKey(keyType string, sharedKey [sharedSecretSize]byte) [keyLen]byte {
492+
func generateKey(keyType string, sharedKey *Hash256) [keyLen]byte {
490493
mac := hmac.New(sha256.New, []byte(keyType))
491494
mac.Write(sharedKey[:])
492495
h := mac.Sum(nil)
@@ -517,12 +520,14 @@ func generateCipherStream(key [keyLen]byte, numBytes uint) []byte {
517520
// computeBlindingFactor for the next hop given the ephemeral pubKey and
518521
// sharedSecret for this hop. The blinding factor is computed as the
519522
// sha-256(pubkey || sharedSecret).
520-
func computeBlindingFactor(hopPubKey *btcec.PublicKey, hopSharedSecret []byte) [sha256.Size]byte {
523+
func computeBlindingFactor(hopPubKey *btcec.PublicKey,
524+
hopSharedSecret []byte) Hash256 {
525+
521526
sha := sha256.New()
522527
sha.Write(hopPubKey.SerializeCompressed())
523528
sha.Write(hopSharedSecret)
524529

525-
var hash [sha256.Size]byte
530+
var hash Hash256
526531
copy(hash[:], sha.Sum(nil))
527532
return hash
528533
}
@@ -547,7 +552,7 @@ func blindBaseElement(blindingFactor []byte) *btcec.PublicKey {
547552
// key. We then take the _entire_ point generated by the ECDH operation,
548553
// serialize that using a compressed format, then feed the raw bytes through a
549554
// single SHA256 invocation. The resulting value is the shared secret.
550-
func generateSharedSecret(pub *btcec.PublicKey, priv *btcec.PrivateKey) [32]byte {
555+
func generateSharedSecret(pub *btcec.PublicKey, priv *btcec.PrivateKey) Hash256 {
551556
s := &btcec.PublicKey{}
552557
x, y := btcec.S256().ScalarMult(pub.X, pub.Y, priv.D.Bytes())
553558
s.X = x
@@ -622,23 +627,19 @@ type Router struct {
622627

623628
onionKey *btcec.PrivateKey
624629

625-
d *persistlog.DecayedLog
630+
log ReplayLog
626631
}
627632

628633
// NewRouter creates a new instance of a Sphinx onion Router given the node's
629634
// currently advertised onion private key, and the target Bitcoin network.
630-
func NewRouter(nodeKey *btcec.PrivateKey, net *chaincfg.Params,
635+
func NewRouter(dbPath string, nodeKey *btcec.PrivateKey, net *chaincfg.Params,
631636
chainNotifier chainntnfs.ChainNotifier) *Router {
632637
var nodeID [addressSize]byte
633638
copy(nodeID[:], btcutil.Hash160(nodeKey.PubKey().SerializeCompressed()))
634639

635640
// Safe to ignore the error here, nodeID is 20 bytes.
636641
nodeAddr, _ := btcutil.NewAddressPubKeyHash(nodeID[:], net)
637642

638-
d := &persistlog.DecayedLog{
639-
Notifier: chainNotifier,
640-
}
641-
642643
return &Router{
643644
nodeID: nodeID,
644645
nodeAddr: nodeAddr,
@@ -652,20 +653,20 @@ func NewRouter(nodeKey *btcec.PrivateKey, net *chaincfg.Params,
652653
},
653654
// TODO(roasbeef): replace instead with bloom filter?
654655
// * https://moderncrypto.org/mail-archive/messaging/2015/001911.html
655-
d: d,
656+
log: NewDecayedLog(dbPath, chainNotifier),
656657
}
657658
}
658659

659660
// Start starts / opens the DecayedLog's channeldb and its accompanying
660661
// garbage collector goroutine.
661662
func (r *Router) Start() error {
662-
return r.d.Start("")
663+
return r.log.Start()
663664
}
664665

665666
// Stop stops / closes the DecayedLog's channeldb and its accompanying
666667
// garbage collector goroutine.
667668
func (r *Router) Stop() {
668-
r.d.Stop()
669+
r.log.Stop()
669670
}
670671

671672
// ProcessOnionPacket processes an incoming onion packet which has been forward
@@ -677,28 +678,46 @@ func (r *Router) Stop() {
677678
// In the case of a successful packet processing, and ProcessedPacket struct is
678679
// returned which houses the newly parsed packet, along with instructions on
679680
// what to do next.
680-
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte) (*ProcessedPacket, error) {
681-
dhKey := onionPkt.EphemeralKey
682-
routeInfo := onionPkt.RoutingInfo
683-
headerMac := onionPkt.HeaderMAC
681+
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket,
682+
assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) {
684683

684+
// Compute the shared secret for this onion packet.
685685
sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
686686
if err != nil {
687687
return nil, err
688688
}
689689

690-
// In order to mitigate replay attacks, if we've seen this particular
691-
// shared secret before, cease processing and just drop this forwarding
692-
// message.
693-
hashedSecret := persistlog.HashSharedSecret(sharedSecret)
694-
cltv, err := r.d.Get(hashedSecret[:])
690+
// Additionally, compute the hash prefix of the shared secret, which
691+
// will serve as an identifier for detecting replayed packets.
692+
hashPrefix := hashSharedSecret(&sharedSecret)
693+
694+
// Continue to optimistically process this packet, deferring replay
695+
// protection until the end to reduce the penalty of multiple IO
696+
// operations.
697+
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
695698
if err != nil {
696699
return nil, err
697700
}
698-
if cltv != math.MaxUint32 {
699-
return nil, ErrReplayedPacket
701+
702+
// Atomically compare this hash prefix with the contents of the on-disk
703+
// log, persisting it only if this entry was not detected as a replay.
704+
if err := r.log.Put(&hashPrefix, incomingCltv); err != nil {
705+
return nil, err
700706
}
701707

708+
return packet, nil
709+
}
710+
711+
// processOnionPacket performs the primary key derivation and handling of onion
712+
// packets. The processed packets returned from this method should only be used
713+
// if the packet was not flagged as a replayed packet.
714+
func processOnionPacket(onionPkt *OnionPacket,
715+
sharedSecret *Hash256, assocData []byte) (*ProcessedPacket, error) {
716+
717+
dhKey := onionPkt.EphemeralKey
718+
routeInfo := onionPkt.RoutingInfo
719+
headerMac := onionPkt.HeaderMAC
720+
702721
// Using the derived shared secret, ensure the integrity of the routing
703722
// information by checking the attached MAC without leaking timing
704723
// information.
@@ -711,9 +730,14 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte) (*P
711730
// Attach the padding zeroes in order to properly strip an encryption
712731
// layer off the routing info revealing the routing information for the
713732
// next hop.
733+
streamBytes := generateCipherStream(
734+
generateKey("rho", sharedSecret),
735+
numStreamBytes,
736+
)
737+
headerWithPadding := append(routeInfo[:],
738+
bytes.Repeat([]byte{0}, hopDataSize)...)
739+
714740
var hopInfo [numStreamBytes]byte
715-
streamBytes := generateCipherStream(generateKey("rho", sharedSecret), numStreamBytes)
716-
headerWithPadding := append(routeInfo[:], bytes.Repeat([]byte{0}, hopDataSize)...)
717741
xor(hopInfo[:], headerWithPadding, streamBytes)
718742

719743
// Randomize the DH group element for the next hop using the
@@ -729,23 +753,6 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte) (*P
729753
return nil, err
730754
}
731755

732-
// The MAC checks out, mark this current shared secret as processed in
733-
// order to mitigate future replay attacks. We need to check to see if
734-
// we already know the secret again since a replay might have happened
735-
// while we were checking the MAC and decoding the HopData.
736-
cltv, err = r.d.Get(hashedSecret[:])
737-
if err != nil {
738-
return nil, err
739-
}
740-
if cltv != math.MaxUint32 {
741-
return nil, ErrReplayedPacket
742-
}
743-
744-
err = r.d.Put(hashedSecret[:], hopData.OutgoingCltv)
745-
if err != nil {
746-
return nil, err
747-
}
748-
749756
// With the necessary items extracted, we'll copy of the onion packet
750757
// for the next node, snipping off our per-hop data.
751758
var nextMixHeader [routingInfoSize]byte
@@ -761,7 +768,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte) (*P
761768
// However if the uncovered 'nextMac' is all zeroes, then this
762769
// indicates that we're the final hop in the route.
763770
var action ProcessCode = MoreHops
764-
if bytes.Compare(bytes.Repeat([]byte{0x00}, hmacSize), hopData.HMAC[:]) == 0 {
771+
if bytes.Compare(zeroHMAC[:], hopData.HMAC[:]) == 0 {
765772
action = ExitNode
766773
}
767774

@@ -773,9 +780,9 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte) (*P
773780
}
774781

775782
// generateSharedSecret generates the shared secret by given ephemeral key.
776-
func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) ([sha256.Size]byte,
783+
func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256,
777784
error) {
778-
var sharedSecret [sha256.Size]byte
785+
var sharedSecret Hash256
779786

780787
// Ensure that the public key is on our curve.
781788
if !btcec.S256().IsOnCurve(dhKey.X, dhKey.Y) {
@@ -786,3 +793,96 @@ func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) ([sha256.Size]byte
786793
sharedSecret = generateSharedSecret(dhKey, r.onionKey)
787794
return sharedSecret, nil
788795
}
796+
797+
// Tx is a transaction consisting of a number of sphinx packets to be atomically
798+
// written to the replay log. This structure helps to coordinate construction of
799+
// the underlying Batch object, and to ensure that the result of the processing
800+
// is idempotent.
801+
type Tx struct {
802+
// batch is the set of packets to be incrementally processed and
803+
// ultimately committed in this transaction
804+
batch *Batch
805+
806+
// router is a reference to the sphinx router that created this
807+
// transaction. Committing this transaction will utilize this router's
808+
// replay log.
809+
router *Router
810+
811+
// packets contains a potentially sparse list of optimistically processed
812+
// packets for this batch. The contents of a particular index should
813+
// only be accessed if the index is *not* included in the replay set, or
814+
// otherwise failed any other stage of the processing.
815+
packets []ProcessedPacket
816+
}
817+
818+
// BeginTxn creates a new transaction that can later be committed back to the
819+
// sphinx router's replay log.
820+
//
821+
// NOTE: The nels parameter should represent the maximum number of that could be
822+
// added to the batch, using sequence numbers that match or exceed this value
823+
// could result in an out-of-bounds panic.
824+
func (r *Router) BeginTxn(id []byte, nels int) *Tx {
825+
return &Tx{
826+
batch: NewBatch(id),
827+
router: r,
828+
packets: make([]ProcessedPacket, nels),
829+
}
830+
}
831+
832+
// ProcessOnionPacket processes an incoming onion packet which has been forward
833+
// to the target Sphinx router. If the encoded ephemeral key isn't on the
834+
// target Elliptic Curve, then the packet is rejected. Similarly, if the
835+
// derived shared secret has been seen before the packet is rejected. Finally
836+
// if the MAC doesn't check the packet is again rejected.
837+
//
838+
// In the case of a successful packet processing, and ProcessedPacket struct is
839+
// returned which houses the newly parsed packet, along with instructions on
840+
// what to do next.
841+
func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
842+
assocData []byte, incomingCltv uint32) error {
843+
844+
// Compute the shared secret for this onion packet.
845+
sharedSecret, err := t.router.generateSharedSecret(
846+
onionPkt.EphemeralKey)
847+
if err != nil {
848+
return err
849+
}
850+
851+
// Additionally, compute the hash prefix of the shared secret, which
852+
// will serve as an identifier for detecting replayed packets.
853+
hashPrefix := hashSharedSecret(&sharedSecret)
854+
855+
// Continue to optimistically process this packet, deferring replay
856+
// protection until the end to reduce the penalty of multiple IO
857+
// operations.
858+
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
859+
if err != nil {
860+
return err
861+
}
862+
863+
// Add the hash prefix to pending batch of shared secrets that will be
864+
// written later via Commit().
865+
err = t.batch.Put(seqNum, &hashPrefix, incomingCltv)
866+
if err != nil {
867+
return err
868+
}
869+
870+
// If we successfully added this packet to the batch, cache the processed
871+
// packet within the Tx which can be accessed after committing if this
872+
// sequence number does not appear in the replay set.
873+
t.packets[seqNum] = *packet
874+
875+
return nil
876+
}
877+
878+
// Commit writes this transaction's batch of sphinx packets to the replay log,
879+
// performing a final check against the log for replays.
880+
func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) {
881+
if t.batch.isCommitted {
882+
return t.packets, t.batch.replaySet, nil
883+
}
884+
885+
rs, err := t.router.log.PutBatch(t.batch)
886+
887+
return t.packets, rs, err
888+
}

0 commit comments

Comments
 (0)