From 921494fe3e09258328320daa1247a9d05fb47952 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 7 May 2023 12:13:57 +0200 Subject: [PATCH 1/9] go.mod: update go version to 1.19 --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d274362..15a7943 100644 --- a/go.mod +++ b/go.mod @@ -11,4 +11,6 @@ require ( golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 ) -go 1.13 +require golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect + +go 1.19 From f30c0a73029d320d499d2dae7bef25d8aebb344d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 7 May 2023 12:05:55 +0200 Subject: [PATCH 2/9] go.mod+test: modernise the TestVariablePayloadOnion test Update the TestVariablePayloadOnion test to make use of the `require` package. --- go.mod | 7 +++++- go.sum | 14 ++++++++++++ sphinx_test.go | 62 +++++++++++++++++--------------------------------- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index 15a7943..dc4c472 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,14 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 + github.com/stretchr/testify v1.8.2 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 ) -require golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect +require ( + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) go 1.19 diff --git a/go.sum b/go.sum index fe83050..a396b02 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= @@ -53,6 +54,15 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= @@ -85,9 +95,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sphinx_test.go b/sphinx_test.go index 3425e76..353b42b 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -5,13 +5,14 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" + "os" "reflect" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" ) // BOLT 4 Test Vectors @@ -749,8 +750,8 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { } } -// testFileName is the name of the multi-frame onion test file. -const testFileName = "testdata/onion-test-multi-frame.json" +// testMultiFrameFileName is the name of the multi-frame onion test file. +const testMultiFrameFileName = "testdata/onion-test-multi-frame.json" type jsonHop struct { Type string `json:"type"` @@ -802,35 +803,26 @@ func TestVariablePayloadOnion(t *testing.T) { t.Parallel() // First, we'll read out the raw JSOn file at the target location. - jsonBytes, err := ioutil.ReadFile(testFileName) - if err != nil { - t.Fatalf("unable to read json file: %v", err) - } + jsonBytes, err := os.ReadFile(testMultiFrameFileName) + require.NoError(t, err) // Once we have the raw file, we'll unpack it into our jsonTestCase // struct defined above. testCase := &jsonTestCase{} - if err := json.Unmarshal(jsonBytes, testCase); err != nil { - t.Fatalf("unable to parse spec json file: %v", err) - } + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) // Next, we'll populate a new OnionHop using the information included // in this test case. var route PaymentPath for i, hop := range testCase.Generate.Hops { pubKeyBytes, err := hex.DecodeString(hop.Pubkey) - if err != nil { - t.Fatalf("unable to decode pubkey: %v", err) - } + require.NoError(t, err) + pubKey, err := btcec.ParsePubKey(pubKeyBytes) - if err != nil { - t.Fatalf("unable to parse BOLT 4 pubkey #%d: %v", i, err) - } + require.NoError(t, err) payload, err := hex.DecodeString(hop.Payload) - if err != nil { - t.Fatalf("unable to decode payload: %v", err) - } + require.NoError(t, err) payloadType := jsonTypeToPayloadType(hop.Type) route[i] = OnionHop{ @@ -854,39 +846,27 @@ func TestVariablePayloadOnion(t *testing.T) { } finalPacket, err := hex.DecodeString(testCase.Onion) - if err != nil { - t.Fatalf("unable to decode packet: %v", err) - } + require.NoError(t, err) sessionKeyBytes, err := hex.DecodeString(testCase.Generate.SessionKey) - if err != nil { - t.Fatalf("unable to generate session key: %v", err) - } + require.NoError(t, err) - associatedData, err := hex.DecodeString(testCase.Generate.AssociatedData) - if err != nil { - t.Fatalf("unable to decode AD: %v", err) - } + assocData, err := hex.DecodeString(testCase.Generate.AssociatedData) + require.NoError(t, err) // With all the required data assembled, we'll craft a new packet. sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) pkt, err := NewOnionPacket( - &route, sessionKey, associatedData, DeterministicPacketFiller, + &route, sessionKey, assocData, DeterministicPacketFiller, ) - if err != nil { - t.Fatalf("unable to construct onion packet: %v", err) - } + require.NoError(t, err) var b bytes.Buffer - if err := pkt.Encode(&b); err != nil { - t.Fatalf("unable to decode onion packet: %v", err) - } + require.NoError(t, pkt.Encode(&b)) // Finally, we expect that our packet matches the packet included in // the spec's test vectors. - if bytes.Compare(b.Bytes(), finalPacket) != 0 { - t.Fatalf("final packet does not match expected BOLT 4 packet, "+ - "want: %s, got %s", hex.EncodeToString(finalPacket), - hex.EncodeToString(b.Bytes())) - } + require.Equalf(t, b.Bytes(), finalPacket, "final packet does not "+ + "match expected BOLT 4 packet, want: %s, got %s", + hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) } From 7f384958327ac5294f26be94156c1cdb12330eae Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 7 May 2023 12:10:16 +0200 Subject: [PATCH 3/9] testdata+test: add pure tlv onion test Add a TestTLVPayloadOnion test that tests the newer spec test vector which tests the construction of an onion in the case where all the hops are using the TLV version payload. --- sphinx_test.go | 90 +++++++++++++++++++++++++++++++++++++++- testdata/onion-test.json | 37 +++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 testdata/onion-test.json diff --git a/sphinx_test.go b/sphinx_test.go index 353b42b..e2b9e37 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" "os" "reflect" "testing" @@ -750,8 +751,14 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { } } -// testMultiFrameFileName is the name of the multi-frame onion test file. -const testMultiFrameFileName = "testdata/onion-test-multi-frame.json" +const ( + // testMultiFrameFileName is the name of the multi-frame onion test + // file. + testMultiFrameFileName = "testdata/onion-test-multi-frame.json" + + // testTLVFileName is the name of the tlv-payload-only onion test file. + testTLVFileName = "testdata/onion-test.json" +) type jsonHop struct { Type string `json:"type"` @@ -796,6 +803,85 @@ func jsonTypeToPayloadType(jsonType string) PayloadType { } } +// TestTLVPayloadOnion tests the construction of an onion where all the payloads +// are of the TLV type. +func TestTLVPayloadOnion(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw JSON file at the target location. + jsonBytes, err := os.ReadFile(testTLVFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our jsonTestCase + // struct defined above. + testCase := &jsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + // Next, we'll populate a new OnionHop using the information included + // in this test case. + var route PaymentPath + for i, hop := range testCase.Generate.Hops { + pubKeyBytes, err := hex.DecodeString(hop.Pubkey) + require.NoError(t, err) + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + require.NoError(t, err) + + // The test has already encoded the length of the TLV payloads, + // so to make it compatible with our PaymentPath, we just + // extract the payload. + payload, err := hex.DecodeString(hop.Payload) + require.NoError(t, err) + + var ( + bufReader = bytes.NewReader(payload) + b [8]byte + ) + varInt, err := ReadVarInt(bufReader, &b) + require.NoError(t, err) + + payloadSize := uint32(varInt) + require.NoError(t, err) + + hopPayload := make([]byte, payloadSize) + _, err = io.ReadFull(bufReader, hopPayload) + require.NoError(t, err) + + route[i] = OnionHop{ + NodePub: *pubKey, + HopPayload: HopPayload{ + Type: PayloadTLV, + Payload: hopPayload, + }, + } + } + + finalPacket, err := hex.DecodeString(testCase.Onion) + require.NoError(t, err) + + sessionKeyBytes, err := hex.DecodeString(testCase.Generate.SessionKey) + require.NoError(t, err) + + assocData, err := hex.DecodeString(testCase.Generate.AssociatedData) + require.NoError(t, err) + + // With all the required data assembled, we'll craft a new packet. + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + pkt, err := NewOnionPacket( + &route, sessionKey, assocData, DeterministicPacketFiller, + ) + require.NoError(t, err) + + var b bytes.Buffer + require.NoError(t, pkt.Encode(&b)) + + // Finally, we expect that our packet matches the packet included in + // the spec's test vectors. + require.Equalf(t, b.Bytes(), finalPacket, "final packet does not "+ + "match expected BOLT 4 packet, want: %s, got %s", + hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) +} + // TestVariablePayloadOnion tests that if we construct a packet that contains a // mix of the old and new payload format, that we match the version that's // included in the spec. diff --git a/testdata/onion-test.json b/testdata/onion-test.json new file mode 100644 index 0000000..193256f --- /dev/null +++ b/testdata/onion-test.json @@ -0,0 +1,37 @@ +{ + "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.", + "generate": { + "session_key": "4141414141414141414141414141414141414141414141414141414141414141", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "hops": [ + { + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "1202023a98040205dc06080000000000000001" + }, + { + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" + }, + { + "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "payload": "12020230d4040204e206080000000000000003" + }, + { + "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "payload": "1202022710040203e806080000000000000004" + }, + { + "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + } + ] + }, + "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20", + "decode": [ + "4141414141414141414141414141414141414141414141414141414141414141", + "4242424242424242424242424242424242424242424242424242424242424242", + "4343434343434343434343434343434343434343434343434343434343434343", + "4444444444444444444444444444444444444444444444444444444444444444", + "4545454545454545454545454545454545454545454545454545454545454545" + ] +} From 258028a7e4d915549398dba74a280c96786f730e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 20:08:12 +0200 Subject: [PATCH 4/9] cmd+go.mod: upgrade cli tool to use urfave lib This commit updates the cli tool to use the urfave library. This makes things easier to read and will make it easier to extend the tool in future. A `genKeys` function is also added as a helper for quickly generating key pairs. --- cmd/example-data/hop-data.json | 26 +++ cmd/example-data/onion.json | 5 + cmd/example-data/onion2.json | 5 + cmd/example-data/onion3.json | 5 + cmd/example-data/onion4.json | 5 + cmd/example-data/onion5.json | 5 + cmd/main.go | 326 +++++++++++++++++++++++---------- go.mod | 4 + go.sum | 10 + 9 files changed, 290 insertions(+), 101 deletions(-) create mode 100644 cmd/example-data/hop-data.json create mode 100644 cmd/example-data/onion.json create mode 100644 cmd/example-data/onion2.json create mode 100644 cmd/example-data/onion3.json create mode 100644 cmd/example-data/onion4.json create mode 100644 cmd/example-data/onion5.json diff --git a/cmd/example-data/hop-data.json b/cmd/example-data/hop-data.json new file mode 100644 index 0000000..72c03cf --- /dev/null +++ b/cmd/example-data/hop-data.json @@ -0,0 +1,26 @@ +{ + "session_key": "4141414141414141414141414141414141414141414141414141414141414141", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "hops": [ + { + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "1202023a98040205dc06080000000000000001" + }, + { + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" + }, + { + "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "payload": "12020230d4040204e206080000000000000003" + }, + { + "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "payload": "1202022710040203e806080000000000000004" + }, + { + "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + } + ] +} diff --git a/cmd/example-data/onion.json b/cmd/example-data/onion.json new file mode 100644 index 0000000..e586c7a --- /dev/null +++ b/cmd/example-data/onion.json @@ -0,0 +1,5 @@ +{ + "session_key": "4141414141414141414141414141414141414141414141414141414141414141", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f6e34152f83f6bc03769e265421e9615471ab9e10bace8e7b7eb4f846df42abb9a31b9d8a0b0b009cbf93fd14da6a1a6cb448b57cf9783025943e97407e52d8783f7b8854ad661941ceb446b5fe5814c7353354282f4cc3c47a3c8a9b75e4f436fbd25df99e608c4e53f76d752f4bd077dc62660f59edcdb55949856ea1ae07ccece645e2d2f995d95cecc6bf796d1208573ae0985bba253883e9cfdc62be6865829435c732e9c2d793c8c036d23248a4aa15024ca3bb960425a83b91ccd60c6e68d3c6a5bdd96832f8757fb39b26a6c3065a2f7e98b3f594ab5afd95286dd54658df649c500a3db88456999ecb3a8c8746082b191ced3571b88a1a16972bfc25fa2dbde775bb8e6299356e51a862eedd28c574e202668f134337374908c24f145ca0112deda44d020bdfc6e6d0ca5ee863f5ae79c849b0e34801fc785a3348e658abe57fd07a87525b5061f2afc5f0726754d2f97243fe36aef1e0c9fbcd41157f482d9c3348fae5a055ce2218f50f1a7e804451cd5ed55b36e3f72306b4b62fa1b31682ccb3c90ea24a33465da7c414fa022ce9c83b4e47c112f7dcf377282b148a02298d08d0ab5a560ba883452acfbd120be623e169dd7cc7d4dd1441074b21d33d4522c17c381f26084d811c612a337a47d44c9023867869f09da5b4ffd29c1674e8689e542f45cc864710a97073cd7b18039515425e66109b38a3a9ca6a14bab3e6e8c1dc5d28324f4920745d0a162644193f87011eb0edf5cb977508c943043d93f7dedf81a366ef6467df908decf4e55236171d17c6fe6995a982fefaa13a43be1ba5a9399be638b01864685b94e1e5e340e9444c11dbdf47681839d7134a9e498b5f53cc062682661d47ddde6a86e76eb0db9fdf1142f0bf4c30e512360967a3f3fd4c770b00d15236e52cb807769c770f3246be54563e57b3a243315d39aafff336c6883c23e2caa8405aec3129187f9f810f76e9bcabd5e93af911a88396f77d41b0b3ae2b3ce0884a0a34434577a48cf332bf844eac35493e33e423780a3c70376fc2c9ac8c403c568424e7691f35c48d1f28f37dd7da0b7168534ad819e14f7a09292b077e69e2edae9b73f40b8ad2ac116665465ec142c79594ec5169a890b3de310b26d38e77268a067c7c621b428e986b51a05aa9f2e8a8d88cfc3fd097e96f4aeb08b9de518e42f543c85a8827e55f59efddcd0b0a024fa44ed0617cc582e33555c5e503f2853754f6bc66339d007d8d9292387275e44d04af368baf56eb88ab9dae9ba9b79400aed0457ab277411a534e330ac8c5822f9c7fb50c05c34c43c4e7e38138bad20cb2142ae067b344d243c84fb4f37ce68111292dc40a2f5e99090e8929cf2cb5796d3c67c2b9dc49517a9d47617ed2962bb8aba03e022ef4fb68932ebeefc7341089294caa6e03a64e552897f8e7a60fc7b841a31a7d120fa64109f5f72cf20f96086cea8bdfc5646adecb547b85ca3c414860f674143fa1608e84393e99214f970ba2208ddd32ca8ee0c5d64d8de5ace4e9e47e9f6fb782eb7b6512c9a2740dcb086e82f4068d2677e866b9bc5e744d14e480268b10cdfb8c784c5ff1bdf9f3dd453e76b98694f0910bf81d57752421354823efc5caa5066ea9e63d4834149b1076f0eee0283a5539ec78421730190535e333ea062e0abf244ccd9ae1ce9c0dd856b37d6e3224f6fe4fe01bbd8d568605827bb6a4148b4554593cf173899714495ec3d19e939bd69fa5a259edca0aff4a34e2381d74a62e6277ee869a9f17131be3c7a24e684f129ad7fc746b7f4cbda16d7e36915c8a69177dc8e33390caf159c1f06aa75390e72db37d6cd0d88214e7e929e8ac5f7362be1eb1f2ce6139fd104eccef828" +} \ No newline at end of file diff --git a/cmd/example-data/onion2.json b/cmd/example-data/onion2.json new file mode 100644 index 0000000..6c725cd --- /dev/null +++ b/cmd/example-data/onion2.json @@ -0,0 +1,5 @@ +{ + "session_key": "4242424242424242424242424242424242424242424242424242424242424242", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "00028f9438bfbf7feac2e108d677e3a82da596be706cc1cf342b75c7b7e22bf4e6e25017528605ca89bc5823fc2ff825f3a9ae4f431d927817917965078c79bb82b59ab4974eea9bc47204d2caa345bd67a12d5bb56c19eb5d93227e23fa995ad13342c067e3ce6cd55d11ef65ec2bde846a7a0598b99c0dc552157271b069bb323e9d9b0d076850d6288f76a8de29aac690d66fd97f994472dc456cf8a68f6120ef6821d3ed092dd1e4c704c93a4f3fcef0f78a156da42a80f2c1ae19935030ce0507e905a93900079ecb2a1c9e233a92be8e97a557f063a59c95cae29c791ad7550f62f5f1888be1b247e6d55a6de26b136c2896d6758314108a429c82c683968493c807933d6b93e30f09c92e1da6401e3af4d774f775ff84b9536192720637cfdab6aa434c1e6c08baf075a8a3b55c13fb9a36ff59fa9ef8cfcca35fa811c3db716509aaeb5a2979dd6933b5fd5fefec35ebf294c779fbf700bac91fa52cdfaf511265354dd0225b15f72fd761280297f620866fa45f92f61fededb7f024a89643ffdd5f57097daf6202091ba8b77ac7e862f0cdbc5ec03daf0968e11237860004e572e26164ace8d2f7e4f50ce6a5a008934b072749c07358c4016738a994476ea215d3623079692e9f3b68b470e4af4c6985a397ceef8a7ac09490b6f43c440c7f83413c1c468d5f09e3cba23d74c94899a32b382dcb8d3994a16c1e7aa9c5b7135b7b3cfde277f68a0e2c1742a53422e5acaac5218eba290ead01561893c2bd6375f5dd0c24e84f820ab6ccaddae5159b3d2b6372c5b865117f3f003b6ceab16b189a8183e0be79e4bc872825bdf2ac56278ac7f971121c221245c6dd31a3fc04945393ef0880ff6cc22bf5e261e8240591997cd3648da417b0a4842fdadc8f24c08e5af67aa19335343830805919e9d4378900eae26fa5e21d7b9fc54111d2410625004f7baa9afc0d0f1099156db4b43bb87074c5e3beac5a97d4b6d9b84f565186792a18cdf42f44b1f080770d1a4f311f597c8a50d29497414641a1c031a2b561cf347f0a907b0af8d8fdc843ae762880af61e64cef17d7aa13a0f72a0ef0b19489f2a90167dc7795a4dd02c5358fafb05945d86ad638d04d2b8ed8189ec5d299b6f17cedd307c1cf6a2462af84dcf2a404e9a7279bbacab908685a587f118a232bd646dfefd60addbfe36a2b1f7e8ff949052d8ea4c0d236d01bee53962eb718ede8a94ff29cb5e58397d7d32b3e6e63220461539858fa8ed0930463df4277b569c6ca579649c8db11d5a631a0000e722ebd657afc973086bb06dc8a73f6464728b6884e950a68b60c86f88184dd260fda8f0b962c417595632bc547fd46417092a77fdabfedd190882f1e52d48323c4345e9542d723f930a253967923ec8d27e71bbbd0af9aa31544c0b4d6da5eef06be3c3c9049457a371712fff06626f4480e0cd082e933104f12dd23bd01ac8464b21623f542b85b6f5fdbc1daba3318c8dc9803506e666dbe13f7424f0279273a687fe89a1228608c249c5abfee75ac742db27ed68b19312a99651ce947fa98e3945e4f3bdb449c2eec79e9efb746731a0f5d5a1974bec7f60d872e207ef2905ba044ba5118ea2f1b116e1dc1aff9975a3eeff8f2ec5b0d72459bb7af222e586869a87fff3da331520a7d34c4fd58c090bb31e00f1309af259ddee14b94473e4517785d673d79a6e71b3e6ee3fc887ace2ab48c19744b0a3b302fa6613a23c0b18bc35a8dad53a6cbe62f23cf1773414331bd6e99beab684b6ccb839dbf0397e85d106e243d760197a364d586989115ade30d309374fea8435815418038534d12e4ffe88b91406a71d89d5a083e3b8224d86b2be11be32169afb04b9ea997854bb72493084f22105fea7b5181eb70fce6ff3d92bffee12f7f19ba453f6bdfc1ca" +} diff --git a/cmd/example-data/onion3.json b/cmd/example-data/onion3.json new file mode 100644 index 0000000..6e5c89c --- /dev/null +++ b/cmd/example-data/onion3.json @@ -0,0 +1,5 @@ +{ + "session_key": "4343434343434343434343434343434343434343434343434343434343434343", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "0003bfd8225241ea71cd0843db7709f4c222f62ff2d4516fd38b39914ab6b83e0da0f13ca6418876711541d1adcd84fae578e80ed4c2aba02acbf1fb3716fd7345e78c0affb24613c2ad822bc7c0e1ea8c9aecfe8305ab1400421c900edf4b4d461df181853cf77799945528b4f40ae3f98ec572ffdffd5d8962bf4d4d8c5780529991c9de9b33176841e7613250616146452e0158206106731b6d1f98cc91bfbd109db6dccf8fb141ee350a99ba0889f011eab62b4b65fa65e2f44d0d5f9adb851e633e3d3654fff80067e594efb67123b5f7343244aff9d457efbad718af0ed16a8788870e3e7b8fde17ef27719e1ddd41e4335c5dcbd14615753b165ac992b9fac7df34ed13bcdbff085d9ec2edec3149307be22ce1b6da70303906139e39840d837ab6d8dd335eb66523637bdeb2b446170cb02994f96d3639b2bc66f1eb46f6c614b70ce6e7f539fb1fe2278035e409fe9f8797bb6fc3604b58c835725bd4e48689e224ff784c6678e54fba9701609b7d3157ea05a7dfb6092b97c362ddca2101f3c1c953a5ef92037d268c0c94ef5f5c082748fd068f2a5b8b8a4c130687591577ddf3d864665a5aded674c33a370e1dda6be247b1ad2885f06c10dc9c1784c869445a7cd0ce5aee125bde1958d11ed3aaa10fdd00d490a58441ed7cc0bfd79d82075929f3b9b93f1f9f61b472c4864787c770a908332b494dac06496d0589d13f61e57b291eb52d70b0f0d431714f842f41b291e469a57989329ea9ac50ae903f51395a51ef554cf4c2cda103f01ca0cde08ed2ad721a25e303bc33d66ebab2d43710378a7e9c5dc211786983f3fc845806f4beaee5833a6b0182396c740285037cce48be4b9552280c3dce3e6f0770f0f2139567441c6d311523632b11348a22cb8c3622acbfb0febc38bebc0d2d260998c13dacb9c90ae1740515f1b21cb6ed53dc0c0aa239f4fc6fde90fc89ec7ab7a113d8ceddfcd3bfe6406a9998b66ce144530b39165e3f05106b85ad99cfcf36fc55abf48cbf388d3331c9961ea20484b72d8d2f831cfb9dbea5c63512c3abbca4148fa238304f698142983777e8cb7806380e37c2c71888bf7bbd8bf1ada837acf18388db21666abfbb6e386dbbcd454a09f0c3ab94ca1b48d9d3b0d7c394f1ce89bb048f9ec3a62c9534262fa989b68719f6b4653c4609614d606030b39d4aca551078b302f1dc0b4fabb1bc9d11f8b3950044cd64befb42075f84337288859bb2e575c8ee0285daf7b2fdb35da2e764e7ea05d30b63e534050fea7111f4712f82e62e9e811c43f1ca0a232e86edc874a0498320bebf60c5dc413f5a7d2a54cd59f0383ca3123ebe51af4e507b8588c23b276813e893ec65aeb795c8b9dc4185acb2cb91fdf39955e55fdeb1865f4192f8fba589fe2cef5c934ab80716d0d3e7e1869d06735a457508b88c795aae03f1b38b4aa3ff36cd20284318048d5fb2ec47ec999a2512ec6dbdfce24276c12ee6bc41eb8e0f53c193a02b1882fe5b4ce61dba6727180bd9ce65a718b8d40471344e8e2a90008d2ebcb65c8cbd6b30c7f8ee57c27460113b29b5c0c1e0bc12a26d2251998ee146235d361579124164b7566d9218dc116f50fab939715074bba73a8cb953e171c187c70022364e140c704a78f93085577f27bf41f9e02f55c155ec186133cfa8b1dd40cd4c773e735a8f15291fccc8c4f55bdcc8ba563004597b5b3de5d54be23671bc9477805ba10d03afb0715782845d2ab45df012f6644207cc5fa4739aa3eaf6bf84e790128aa08aede33bf30c6be2b264b33fac566209e73715a254047517e6aa05e31afb8bd076a3e56fb1c5bbc5dd44f6f095d7f927939ab97b6899cb7bd8bbc5610db5e2556a698c01ffaeda20c8daf398a339af8e2ba28d4b14eb916b64a91a09113431f38193d0" +} diff --git a/cmd/example-data/onion4.json b/cmd/example-data/onion4.json new file mode 100644 index 0000000..b71d833 --- /dev/null +++ b/cmd/example-data/onion4.json @@ -0,0 +1,5 @@ +{ + "session_key": "4444444444444444444444444444444444444444444444444444444444444444", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "00031dde6926381289671300239ea8e57ffaf9bebd05b9a5b95beaf07af05cd4359526cc8aa7d0f6392e039a9f2846827fc997c3312fa06cac98e5199c529e328427e7bca6533bfecbbc53af0e076057c647a07d93a056ea323538d4feafbc057a4f654e7732292561b85f349c7ed03c891b888a78050869ae4008c4f0eec16e1d902424c2b3a0a3f7d37684cd5e0c41b841b3449e32af7e3412cddb772701dd91feb687edb7bc26d8b530b32b93465b906001ae610384d3c2a39626e5905340ec5706a7b1f0c8f3cf4221a44649e90d64a9f7cd7bde3fe269782e22a58ee3ab3cc3aae66387003a54ac70a464fffb474b17f591e7d6ca03d8fc6319e2ac03977e6320477ba64272cce10acded883e472d5cb9c151e9d25a6dab669d6b228a96d28dd8a1b745f87f8d4e21851fa26da8c9dd7354a73160e014249ba64bce9a2fa55b67bfd86a05187a15b3989a114aac102c6cf1fa1306b06dcb7f4d3e1c292f09eb52ae2c6295307e43bc373d70a32c1aad7731d624e2a7160e3fdb6e3baa516aa5ce1cbc0227fca581242b0bd6ac2f37213d3d8b68cdcd37fb6a53151467a736f98cebe6dddf2a97a9b6f04155a1f41464d42dec10c9bfbed84c6fbf7859af049f17dcb69cc4e642e243c3fdb9359bf70d0390688cce1d297b584cbd7fc8439ed4366bf1f7a64274a1f0917e0c36f1b3600a87fa4c450bb4fa9d884d0c5bfea6a51b7e1d25157330d72e171a4b7b994dc7c391ae79e63507d86ab07215c9264fab83303c8d0084ab00bc1b8e00a9608eb58278f55b765c5bcb4e4baab79283560859fd63538c80f8b58ccbd2f30623c29b4bc177aab641150496c2ba56bc8d4d6d9630d74692712c9f6a9e4efb3091cff9b7fb0bd285d558e55781183a19c2faa47c32674638750dfab3cdd5998514f8d57c46bd341f1d0ad2b6f08ac93ae1e6acd71c49509a5ced3427adfc4a6b0216bc83171c071a45b2a612ed296d312a7701aa0521319000748d3c1cbd6b3ead745875e08c04b95e7bf3b3cef6008decb530ebef1cb5e8f2e54c8b77b419123b23582899823ddb6c16736be4259c933eeb50d0f456e6c60564fd75490b1485faa2781ffa6fb6aa74599ac3a86ce24ec93d0e281ded02cb851f7730424911a5e59b72c35488e181a874af550915c69219c08386942b1d210bc5ecd7c0f4e9750846e880d36c06e0abccd4431b6aa2da300adcc5413ae323a19d8c3ec390ecc4f382b10c9b2c1d75e580fcae94160ba2fd2ca7fcf7a2ff5d02235319ed5835c28994654e37329d5e9fdbd1d47062e75ef089e6f4a6e822e1badbe620e83346545be79a0ed25be303c60e966c9f630b7ad3b85aa7be800e16edc6a8f9d012969bfad73523f4f3149a3ac8db49931d631dcee7c29d6f43d9882371c79a1eb608c570bc7d49126a0c456951f583cb06fdd45976e6f1ca4bac239202f7b52acbaf62f26ee24778039c855cdb9b0e7649072a142f14cf14c3cec4ee0f76a5bc16bbc50e6bcf726ca01904b0f4e7154ef06791260be1127259d4eae5044665d07329e18a6308f8028c0d3dea773b0b905ba29ae6f2f683cce6040a4391c0fb03731ce7912edaeac97df5b44bce0a1d82cb3c9a21d48869f10bb11572b7ce1e051d164900a2b2b3bb6c36626f410b9e5b65e57a206983c779b77ce6ab15fa0f9078b4ac9ee578731018fc981341d5a95103767c7cf24b93cdbe1f8644622fe4f5472522b2d63760f504954c63e496d353acd777f9aea905b65b9bb419a67cc967b0fac6872389b7f737f9038fabb526cbbdee38238d51953f372fdd9ebc260b0ee2e391420c4b11145bd439954834d9a79e78abc57e03d3ee20d239d8a13014976e3f057ab3c38ca79ee82ebd2f9fd2aa46f2a36d6fffe4715c2bdfcc1958136742946bc5993d5b51e947" +} diff --git a/cmd/example-data/onion5.json b/cmd/example-data/onion5.json new file mode 100644 index 0000000..50221c2 --- /dev/null +++ b/cmd/example-data/onion5.json @@ -0,0 +1,5 @@ +{ + "session_key": "4545454545454545454545454545454545454545454545454545454545454545", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "0003a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f40bacffa7597e025eea9286b58117e0cd9250c5800ba52f9342e43477eac9c03bed26ea0d0e31f62c1ee71220d71ed638f95b2c2fe332371c59abd7e934d423437744e6237aa9eb7d373ac72d44c33e5e7e53f5c7e09264d712a3cc1d757e6f48c0604f08297d12839030b09e0a69137cb717260b590edbffd98b2dc31a87cf21bb1d5dfb3e2abcc89846ce5eac9c404d6ada0c84bb68bb3fcb585aef8f689ee206ce12587a4a0ead0cbf445c13f04e8f76c62a9c3d3312185bca81bdf0d46e8733645912c0ae31d8707a772a6bc179f62724461553acdc4e54f4b90d6c583889e07d0ec6c27a11d090122a1d075e5094063d6696078a37f34dd621847cf8965baecbfff2094c31a5da449e4cfea9fcd0438ed29fb8c1c3006b7c047055b12407925ea74c2bb09ca21f4bc9d39dc4a7237ef6facbb15f2c10ccfeefc52524019ad0c33bf62729e98130384c2f5ec4e68fae784f71674ba9fd2a89c328695e582d3bc850dc0e1fe86ddb88138a56df283dc380f5114e3a3970f71eac063e5447ec37777691e56e0f79e372eeed6bfb7884cf9c5dd6495c2d7fe56bd1c6d112a69b078e0f82b9096ec322a7951318a23c2b06e78c3c0bd4aa76f8da1f7b285159d3f89bc47993d9a4d9759d7db1f2b1ca9b3629da41d647d7a4451801e66fd44670d4881fecce3bc6e9c09d4f7a4c5c6ffbb248c3e29cb9a5d7a75490f2f5ed28a7241139588b64bb692f92241e312de035d21f3a52269693cd985aecf6e80eb80779ac73bb3814809b4434291e423c2caeb91b3f0f744425a7f2640c7cd481b9808e6251f963020ae3403c0c719aa8a06207b3d92e65ea39f9b1a6637b9f71a4ec8d9b38cf42a6bcb02ee771fbba765e852d21398b1612328a54c50f165efebc5d258edbc02e1deaaaffb201c61bda6a85eb802aaec66d71a8121c7520127680d11a6a3017722af48b0bf796fa39c37bfde2217f896f764eded018b096671f63f066dbc7018d23966be95b244b5eaf81cbea0edbbb75bfc31c12a63c5262b8b1468ae4282112f8531bb8dfa45ac5b62dd01039737e808074ebdf0a49c73a30462bba152c778ae63c90413333e508225c1890983798b1bbd2fe75da1236e8752db4ad8abba0161255fbc47dcc07e1b213f8f78a75b8d5ba7041e294a30f6cf558f73ac6ec48046a087946dbc2612116d1fe2deaeacc24eda2277d5bf92892468590f5d2e467cff584aad2efaa601f30e1958afc1d84efca83e2bc55ec6bbe3b7f0981c5f46b3bbc39f71754dcc965b1b3e6972f7aa18914cb27675af3137b8421cded725a2cb1e73ec6a6a149ab6647babeae10f26d34fce7ef961c2790124cdb2d9f23aa3caaf3d52cac0b1bfc1b9c8ae7df401bd051ad89b5a0ae7352dd9e8f029ff080d856d07b44a5b430875f89849bac2aeea155573f59f09be57de93f1169964647d58b21c1d925d0ee2277cd11e129090a019a4a193563c2051ed1ca1b29bfa72612293713110939392056abcd296e54dc6dfcfedaea1c4d7358293e9dc6cf381cc8dea6cf90705700ce7a0555ef2142b061c02ead628fc8f0ddc2409ebda4fbb275d4c44fc8644a4431628f4afcb2afdc8de0e0e1ecfe7fc81d65ee4a1554587be83b82f391e61be12c18ab7b695d3f2284291b56bae638d098411cdfa27915f128d39727e5a8f315fea61937efb1a5f41f2a5b74e0ce78c401e5f55fa3451ed4f60469a135a3dea7dad750f39082b3d9a7477d74963aaefcf22c9c02d50e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065cf435b4dc1968a05e5471b20147684e96c4de4894a0543647a046d8c92abebd08" +} diff --git a/cmd/main.go b/cmd/main.go index 5266555..e3ac142 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,160 +5,284 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" "log" "os" - "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/urfave/cli" ) -type OnionHopSpec struct { - Realm int `json:"realm"` - PublicKey string `json:"pubkey"` - Payload string `json:"payload"` -} +const ( + defaultHopDataPath = "cmd/example-data/hop-data.json" + defaultOnionPath = "cmd/example-data/onion.json" +) -type OnionSpec struct { - SessionKey string `json:"session_key,omitempty"` - Hops []OnionHopSpec `json:"hops"` -} +// main implements a simple command line utility that can be used in order to +// either generate a fresh mix-header or decode and fully process an existing +// one given a private key. +func main() { + app := cli.NewApp() + app.Name = "sphinx-cli" + app.Commands = []cli.Command{ + { + Name: "genkeys", + Usage: "A helper function to generate a random new " + + "private-public key pair.", + Action: genKeys, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "priv", + Usage: "An optional flag to provide " + + "a private key. In this " + + "case, this command just " + + "calculates and prints the " + + "associated public key", + }, + }, + }, + { + Name: "generate", + Usage: "Build a new onion.", + Action: generate, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "Path to json file containing " + + "the session key and hops " + + "data.", + Value: defaultHopDataPath, + }, + }, + }, + { + Name: "peel", + Usage: "Peel the onion.", + Action: peel, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "Path to json file containing " + + "the onion to decode along " + + "with the session key and " + + "associated data.", + Value: defaultOnionPath, + }, + }, + }, + } -func parseOnionSpec(spec OnionSpec) (*sphinx.PaymentPath, *btcec.PrivateKey, error) { - var path sphinx.PaymentPath - var binSessionKey []byte - var err error + if err := app.Run(os.Args); err != nil { + log.Fatalln(err) + } +} - if spec.SessionKey != "" { - binSessionKey, err = hex.DecodeString(spec.SessionKey) +func genKeys(cli *cli.Context) error { + var ( + priv *btcec.PrivateKey + pub *btcec.PublicKey + err error + ) + if privKeyStr := cli.String("priv"); privKeyStr != "" { + privBytes, err := hex.DecodeString(privKeyStr) if err != nil { - log.Fatalf("Unable to decode the sessionKey %v: %v\n", spec.SessionKey, err) + return err } + priv, pub = btcec.PrivKeyFromBytes(privBytes) - if len(binSessionKey) != 32 { - log.Fatalf("Session key must be a 32 byte hex string: %v\n", spec.SessionKey) - } } else { - binSessionKey = bytes.Repeat([]byte{'A'}, 32) + priv, err = btcec.NewPrivateKey() + if err != nil { + return err + } + + pub = priv.PubKey() + } + + fmt.Printf("Private Key: %x\nPublic Key: %x\n", priv.Serialize(), + pub.SerializeCompressed()) + + return nil +} + +type pathData struct { + SessionKey string `json:"session_key"` + AssociatedData string `json:"associated_data"` + Hops []hopData `json:"hops"` +} + +type hopData struct { + PublicKey string `json:"pubkey"` + Payload string `json:"payload"` +} + +func parsePathData(data pathData) (*sphinx.PaymentPath, *btcec.PrivateKey, + []byte, error) { + + sessionKeyBytes, err := hex.DecodeString(data.SessionKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "sessionKey %v: %v", data.SessionKey, err) } - sessionKey, _ := btcec.PrivKeyFromBytes(binSessionKey) + if len(sessionKeyBytes) != 32 { + return nil, nil, nil, fmt.Errorf("session priv key must be " + + "32 bytes long") + } - for i, hop := range spec.Hops { + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + + assocData, err := hex.DecodeString(data.AssociatedData) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "associate data %v: %v", data.AssociatedData, err) + } + + var path sphinx.PaymentPath + for i, hop := range data.Hops { binKey, err := hex.DecodeString(hop.PublicKey) - if err != nil || len(binKey) != 33 { - log.Fatalf("%s is not a valid hex pubkey %s", hop.PublicKey, err) + if err != nil { + return nil, nil, nil, err } pubkey, err := btcec.ParsePubKey(binKey) if err != nil { - log.Fatalf("%s is not a valid hex pubkey %s", hop.PublicKey, err) + return nil, nil, nil, err } path[i].NodePub = *pubkey payload, err := hex.DecodeString(hop.Payload) if err != nil { - log.Fatalf("%s is not a valid hex payload %s", - hop.Payload, err) + return nil, nil, nil, err } hopPayload, err := sphinx.NewHopPayload(nil, payload) if err != nil { - log.Fatalf("unable to make payload: %v", err) + return nil, nil, nil, err } path[i].HopPayload = hopPayload - - fmt.Fprintf(os.Stderr, "Node %d pubkey %x\n", i, pubkey.SerializeCompressed()) } - return &path, sessionKey, nil + + return &path, sessionKey, assocData, nil } -// main implements a simple command line utility that can be used in order to -// either generate a fresh mix-header or decode and fully process an existing -// one given a private key. -func main() { - args := os.Args +func generate(ctx *cli.Context) error { + file := ctx.String("file") + jsonSpec, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("unable to read JSON onion spec from file "+ + "%v: %v", file, err) + } - assocData := bytes.Repeat([]byte{'B'}, 32) + var spec pathData + if err := json.Unmarshal(jsonSpec, &spec); err != nil { + return fmt.Errorf("unable to peel JSON onion spec: %v", err) + } - if len(args) < 3 { - fmt.Printf("Usage: %s (generate|decode) \n", args[0]) - return - } else if args[1] == "generate" { - var spec OnionSpec + path, sessionKey, assocData, err := parsePathData(spec) + if err != nil { + return fmt.Errorf("could not peel onion spec: %v", err) + } - jsonSpec, err := ioutil.ReadFile(args[2]) - if err != nil { - log.Fatalf("Unable to read JSON onion spec from file %v: %v", args[2], err) - } + msg, err := sphinx.NewOnionPacket( + path, sessionKey, assocData, sphinx.DeterministicPacketFiller, + ) + if err != nil { + return fmt.Errorf("error creating message: %v", err) + } - if err := json.Unmarshal(jsonSpec, &spec); err != nil { - log.Fatalf("Unable to parse JSON onion spec: %v", err) - } + w := bytes.NewBuffer([]byte{}) + err = msg.Encode(w) + if err != nil { + return fmt.Errorf("error serializing message: %v", err) + } - path, sessionKey, err := parseOnionSpec(spec) - if err != nil { - log.Fatalf("could not parse onion spec: %v", err) - } + fmt.Printf("%x\n", w.Bytes()) + return nil +} - msg, err := sphinx.NewOnionPacket( - path, sessionKey, assocData, - sphinx.DeterministicPacketFiller, - ) - if err != nil { - log.Fatalf("Error creating message: %v", err) - } +type onionInfo struct { + SessionKey string `json:"session_key"` + AssociatedData string `json:"associated_data"` + Onion string `json:"onion"` +} - w := bytes.NewBuffer([]byte{}) - err = msg.Encode(w) - if err != nil { - log.Fatalf("Error serializing message: %v", err) - } +func parseOnionInfo(info *onionInfo) (*sphinx.OnionPacket, *btcec.PrivateKey, + []byte, error) { - fmt.Printf("%x\n", w.Bytes()) - } else if args[1] == "decode" { - binKey, err := hex.DecodeString(args[2]) - if len(binKey) != 32 || err != nil { - log.Fatalf("Argument not a valid hex private key") - } + sessionKeyBytes, err := hex.DecodeString(info.SessionKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "sessionKey %v: %v", info.SessionKey, err) + } - hexBytes, _ := ioutil.ReadAll(os.Stdin) - binMsg, err := hex.DecodeString(strings.TrimSpace(string(hexBytes))) - if err != nil { - log.Fatalf("Error decoding message: %s", err) - } + if len(sessionKeyBytes) != 32 { + return nil, nil, nil, fmt.Errorf("session priv key must be " + + "32 bytes long") + } - privkey, _ := btcec.PrivKeyFromBytes(binKey) - privKeyECDH := &sphinx.PrivKeyECDH{PrivKey: privkey} - replayLog := sphinx.NewMemoryReplayLog() - s := sphinx.NewRouter( - privKeyECDH, &chaincfg.TestNet3Params, replayLog, - ) + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) - replayLog.Start() - defer replayLog.Stop() + assocData, err := hex.DecodeString(info.AssociatedData) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "associate data %v: %v", info.AssociatedData, err) + } - var packet sphinx.OnionPacket - err = packet.Decode(bytes.NewBuffer(binMsg)) + onion, err := hex.DecodeString(info.Onion) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "onion %v: %v", info.Onion, err) + } - if err != nil { - log.Fatalf("Error parsing message: %v", err) - } - p, err := s.ProcessOnionPacket(&packet, assocData, 10) - if err != nil { - log.Fatalf("Failed to decode message: %s", err) - } + var packet sphinx.OnionPacket + err = packet.Decode(bytes.NewBuffer(onion)) + if err != nil { + return nil, nil, nil, err + } - w := bytes.NewBuffer([]byte{}) - err = p.NextPacket.Encode(w) + return &packet, sessionKey, assocData, nil +} - if err != nil { - log.Fatalf("Error serializing message: %v", err) - } - fmt.Printf("%x\n", w.Bytes()) +func peel(ctx *cli.Context) error { + file := ctx.String("file") + jsonSpec, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("unable to read JSON onion spec from file "+ + "%v: %v", file, err) + } + + var info onionInfo + if err := json.Unmarshal(jsonSpec, &info); err != nil { + return err + } + + packet, sessionKey, assocData, err := parseOnionInfo(&info) + if err != nil { + return err + } + + s := sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: sessionKey}, + &chaincfg.TestNet3Params, sphinx.NewMemoryReplayLog(), + ) + s.Start() + defer s.Stop() + + p, err := s.ProcessOnionPacket(packet, assocData, 10) + if err != nil { + return err + } + + w := bytes.NewBuffer([]byte{}) + if err = p.NextPacket.Encode(w); err != nil { + return fmt.Errorf("error serializing message: %v", err) } + + fmt.Printf("%x\n", w.Bytes()) + + return nil } diff --git a/go.mod b/go.mod index dc4c472..db36ece 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,15 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/stretchr/testify v1.8.2 + github.com/urfave/cli v1.22.5 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 ) require ( + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a396b02..e8e4f31 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -20,6 +21,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -56,6 +59,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -63,6 +70,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= @@ -100,6 +109,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 6afc43f3fc983ae37685812de027d0747e136b8f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 3 Aug 2023 21:13:03 +0200 Subject: [PATCH 5/9] multi: refactor and move HopPayload methods This commit is a pure refactor and move commit. It moves the HopPayload methods out from the `path.go` file and into a dedicated `payload.go` file in order to prepare for a follow up commit that will add a blinded path struct to the `path.go` file. The opportunity is also taken to separate out the tlv vs legacy encoding of the hop payload a bit (seprate constructors for example) in order to prepare for a future where the legacy encoding will be removed. --- bench_test.go | 2 +- cmd/main.go | 2 +- path.go | 300 ------------------------------------------ payload.go | 347 +++++++++++++++++++++++++++++++++++++++++++++++++ sphinx_test.go | 16 +-- 5 files changed, 357 insertions(+), 310 deletions(-) create mode 100644 payload.go diff --git a/bench_test.go b/bench_test.go index ef9e8ba..c288645 100644 --- a/bench_test.go +++ b/bench_test.go @@ -33,7 +33,7 @@ func BenchmarkPathPacketConstruction(b *testing.B) { } copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) - hopPayload, err := NewHopPayload(&hopData, nil) + hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { b.Fatalf("unable to create new hop payload: %v", err) } diff --git a/cmd/main.go b/cmd/main.go index e3ac142..b78ec30 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -158,7 +158,7 @@ func parsePathData(data pathData) (*sphinx.PaymentPath, *btcec.PrivateKey, return nil, nil, nil, err } - hopPayload, err := sphinx.NewHopPayload(nil, payload) + hopPayload, err := sphinx.NewTLVHopPayload(payload) if err != nil { return nil, nil, nil, err } diff --git a/path.go b/path.go index f46ba01..736bbeb 100644 --- a/path.go +++ b/path.go @@ -1,309 +1,9 @@ package sphinx import ( - "bufio" - "bytes" - "encoding/binary" - "fmt" - "io" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/wire" -) - -// HopData is the information destined for individual hops. It is a fixed size -// 64 bytes, prefixed with a 1 byte realm that indicates how to interpret it. -// For now we simply assume it's the bitcoin realm (0x00) and hence the format -// is fixed. The last 32 bytes are always the HMAC to be passed to the next -// hop, or zero if this is the packet is not to be forwarded, since this is the -// last hop. -type HopData struct { - // Realm denotes the "real" of target chain of the next hop. For - // bitcoin, this value will be 0x00. - Realm [RealmByteSize]byte - - // NextAddress is the address of the next hop that this packet should - // be forward to. - NextAddress [AddressSize]byte - - // ForwardAmount is the HTLC amount that the next hop should forward. - // This value should take into account the fee require by this - // particular hop, and the cumulative fee for the entire route. - ForwardAmount uint64 - - // OutgoingCltv is the value of the outgoing absolute time-lock that - // should be included in the HTLC forwarded. - OutgoingCltv uint32 - - // ExtraBytes is the set of unused bytes within the onion payload. This - // extra set of bytes can be utilized by higher level applications to - // package additional data within the per-hop payload, or signal that a - // portion of the remaining set of hops are to be consumed as Extra - // Onion Blobs. - // - // TODO(roasbeef): rename to padding bytes? - ExtraBytes [NumPaddingBytes]byte -} - -// Encode writes the serialized version of the target HopData into the passed -// io.Writer. -func (hd *HopData) Encode(w io.Writer) error { - if _, err := w.Write(hd.Realm[:]); err != nil { - return err - } - - if _, err := w.Write(hd.NextAddress[:]); err != nil { - return err - } - - if err := binary.Write(w, binary.BigEndian, hd.ForwardAmount); err != nil { - return err - } - - if err := binary.Write(w, binary.BigEndian, hd.OutgoingCltv); err != nil { - return err - } - - if _, err := w.Write(hd.ExtraBytes[:]); err != nil { - return err - } - - return nil -} - -// Decodes populates the target HopData with the contents of a serialized -// HopData packed into the passed io.Reader. -func (hd *HopData) Decode(r io.Reader) error { - if _, err := io.ReadFull(r, hd.Realm[:]); err != nil { - return err - } - - if _, err := io.ReadFull(r, hd.NextAddress[:]); err != nil { - return err - } - - err := binary.Read(r, binary.BigEndian, &hd.ForwardAmount) - if err != nil { - return err - } - - err = binary.Read(r, binary.BigEndian, &hd.OutgoingCltv) - if err != nil { - return err - } - - _, err = io.ReadFull(r, hd.ExtraBytes[:]) - return err -} - -// PayloadType denotes the type of the payload included in the onion packet. -// Serialization of a raw HopPayload will depend on the payload type, as some -// include a varint length prefix, while others just encode the raw payload. -type PayloadType uint8 - -const ( - // PayloadLegacy is the legacy payload type. It includes a fixed 32 - // bytes, 12 of which are padding, and uses a "zero length" (the old - // realm) prefix. - PayloadLegacy PayloadType = iota - - // PayloadTLV is the new modern TLV based format. This payload includes - // a set of opaque bytes with a varint length prefix. The varint used - // is the same CompactInt as used in the Bitcoin protocol. - PayloadTLV ) -// HopPayload is a slice of bytes and associated payload-type that are destined -// for a specific hop in the PaymentPath. The payload itself is treated as an -// opaque data field by the onion router. The included Type field informs the -// serialization/deserialziation of the raw payload. -type HopPayload struct { - // Type is the type of the payload. - Type PayloadType - - // Payload is the raw bytes of the per-hop payload for this hop. - // Depending on the realm, this pay be the regular legacy hop data, or - // a set of opaque blobs to be parsed by higher layers. - Payload []byte - - // HMAC is an HMAC computed over the entire per-hop payload that also - // includes the higher-level (optional) associated data bytes. - HMAC [HMACSize]byte -} - -// NewHopPayload creates a new hop payload given an optional set of forwarding -// instructions for a hop, and a set of optional opaque extra onion bytes to -// drop off at the target hop. If both values are not specified, then an error -// is returned. -func NewHopPayload(hopData *HopData, eob []byte) (HopPayload, error) { - var ( - h HopPayload - b bytes.Buffer - ) - - // We can't proceed if neither the hop data or the EOB has been - // specified by the caller. - switch { - case hopData == nil && len(eob) == 0: - return h, fmt.Errorf("either hop data or eob must " + - "be specified") - - case hopData != nil && len(eob) > 0: - return h, fmt.Errorf("cannot provide both hop data AND an eob") - - } - - // If the hop data is specified, then we'll write that now, as it - // should proceed the EOB portion of the payload. - if hopData != nil { - if err := hopData.Encode(&b); err != nil { - return h, nil - } - - // We'll also mark that this particular hop will be using the - // legacy format as the modern format packs the existing hop - // data information into the EOB space as a TLV stream. - h.Type = PayloadLegacy - } else { - // Otherwise, we'll write out the raw EOB which contains a set - // of opaque bytes that the recipient can decode to make a - // forwarding decision. - if _, err := b.Write(eob); err != nil { - return h, nil - } - - h.Type = PayloadTLV - } - - h.Payload = b.Bytes() - - return h, nil -} - -// NumBytes returns the number of bytes it will take to serialize the full -// payload. Depending on the payload type, this may include some additional -// signalling bytes. -func (hp *HopPayload) NumBytes() int { - // The base size is the size of the raw payload, and the size of the - // HMAC. - size := len(hp.Payload) + HMACSize - - // If this is the new TLV format, then we'll also accumulate the number - // of bytes that it would take to encode the size of the payload. - if hp.Type == PayloadTLV { - payloadSize := len(hp.Payload) - size += int(wire.VarIntSerializeSize(uint64(payloadSize))) - } - - return size -} - -// Encode encodes the hop payload into the passed writer. -func (hp *HopPayload) Encode(w io.Writer) error { - switch hp.Type { - - // For the legacy payload, we don't need to add any additional bytes as - // our realm byte serves as our zero prefix byte. - case PayloadLegacy: - break - - // For the TLV payload, we'll first prepend the length of the payload - // as a var-int. - case PayloadTLV: - var b [8]byte - err := WriteVarInt(w, uint64(len(hp.Payload)), &b) - if err != nil { - return err - } - } - - // Finally, we'll write out the raw payload, then the HMAC in series. - if _, err := w.Write(hp.Payload); err != nil { - return err - } - if _, err := w.Write(hp.HMAC[:]); err != nil { - return err - } - - return nil -} - -// Decode unpacks an encoded HopPayload from the passed reader into the target -// HopPayload. -func (hp *HopPayload) Decode(r io.Reader) error { - bufReader := bufio.NewReader(r) - - // In order to properly parse the payload, we'll need to check the - // first byte. We'll use a bufio reader to peek at it without consuming - // it from the buffer. - peekByte, err := bufReader.Peek(1) - if err != nil { - return err - } - - var payloadSize uint32 - - switch int(peekByte[0]) { - // If the first byte is a zero (the realm), then this is the normal - // payload. - case 0x00: - // Our size is just the payload, without the HMAC. This means - // that this is the legacy payload type. - payloadSize = LegacyHopDataSize - HMACSize - hp.Type = PayloadLegacy - - default: - // Otherwise, this is the new TLV based payload type, so we'll - // extract the payload length encoded as a var-int. - var b [8]byte - varInt, err := ReadVarInt(bufReader, &b) - if err != nil { - return err - } - - payloadSize = uint32(varInt) - hp.Type = PayloadTLV - } - - // Now that we know the payload size, we'll create a new buffer to - // read it out in full. - // - // TODO(roasbeef): can avoid all these copies - hp.Payload = make([]byte, payloadSize) - if _, err := io.ReadFull(bufReader, hp.Payload[:]); err != nil { - return err - } - if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil { - return err - } - - return nil -} - -// HopData attempts to extract a set of forwarding instructions from the target -// HopPayload. If the realm isn't what we expect, then an error is returned. -// This method also returns the left over EOB that remain after the hop data -// has been parsed. Callers may want to map this blob into something more -// concrete. -func (hp *HopPayload) HopData() (*HopData, error) { - payloadReader := bytes.NewBuffer(hp.Payload) - - // If this isn't the "base" realm, then we can't extract the expected - // hop payload structure from the payload. - if hp.Type != PayloadLegacy { - return nil, nil - } - - // Now that we know the payload has the structure we expect, we'll - // decode the payload into the HopData. - var hd HopData - if err := hd.Decode(payloadReader); err != nil { - return nil, err - } - - return &hd, nil -} - // NumMaxHops is the maximum path length. There is a maximum of 1300 bytes in // the routing info block. Legacy hop payloads are always 65 bytes, while tlv // payloads are at least 47 bytes (tlvlen 1, amt 2, timelock 2, nextchan 10, diff --git a/payload.go b/payload.go new file mode 100644 index 0000000..9e89dad --- /dev/null +++ b/payload.go @@ -0,0 +1,347 @@ +package sphinx + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + + "github.com/btcsuite/btcd/wire" +) + +// PayloadType denotes the type of the payload included in the onion packet. +// Serialization of a raw HopPayload will depend on the payload type, as some +// include a varint length prefix, while others just encode the raw payload. +type PayloadType uint8 + +const ( + // PayloadLegacy is the legacy payload type. It includes a fixed 32 + // bytes, 12 of which are padding, and uses a "zero length" (the old + // realm) prefix. + PayloadLegacy PayloadType = iota + + // PayloadTLV is the new modern TLV based format. This payload includes + // a set of opaque bytes with a varint length prefix. The varint used + // is the same CompactInt as used in the Bitcoin protocol. + PayloadTLV +) + +// HopPayload is a slice of bytes and associated payload-type that are destined +// for a specific hop in the PaymentPath. The payload itself is treated as an +// opaque data field by the onion router. The included Type field informs the +// serialization/deserialziation of the raw payload. +type HopPayload struct { + // Type is the type of the payload. + Type PayloadType + + // Payload is the raw bytes of the per-hop payload for this hop. + // Depending on the realm, this pay be the regular legacy hop data, or + // a set of opaque blobs to be parsed by higher layers. + Payload []byte + + // HMAC is an HMAC computed over the entire per-hop payload that also + // includes the higher-level (optional) associated data bytes. + HMAC [HMACSize]byte +} + +// NewTLVHopPayload creates a new TLV encoded HopPayload. The payload will be +// a TLV encoded stream that will contain forwarding instructions for a hop. +func NewTLVHopPayload(payload []byte) (HopPayload, error) { + var ( + h HopPayload + b bytes.Buffer + ) + + // Write out the raw payload which contains a set of opaque bytes that + // the recipient can decode to make a forwarding decision. + if _, err := b.Write(payload); err != nil { + return h, nil + } + + h.Type = PayloadTLV + h.Payload = b.Bytes() + + return h, nil +} + +// NumBytes returns the number of bytes it will take to serialize the full +// payload. Depending on the payload type, this may include some additional +// signalling bytes. +func (hp *HopPayload) NumBytes() int { + if hp.Type == PayloadLegacy { + return legacyNumBytes() + } + + return tlvNumBytes(len(hp.Payload)) +} + +// Encode encodes the hop payload into the passed writer. +func (hp *HopPayload) Encode(w io.Writer) error { + if hp.Type == PayloadLegacy { + return encodeLegacyHopPayload(hp, w) + } + + return encodeTLVHopPayload(hp, w) +} + +// Decode unpacks an encoded HopPayload from the passed reader into the target +// HopPayload. +func (hp *HopPayload) Decode(r io.Reader) error { + bufReader := bufio.NewReader(r) + + // In order to properly parse the payload, we'll need to check the + // first byte. We'll use a bufio reader to peek at it without consuming + // it from the buffer. + peekByte, err := bufReader.Peek(1) + if err != nil { + return err + } + + var ( + legacyPayload = isLegacyPayloadByte(peekByte[0]) + payloadSize uint16 + ) + + if legacyPayload { + payloadSize = legacyPayloadSize() + hp.Type = PayloadLegacy + } else { + payloadSize, err = tlvPayloadSize(bufReader) + if err != nil { + return err + } + + hp.Type = PayloadTLV + } + + // Now that we know the payload size, we'll create a new buffer to + // read it out in full. + // + // TODO(roasbeef): can avoid all these copies + hp.Payload = make([]byte, payloadSize) + if _, err := io.ReadFull(bufReader, hp.Payload[:]); err != nil { + return err + } + if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil { + return err + } + + return nil +} + +// HopData attempts to extract a set of forwarding instructions from the target +// HopPayload. If the realm isn't what we expect, then an error is returned. +// This method also returns the left over EOB that remain after the hop data +// has been parsed. Callers may want to map this blob into something more +// concrete. +func (hp *HopPayload) HopData() (*HopData, error) { + // The HopData can only be extracted at this layer for payloads using + // the legacy encoding. + if hp.Type == PayloadLegacy { + return decodeLegacyHopData(hp.Payload) + } + + return nil, nil +} + +// tlvPayloadSize uses the passed reader to extract the payload length encoded +// as a var-int. +func tlvPayloadSize(r io.Reader) (uint16, error) { + var b [8]byte + varInt, err := ReadVarInt(r, &b) + if err != nil { + return 0, err + } + + if varInt > math.MaxUint16 { + return 0, fmt.Errorf("payload size of %d is larger than the "+ + "maximum allowed size of %d", varInt, math.MaxUint16) + } + + return uint16(varInt), nil +} + +// tlvNumBytes takes the length of the payload and returns the number of bytes +// that it would take to serialise such a payload. For the TLV type encoding, +// the payload length itself would be encoded as a var-int, this is then +// followed by the payload itself and finally an HMAC would be appended. +func tlvNumBytes(payloadLen int) int { + return wire.VarIntSerializeSize(uint64(payloadLen)) + payloadLen + + HMACSize +} + +// encodeTLVHopPayload takes a HopPayload and writes it to the given writer +// using the TLV encoding which requires the payload and HMAC to be pre-fixed +// with a var-int encoded length. +func encodeTLVHopPayload(hp *HopPayload, w io.Writer) error { + // First, the length of the payload is encoded as a var-int. + var b [8]byte + err := WriteVarInt(w, uint64(len(hp.Payload)), &b) + if err != nil { + return err + } + + // Then, the raw payload and he HMAC are written in series. + if _, err := w.Write(hp.Payload); err != nil { + return err + } + + _, err = w.Write(hp.HMAC[:]) + + return err +} + +// HopData is the information destined for individual hops. It is a fixed size +// 64 bytes, prefixed with a 1 byte realm that indicates how to interpret it. +// For now we simply assume it's the bitcoin realm (0x00) and hence the format +// is fixed. The last 32 bytes are always the HMAC to be passed to the next +// hop, or zero if this is the packet is not to be forwarded, since this is the +// last hop. +type HopData struct { + // Realm denotes the "real" of target chain of the next hop. For + // bitcoin, this value will be 0x00. + Realm [RealmByteSize]byte + + // NextAddress is the address of the next hop that this packet should + // be forward to. + NextAddress [AddressSize]byte + + // ForwardAmount is the HTLC amount that the next hop should forward. + // This value should take into account the fee require by this + // particular hop, and the cumulative fee for the entire route. + ForwardAmount uint64 + + // OutgoingCltv is the value of the outgoing absolute time-lock that + // should be included in the HTLC forwarded. + OutgoingCltv uint32 + + // ExtraBytes is the set of unused bytes within the onion payload. This + // extra set of bytes can be utilized by higher level applications to + // package additional data within the per-hop payload, or signal that a + // portion of the remaining set of hops are to be consumed as Extra + // Onion Blobs. + // + // TODO(roasbeef): rename to padding bytes? + ExtraBytes [NumPaddingBytes]byte +} + +// Encode writes the serialized version of the target HopData into the passed +// io.Writer. +func (hd *HopData) Encode(w io.Writer) error { + if _, err := w.Write(hd.Realm[:]); err != nil { + return err + } + + if _, err := w.Write(hd.NextAddress[:]); err != nil { + return err + } + + err := binary.Write(w, binary.BigEndian, hd.ForwardAmount) + if err != nil { + return err + } + + err = binary.Write(w, binary.BigEndian, hd.OutgoingCltv) + if err != nil { + return err + } + + if _, err := w.Write(hd.ExtraBytes[:]); err != nil { + return err + } + + return nil +} + +// Decode Decodes populates the target HopData with the contents of a serialized +// HopData packed into the passed io.Reader. +func (hd *HopData) Decode(r io.Reader) error { + if _, err := io.ReadFull(r, hd.Realm[:]); err != nil { + return err + } + + if _, err := io.ReadFull(r, hd.NextAddress[:]); err != nil { + return err + } + + err := binary.Read(r, binary.BigEndian, &hd.ForwardAmount) + if err != nil { + return err + } + + err = binary.Read(r, binary.BigEndian, &hd.OutgoingCltv) + if err != nil { + return err + } + + _, err = io.ReadFull(r, hd.ExtraBytes[:]) + return err +} + +// NewLegacyHopPayload creates a new hop payload given a set of forwarding +// instructions specified as HopData for a hop. This is the legacy encoding +// for a HopPayload. +func NewLegacyHopPayload(hopData *HopData) (HopPayload, error) { + var ( + h HopPayload + b bytes.Buffer + ) + + if err := hopData.Encode(&b); err != nil { + return h, nil + } + + // We'll also mark that this particular hop will be using the legacy + // format as the modern format packs the existing hop data information + // into the EOB space as a TLV stream. + h.Type = PayloadLegacy + h.Payload = b.Bytes() + + return h, nil +} + +// legacyPayloadSize returns the size of payloads encoded using the legacy +// fixed-size encoding. +func legacyPayloadSize() uint16 { + return LegacyHopDataSize - HMACSize +} + +// legacyNumBytes returns the number of bytes it will take to serialize the full +// payload. For the legacy encoding type, this is always a fixed number. +func legacyNumBytes() int { + return LegacyHopDataSize +} + +// isLegacyPayload returns true if the given byte is equal to the 0x00 byte +// which indicates that the payload should be decoded as a legacy payload. +func isLegacyPayloadByte(b byte) bool { + return b == 0x00 +} + +// encodeLegacyHopPayload takes a HopPayload and writes it to the given writer +// using the legacy encoding. +func encodeLegacyHopPayload(hp *HopPayload, w io.Writer) error { + // The raw payload and he HMAC are written in series. + if _, err := w.Write(hp.Payload); err != nil { + return err + } + + _, err := w.Write(hp.HMAC[:]) + + return err +} + +// decodeLegacyHopData takes a payload and decodes it into a HopData struct. +func decodeLegacyHopData(payload []byte) (*HopData, error) { + var ( + payloadReader = bytes.NewBuffer(payload) + hd HopData + ) + if err := hd.Decode(payloadReader); err != nil { + return nil, err + } + + return &hd, nil +} diff --git a/sphinx_test.go b/sphinx_test.go index e2b9e37..45c935d 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -68,7 +68,7 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke } copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) - hopPayload, err := NewHopPayload(&hopData, nil) + hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { return nil, nil, nil, nil, fmt.Errorf("unable to "+ "create new hop payload: %v", err) @@ -129,7 +129,7 @@ func TestBolt4Packet(t *testing.T) { copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) hopsData = append(hopsData, hopData) - hopPayload, err := NewHopPayload(&hopData, nil) + hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { t.Fatalf("unable to make hop payload: %v", err) } @@ -523,8 +523,8 @@ func newEOBRoute(numHops uint32, return fwdMsg, nodes, nil } -func mustNewHopPayload(hopData *HopData, eob []byte) HopPayload { - payload, err := NewHopPayload(hopData, eob) +func mustNewLegacyHopPayload(hopData *HopData) HopPayload { + payload, err := NewLegacyHopPayload(hopData) if err != nil { panic(err) } @@ -577,12 +577,12 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { { numNodes: 2, eobMapping: map[int]HopPayload{ - 0: mustNewHopPayload(&HopData{ + 0: mustNewLegacyHopPayload(&HopData{ Realm: [1]byte{0x00}, ForwardAmount: 2, OutgoingCltv: 3, NextAddress: [8]byte{1, 1, 1, 1, 1, 1, 1, 1}, - }, nil), + }), 1: HopPayload{ Type: PayloadTLV, Payload: bytes.Repeat([]byte("a"), LegacyHopDataSize*2), @@ -600,12 +600,12 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { Type: PayloadTLV, Payload: bytes.Repeat([]byte("a"), 100), }, - 1: mustNewHopPayload(&HopData{ + 1: mustNewLegacyHopPayload(&HopData{ Realm: [1]byte{0x00}, ForwardAmount: 22, OutgoingCltv: 9, NextAddress: [8]byte{1, 1, 1, 1, 1, 1, 1, 1}, - }, nil), + }), 2: HopPayload{ Type: PayloadTLV, Payload: bytes.Repeat([]byte("a"), 256), From e9cb480bb0fe1acc2a8c32a7d79661fef01af563 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 20:57:57 +0200 Subject: [PATCH 6/9] 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. --- crypto.go | 34 +++++- path.go | 181 ++++++++++++++++++++++++++++-- path_test.go | 169 ++++++++++++++++++++++++++++ testdata/route-blinding-test.json | 179 +++++++++++++++++++++++++++++ 4 files changed, 552 insertions(+), 11 deletions(-) create mode 100644 path_test.go create mode 100644 testdata/route-blinding-test.json diff --git a/crypto.go b/crypto.go index 0930544..1f3d35a 100644 --- a/crypto.go +++ b/crypto.go @@ -10,6 +10,7 @@ import ( "github.com/aead/chacha20" "github.com/btcsuite/btcd/btcec/v2" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "golang.org/x/crypto/chacha20poly1305" ) const ( @@ -19,6 +20,10 @@ const ( HMACSize = 32 ) +// chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305 +// encryption and decryption. +var chaChaPolyZeroNonce [chacha20poly1305.NonceSize]byte + // Hash256 is a statically sized, 32-byte array, typically containing // the output of a SHA256 hash. type Hash256 [sha256.Size]byte @@ -61,8 +66,8 @@ func (p *PrivKeyECDH) PubKey() *btcec.PublicKey { // k is our private key, and P is the public key, we perform the following // operation: // -// sx := k*P -// s := sha256(sx.SerializeCompressed()) +// sx := k*P +// s := sha256(sx.SerializeCompressed()) // // NOTE: This is part of the SingleKeyECDH interface. func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) { @@ -199,6 +204,31 @@ func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey { return priv.PubKey() } +// chacha20polyEncrypt initialises the ChaCha20Poly1305 algorithm with the given +// key and uses it to encrypt the passed message. This uses an all-zero nonce as +// required by the route-blinding spec. +func chacha20polyEncrypt(key, plainTxt []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + return aead.Seal(plainTxt[:0], chaChaPolyZeroNonce[:], plainTxt, nil), + nil +} + +// chacha20polyDecrypt initialises the ChaCha20Poly1305 algorithm with the given +// key and uses it to decrypt the passed cipher text. This uses an all-zero +// nonce as required by the route-blinding spec. +func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + return aead.Open(cipherTxt[:0], chaChaPolyZeroNonce[:], cipherTxt, nil) +} + // sharedSecretGenerator is an interface that abstracts away exactly *how* the // shared secret for each hop is generated. // diff --git a/path.go b/path.go index 736bbeb..6b8e839 100644 --- a/path.go +++ b/path.go @@ -1,18 +1,26 @@ package sphinx import ( + "errors" + "fmt" + "github.com/btcsuite/btcd/btcec/v2" ) -// NumMaxHops is the maximum path length. There is a maximum of 1300 bytes in -// the routing info block. Legacy hop payloads are always 65 bytes, while tlv -// payloads are at least 47 bytes (tlvlen 1, amt 2, timelock 2, nextchan 10, -// hmac 32) for the intermediate hops and 37 bytes (tlvlen 1, amt 2, timelock 2, -// hmac 32) for the exit hop. The maximum path length can therefore only be -// reached by using tlv payloads only. With that, the maximum number of -// intermediate hops is: Floor((1300 - 37) / 47) = 26. Including the exit hop, -// the maximum path length is 27 hops. -const NumMaxHops = 27 +const ( + // NumMaxHops is the maximum path length. There is a maximum of 1300 + // bytes in the routing info block. Legacy hop payloads are always 65 + // bytes, while tlv payloads are at least 47 bytes (tlvlen 1, amt 2, + // timelock 2, nextchan 10, hmac 32) for the intermediate hops and 37 + // bytes (tlvlen 1, amt 2, timelock 2, hmac 32) for the exit hop. The + // maximum path length can therefore only be reached by using tlv + // payloads only. With that, the maximum number of intermediate hops + // is: Floor((1300 - 37) / 47) = 26. Including the exit hop, the + // maximum path length is 27 hops. + NumMaxHops = 27 + + routeBlindingHMACKey = "blinded_node_id" +) // PaymentPath represents a series of hops within the Lightning Network // starting at a sender and terminating at a receiver. Each hop contains a set @@ -93,3 +101,158 @@ func (p *PaymentPath) TotalPayloadSize() int { return totalSize } + +// BlindedPath represents all the data that the creator of a blinded path must +// transmit to the builder of route that will send to this path. +type BlindedPath struct { + // IntroductionPoint is the real node ID of the first hop in the blinded + // path. The sender should be able to find this node in the network + // graph and route to it. + IntroductionPoint *btcec.PublicKey + + // BlindingPoint is the first ephemeral blinding point. This is the + // point that the introduction node will use in order to create a shared + // secret with the builder of the blinded route. This point will need + // to be communicated to the introduction node by the sender in some + // way. + BlindingPoint *btcec.PublicKey + + // BlindedHops is a list of ordered BlindedHopInfo. Each entry + // represents a hop in the blinded path along with the encrypted data to + // be sent to that node. Note that the first entry in the list + // represents the introduction point of the path and so the node ID of + // this point does not strictly need to be transmitted to the sender + // since they will be able to derive the point using the BlindingPoint. + BlindedHops []*BlindedHopInfo +} + +// BlindedHopInfo represents a blinded node pub key along with the encrypted +// data for a node in a blinded route. +type BlindedHopInfo struct { + // BlindedNodePub is the blinded public key of the node in the blinded + // route. + BlindedNodePub *btcec.PublicKey + + // CipherText is the encrypted payload to be transported to the hop in + // the blinded route. + CipherText []byte +} + +// HopInfo represents a real node pub key along with the plaintext data for a +// node in a blinded route. +type HopInfo struct { + // NodePub is the real public key of the node in the blinded route. + NodePub *btcec.PublicKey + + // PlainText is the un-encrypted payload to be transported to the hop + // the blinded route. + PlainText []byte +} + +// Encrypt uses the given sharedSecret to blind the public key of the node and +// encrypt the payload and returns the resulting BlindedHopInfo. +func (i *HopInfo) Encrypt(sharedSecret Hash256) (*BlindedHopInfo, error) { + blindedData, err := encryptBlindedHopData(sharedSecret, i.PlainText) + if err != nil { + return nil, err + } + + return &BlindedHopInfo{ + BlindedNodePub: blindNodeID(sharedSecret, i.NodePub), + CipherText: blindedData, + }, nil +} + +// BuildBlindedPath creates a new BlindedPath from a session key along with a +// list of HopInfo representing the nodes in the blinded path. The first hop in +// paymentPath is expected to be the introduction node. +func BuildBlindedPath(sessionKey *btcec.PrivateKey, + paymentPath []*HopInfo) (*BlindedPath, error) { + + if len(paymentPath) < 1 { + return nil, errors.New("at least 1 hop is required to create " + + "a blinded path") + } + + bp := &BlindedPath{ + IntroductionPoint: paymentPath[0].NodePub, + BlindingPoint: sessionKey.PubKey(), + BlindedHops: make([]*BlindedHopInfo, len(paymentPath)), + } + + keys := make([]*btcec.PublicKey, len(paymentPath)) + for i, p := range paymentPath { + keys[i] = p.NodePub + } + + hopSharedSecrets, err := generateSharedSecrets(keys, sessionKey) + if err != nil { + return nil, fmt.Errorf("error generating shared secret: %v", + err) + } + + for i, hop := range paymentPath { + blindedInfo, err := hop.Encrypt(hopSharedSecrets[i]) + if err != nil { + return nil, err + } + + bp.BlindedHops[i] = blindedInfo + } + + return bp, nil +} + +// blindNodeID blinds the given public key using the provided shared secret. +func blindNodeID(sharedSecret Hash256, + pubKey *btcec.PublicKey) *btcec.PublicKey { + + blindingFactorBytes := generateKey(routeBlindingHMACKey, &sharedSecret) + + var blindingFactor btcec.ModNScalar + blindingFactor.SetBytes(&blindingFactorBytes) + + return blindGroupElement(pubKey, blindingFactor) +} + +// encryptBlindedHopData blinds/encrypts the given plain text data using the +// provided shared secret. +func encryptBlindedHopData(sharedSecret Hash256, plainTxt []byte) ([]byte, + error) { + + rhoKey := generateKey("rho", &sharedSecret) + + return chacha20polyEncrypt(rhoKey[:], plainTxt) +} + +// decryptBlindedHopData decrypts the data encrypted by the creator of the +// blinded route. +func decryptBlindedHopData(privKey SingleKeyECDH, ephemPub *btcec.PublicKey, + encryptedData []byte) ([]byte, error) { + + ss, err := privKey.ECDH(ephemPub) + if err != nil { + return nil, err + } + + ssHash := Hash256(ss) + rho := generateKey("rho", &ssHash) + + return chacha20polyDecrypt(rho[:], encryptedData) +} + +// NextEphemeral computes the next ephemeral key given the current ephemeral +// key and this node's private key. +func NextEphemeral(privKey SingleKeyECDH, + ephemPub *btcec.PublicKey) (*btcec.PublicKey, error) { + + ss, err := privKey.ECDH(ephemPub) + if err != nil { + return nil, err + } + + blindingFactor := computeBlindingFactor(ephemPub, ss[:]) + nextEphem := blindGroupElement(ephemPub, blindingFactor) + + return nextEphem, nil +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..9d000ef --- /dev/null +++ b/path_test.go @@ -0,0 +1,169 @@ +package sphinx + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +const routeBlindingTestFileName = "testdata/route-blinding-test.json" + +// TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against +// the spec test vectors. +func TestBuildBlindedRoute(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(routeBlindingTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // blindingJsonTestCase struct defined below. + testCase := &blindingJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + require.Len(t, testCase.Generate.Hops, 4) + + // buildPaymentPath is a helper closure used to convert hopData objects + // into BlindedPathHop objects. + buildPaymentPath := func(h []hopData) []*HopInfo { + path := make([]*HopInfo, len(h)) + for i, hop := range h { + nodeIDStr, _ := hex.DecodeString(hop.NodeID) + nodeID, _ := btcec.ParsePubKey(nodeIDStr) + payload, _ := hex.DecodeString(hop.EncodedTLVs) + + path[i] = &HopInfo{ + NodePub: nodeID, + PlainText: payload, + } + } + return path + } + + // First, Eve will build a blinded path from Dave to herself. + eveSessKey := privKeyFromString(testCase.Generate.Hops[2].SessionKey) + eveDavePath := buildPaymentPath(testCase.Generate.Hops[2:]) + pathED, err := BuildBlindedPath(eveSessKey, eveDavePath) + require.NoError(t, err) + + // At this point, Eve will give her blinded path to Bob who will then + // build his own blinded route from himself to Carol. He will then + // concatenate the two paths. Note that in his TLV for Carol, Bob will + // add the `next_blinding_override` field which he will set to the + // first blinding point in Eve's blinded route. This will indicate to + // Carol that she should use this point for the next blinding key + // instead of the next blinding key that she derives. + bobCarolPath := buildPaymentPath(testCase.Generate.Hops[:2]) + bobSessKey := privKeyFromString(testCase.Generate.Hops[0].SessionKey) + pathBC, err := BuildBlindedPath(bobSessKey, bobCarolPath) + require.NoError(t, err) + + // Construct the concatenated path. + path := &BlindedPath{ + IntroductionPoint: pathBC.IntroductionPoint, + BlindingPoint: pathBC.BlindingPoint, + BlindedHops: append(pathBC.BlindedHops, + pathED.BlindedHops...), + } + + // Check that the constructed path is equal to the test vector path. + require.True(t, equalPubKeys( + testCase.Route.IntroductionNodeID, path.IntroductionPoint, + )) + require.True(t, equalPubKeys( + testCase.Route.Blinding, path.BlindingPoint, + )) + + for i, hop := range testCase.Route.Hops { + require.True(t, equalPubKeys( + hop.BlindedNodeID, path.BlindedHops[i].BlindedNodePub, + )) + + data, _ := hex.DecodeString(hop.EncryptedData) + require.True( + t, bytes.Equal(data, path.BlindedHops[i].CipherText), + ) + } + + // Assert that each hop is able to decode the encrypted data meant for + // it. + for i, hop := range testCase.Unblind.Hops { + priv := privKeyFromString(hop.NodePrivKey) + ephem := pubKeyFromString(hop.EphemeralPubKey) + + data, err := decryptBlindedHopData( + &PrivKeyECDH{PrivKey: priv}, ephem, + path.BlindedHops[i].CipherText, + ) + require.NoError(t, err) + + decoded, _ := hex.DecodeString(hop.DecryptedData) + require.True(t, bytes.Equal(data, decoded)) + + nextEphem, err := NextEphemeral(&PrivKeyECDH{priv}, ephem) + require.NoError(t, err) + + require.True(t, equalPubKeys( + hop.NextEphemeralPubKey, nextEphem, + )) + } +} + +type blindingJsonTestCase struct { + Generate generateData `json:"generate"` + Route routeData `json:"route"` + Unblind unblindData `json:"unblind"` +} + +type routeData struct { + IntroductionNodeID string `json:"introduction_node_id"` + Blinding string `json:"blinding"` + Hops []blindedHop `json:"hops"` +} + +type unblindData struct { + Hops []unblindedHop `json:"hops"` +} + +type generateData struct { + Hops []hopData `json:"hops"` +} + +type unblindedHop struct { + NodePrivKey string `json:"node_privkey"` + EphemeralPubKey string `json:"ephemeral_pubkey"` + DecryptedData string `json:"decrypted_data"` + NextEphemeralPubKey string `json:"next_ephemeral_pubkey"` +} + +type hopData struct { + SessionKey string `json:"session_key"` + NodeID string `json:"node_id"` + EncodedTLVs string `json:"encoded_tlvs"` +} + +type blindedHop struct { + BlindedNodeID string `json:"blinded_node_id"` + EncryptedData string `json:"encrypted_data"` +} + +func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool { + return hex.EncodeToString(pk.SerializeCompressed()) == pkStr +} + +func privKeyFromString(pkStr string) *btcec.PrivateKey { + bytes, _ := hex.DecodeString(pkStr) + key, _ := btcec.PrivKeyFromBytes(bytes) + return key +} + +func pubKeyFromString(pkStr string) *btcec.PublicKey { + bytes, _ := hex.DecodeString(pkStr) + key, _ := btcec.ParsePubKey(bytes) + return key +} diff --git a/testdata/route-blinding-test.json b/testdata/route-blinding-test.json new file mode 100644 index 0000000..bba9031 --- /dev/null +++ b/testdata/route-blinding-test.json @@ -0,0 +1,179 @@ +{ + "comment": "test vector for using blinded routes", + "generate": { + "comment": "This section contains test data for creating a blinded route. This route is the concatenation of two blinded routes: one from Dave to Eve and one from Bob to Carol.", + "hops": [ + { + "comment": "Bob creates a Bob -> Carol route with the following session_key and concatenates it with the Dave -> Eve route.", + "session_key": "0202020202020202020202020202020202020202020202020202020202020202", + "alias": "Bob", + "node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x1729", + "payment_relay": { + "cltv_expiry_delta": 36, + "fee_proportional_millionths": 150, + "fee_base_msat": 10000 + }, + "payment_constraints": { + "max_cltv_expiry": 748005, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [] + }, + "unknown_tag_561": "123456" + }, + "encoded_tlvs": "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", + "ephemeral_privkey": "0202020202020202020202020202020202020202020202020202020202020202", + "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "shared_secret": "76771bab0cc3d0de6e6f60147fd7c9c7249a5ced3d0612bdfaeec3b15452229d", + "rho": "ba217b23c0978d84c4a19be8a9ff64bc1b40ed0d7ecf59521567a5b3a9a1dd48", + "encrypted_data": "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb", + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25" + }, + { + "comment": "Notice the next_blinding_override tlv in Carol's payload, indicating that Bob concatenated his route with another blinded route starting at Dave.", + "alias": "Carol", + "node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "tlvs": { + "short_channel_id": "0x0x1105", + "next_blinding_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "payment_relay": { + "cltv_expiry_delta": 48, + "fee_proportional_millionths": 100, + "fee_base_msat": 500 + }, + "payment_constraints": { + "max_cltv_expiry": 747969, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [] + } + }, + "encoded_tlvs": "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", + "ephemeral_privkey": "0a2aa791ac81265c139237b2b84564f6000b1d4d0e68d4b9cc97c5536c9b61c1", + "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "shared_secret": "dc91516ec6b530a3d641c01f29b36ed4dc29a74e063258278c0eeed50313d9b8", + "rho": "d1e62bae1a8e169da08e6204997b60b1a7971e0f246814c648125c35660f5416", + "encrypted_data": "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e", + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7" + }, + { + "comment": "Eve creates a Dave -> Eve blinded route using the following session_key.", + "session_key": "0101010101010101010101010101010101010101010101010101010101010101", + "alias": "Dave", + "node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x561", + "payment_relay": { + "cltv_expiry_delta": 144, + "fee_proportional_millionths": 250 + }, + "payment_constraints": { + "max_cltv_expiry": 747921, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [] + } + }, + "encoded_tlvs": "01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00", + "ephemeral_privkey": "0101010101010101010101010101010101010101010101010101010101010101", + "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "shared_secret": "dc46f3d1d99a536300f17bc0512376cc24b9502c5d30144674bfaa4b923d9057", + "rho": "393aa55d35c9e207a8f28180b81628a31dff558c84959cdc73130f8c321d6a06", + "encrypted_data": "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105", + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf" + }, + { + "comment": "Eve is the final recipient, so she included a path_id in her own payload to verify that the route is used when she expects it.", + "alias": "Eve", + "node_id": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000", + "path_id": "deadbeef", + "payment_constraints": { + "max_cltv_expiry": 747777, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [113] + }, + "unknown_tag_65535": "06c1" + }, + "encoded_tlvs": "011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1", + "ephemeral_privkey": "62e8bcd6b5f7affe29bec4f0515aab2eebd1ce848f4746a9638aa14e3024fb1b", + "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "shared_secret": "352a706b194c2b6d0a04ba1f617383fb816dc5f8f9ac0b60dd19c9ae3b517289", + "rho": "719d0307340b1c68b79865111f0de6e97b093a30bc603cebd1beb9eef116f2d8", + "encrypted_data": "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8", + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae" + } + ] + }, + "route": { + "comment": "This section contains the resulting blinded route, which can then be used inside onion messages or payments.", + "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "hops": [ + { + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", + "encrypted_data": "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb" + }, + { + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "encrypted_data": "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e" + }, + { + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "encrypted_data": "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105" + }, + { + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "encrypted_data": "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8" + } + ] + }, + "unblind": { + "comment": "This section contains test data for unblinding the route at each intermediate hop.", + "hops": [ + { + "alias": "Bob", + "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "blinded_privkey": "d12fec0332c3e9d224789a17ebd93595f37d37bd8ef8bd3d2e6ce50acb9e554f", + "decrypted_data": "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", + "next_ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" + }, + { + "alias": "Carol", + "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "blinded_privkey": "bfa697fbbc8bbc43ca076e6dd60d306038a32af216b9dc6fc4e59e5ae28823c1", + "decrypted_data": "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", + "next_ephemeral_pubkey": "03af5ccc91851cb294e3a364ce63347709a08cdffa58c672e9a5c587ddd1bbca60", + "next_ephemeral_pubkey_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "alias": "Dave", + "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "blinded_privkey": "cebc115c7fce4c295dc396dea6c79115b289b8ceeceea2ed61cf31428d88fc4e", + "decrypted_data": "01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00", + "next_ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" + }, + { + "alias": "Eve", + "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", + "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "blinded_privkey": "ff4e07da8d92838bedd019ce532eb990ed73b574e54a67862a1df81b40c0d2af", + "decrypted_data": "011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1", + "next_ephemeral_pubkey": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" + } + ] + } +} \ No newline at end of file From b1469b8636774dd27bfd225ef7b993b88f04eb1c Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 21:32:07 +0200 Subject: [PATCH 7/9] multi: handle route blinding in ProcessOnionPacket In this commit, ProcessOnionPacket is updated to take in a blinding key as a parameter. This key is then used in order to determine the blinding factor necessary to decrypt the onion. This change is accompanied with a test that tests this change against a test vector from the route blinding spec PR. --- bench_test.go | 4 +- crypto.go | 56 +++++++- obfuscation.go | 16 ++- path_test.go | 119 ++++++++++++++- sphinx.go | 77 +++++++--- sphinx_test.go | 27 ++-- testdata/onion-route-blinding-test.json | 183 ++++++++++++++++++++++++ 7 files changed, 441 insertions(+), 41 deletions(-) create mode 100644 testdata/onion-route-blinding-test.json diff --git a/bench_test.go b/bench_test.go index c288645..57f0c08 100644 --- a/bench_test.go +++ b/bench_test.go @@ -77,7 +77,9 @@ func BenchmarkProcessPacket(b *testing.B) { pkt *ProcessedPacket ) for i := 0; i < b.N; i++ { - pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil, uint32(i)) + pkt, err = path[0].ProcessOnionPacket( + sphinxPacket, nil, uint32(i), + ) if err != nil { b.Fatalf("unable to process packet %d: %v", i, err) } diff --git a/crypto.go b/crypto.go index 1f3d35a..73fbed2 100644 --- a/crypto.go +++ b/crypto.go @@ -239,17 +239,63 @@ type sharedSecretGenerator interface { generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) } -// generateSharedSecret generates the shared secret by given ephemeral key. -func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { +// generateSharedSecret generates the shared secret using the given ephemeral +// pub key and the Router's private key. If a blindingPoint is provided then it +// is used to tweak the Router's private key before creating the shared secret +// with the ephemeral pub key. The blinding point is used to determine our +// shared secret with the receiver. From that we can determine our shared +// secret with the sender using the dhKey. +func (r *Router) generateSharedSecret(dhKey, + blindingPoint *btcec.PublicKey) (Hash256, error) { + + // If no blinding point is provided, then the un-tweaked dhKey can + // be used to derive the shared secret + if blindingPoint == nil { + return sharedSecret(r.onionKey, dhKey) + } + + // We use the blinding point to calculate the blinding factor that the + // receiver used with us so that we can use it to tweak our priv key. + // The sender would have created their shared secret with our blinded + // pub key. + // * ss_receiver = H(k * E_receiver) + ssReceiver, err := sharedSecret(r.onionKey, blindingPoint) + if err != nil { + return Hash256{}, err + } + + // Compute the blinding factor that the receiver would have used to + // blind our public key. + // + // * bf = HMAC256("blinded_node_id", ss_receiver) + blindingFactorBytes := generateKey(routeBlindingHMACKey, &ssReceiver) + var blindingFactor btcec.ModNScalar + blindingFactor.SetBytes(&blindingFactorBytes) + + // Now, we want to calculate the shared secret between the sender and + // our blinded key. In other words we want to calculate: + // * ss_sender = H(E_sender * bf * k) + // + // Since the order in which the above multiplication happens does not + // matter, we will first multiply E_sender with the blinding factor: + blindedEphemeral := blindGroupElement(dhKey, blindingFactor) + + // Finally, we compute the ECDH to get the shared secret, ss_sender: + return sharedSecret(r.onionKey, blindedEphemeral) +} + +// sharedSecret does a ECDH operation on the passed private and public keys and +// returns the result. +func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) { var sharedSecret Hash256 // Ensure that the public key is on our curve. - if !dhKey.IsOnCurve() { + if !pub.IsOnCurve() { return sharedSecret, ErrInvalidOnionKey } - // Compute our shared secret. - return r.onionKey.ECDH(dhKey) + // Compute the shared secret. + return priv.ECDH(pub) } // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a diff --git a/obfuscation.go b/obfuscation.go index b8df7cc..a731a6c 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -13,12 +13,18 @@ type OnionErrorEncrypter struct { } // NewOnionErrorEncrypter creates new instance of the onion encrypter backed by -// the passed router, with encryption to be doing using the passed -// ephemeralKey. -func NewOnionErrorEncrypter(router *Router, - ephemeralKey *btcec.PublicKey) (*OnionErrorEncrypter, error) { +// the passed router, with encryption to be done using the passed ephemeralKey. +func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey, + opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) { - sharedSecret, err := router.generateSharedSecret(ephemeralKey) + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } + + sharedSecret, err := router.generateSharedSecret( + ephemeralKey, cfg.blindingPoint, + ) if err != nil { return nil, err } diff --git a/path_test.go b/path_test.go index 9d000ef..c61f145 100644 --- a/path_test.go +++ b/path_test.go @@ -8,10 +8,14 @@ import ( "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/require" ) -const routeBlindingTestFileName = "testdata/route-blinding-test.json" +const ( + routeBlindingTestFileName = "testdata/route-blinding-test.json" + onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" +) // TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against // the spec test vectors. @@ -114,6 +118,119 @@ func TestBuildBlindedRoute(t *testing.T) { } } +// TestOnionRouteBlinding tests that an onion packet can correctly be processed +// by a node in a blinded route. +func TestOnionRouteBlinding(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(onionRouteBlindingTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // blindingJsonTestCase struct defined above. + testCase := &onionBlindingJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + assoc, err := hex.DecodeString(testCase.Generate.AssocData) + require.NoError(t, err) + + // Extract the original onion packet to be processed. + onion, err := hex.DecodeString(testCase.Generate.Onion) + require.NoError(t, err) + + onionBytes := bytes.NewReader(onion) + onionPacket := &OnionPacket{} + require.NoError(t, onionPacket.Decode(onionBytes)) + + // peelOnion is a helper closure that can be used to set up a Router + // and use it to process the given onion packet. + peelOnion := func(key *btcec.PrivateKey, + blindingPoint *btcec.PublicKey) *ProcessedPacket { + + r := NewRouter( + &PrivKeyECDH{PrivKey: key}, &chaincfg.MainNetParams, + NewMemoryReplayLog(), + ) + + require.NoError(t, r.Start()) + defer r.Stop() + + res, err := r.ProcessOnionPacket( + onionPacket, assoc, 10, + WithBlindingPoint(blindingPoint), + ) + require.NoError(t, err) + + return res + } + + hops := testCase.Decrypt.Hops + require.Len(t, hops, 5) + + // There are some things that the processor of the onion packet will + // only be able to determine from the actual contents of the encrypted + // data it receives. These things include the next_blinding_point for + // the introduction point and the next_blinding_override. The decryption + // of this data is dependent on the encoding chosen by higher layers. + // The test uses TLVs. Since the extraction of this data is dependent + // on layers outside the scope of this library, we provide handle these + // cases manually for the sake of the test. + var ( + introPointIndex = 2 + firstBlinding = pubKeyFromString(hops[1].NextBlinding) + + concatIndex = 3 + blindingOverride = pubKeyFromString(hops[2].NextBlinding) + ) + + var blindingPoint *btcec.PublicKey + for i, hop := range testCase.Decrypt.Hops { + buff := bytes.NewBuffer(nil) + require.NoError(t, onionPacket.Encode(buff)) + require.Equal(t, hop.Onion, hex.EncodeToString(buff.Bytes())) + + priv := privKeyFromString(hop.NodePrivKey) + + if i == introPointIndex { + blindingPoint = firstBlinding + } else if i == concatIndex { + blindingPoint = blindingOverride + } + + processedPkt := peelOnion(priv, blindingPoint) + + if blindingPoint != nil { + blindingPoint, err = NextEphemeral( + &PrivKeyECDH{priv}, blindingPoint, + ) + require.NoError(t, err) + } + onionPacket = processedPkt.NextPacket + } +} + +type onionBlindingJsonTestCase struct { + Generate generateOnionData `json:"generate"` + Decrypt decryptData `json:"decrypt"` +} + +type generateOnionData struct { + SessionKey string `json:"session_key"` + AssocData string `json:"associated_data"` + Onion string `json:"onion"` +} + +type decryptData struct { + Hops []decryptHops `json:"hops"` +} + +type decryptHops struct { + Onion string `json:"onion"` + NodePrivKey string `json:"node_privkey"` + NextBlinding string `json:"next_blinding"` +} + type blindingJsonTestCase struct { Generate generateData `json:"generate"` Route routeData `json:"route"` diff --git a/sphinx.go b/sphinx.go index 36e9a81..a262c46 100644 --- a/sphinx.go +++ b/sphinx.go @@ -521,20 +521,48 @@ func (r *Router) Stop() { r.log.Stop() } +// processOnionCfg is a set of config values that can be used to modify how an +// onion is processed. +type processOnionCfg struct { + blindingPoint *btcec.PublicKey +} + +// ProcessOnionOpt defines the signature of a function option that can be used +// to modify how an onion is processed. +type ProcessOnionOpt func(cfg *processOnionCfg) + +// WithBlindingPoint is a function option that can be used to set the blinding +// point to be used when processing an onion. +func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt { + return func(cfg *processOnionCfg) { + cfg.blindingPoint = point + } +} + // ProcessOnionPacket processes an incoming onion packet which has been forward // to the target Sphinx router. If the encoded ephemeral key isn't on the // target Elliptic Curve, then the packet is rejected. Similarly, if the -// derived shared secret has been seen before the packet is rejected. Finally -// if the MAC doesn't check the packet is again rejected. +// derived shared secret has been seen before the packet is rejected. If the +// blinded point is specified, then it will be used along with the ephemeral key +// in the onion packet to derive the shared secret. Finally, if the MAC doesn't +// check the packet is again rejected. // // In the case of a successful packet processing, and ProcessedPacket struct is // returned which houses the newly parsed packet, along with instructions on // what to do next. -func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, - assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) { +func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, + incomingCltv uint32, opts ...ProcessOnionOpt) (*ProcessedPacket, + error) { + + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.generateSharedSecret( + onionPkt.EphemeralKey, cfg.blindingPoint, + ) if err != nil { return nil, err } @@ -546,7 +574,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData, r) + packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) if err != nil { return nil, err } @@ -564,16 +592,23 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, // // NOTE: This method does not do any sort of replay protection, and should only // be used to reconstruct packets that were successfully processed previously. -func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, - assocData []byte) (*ProcessedPacket, error) { +func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, + opts ...ProcessOnionOpt) (*ProcessedPacket, error) { + + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.generateSharedSecret( + onionPkt.EphemeralKey, cfg.blindingPoint, + ) if err != nil { return nil, err } - return processOnionPacket(onionPkt, &sharedSecret, assocData, r) + return processOnionPacket(onionPkt, &sharedSecret, assocData) } // unwrapPacket wraps a layer of the passed onion packet using the specified @@ -640,8 +675,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // packets. The processed packets returned from this method should only be used // if the packet was not flagged as a replayed packet. func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, - assocData []byte, - sharedSecretGen sharedSecretGenerator) (*ProcessedPacket, error) { + assocData []byte) (*ProcessedPacket, error) { // First, we'll unwrap an initial layer of the onion packet. Typically, // we'll only have a single layer to unwrap, However, if the sender has @@ -721,18 +755,25 @@ func (r *Router) BeginTxn(id []byte, nels int) *Tx { // ProcessOnionPacket processes an incoming onion packet which has been forward // to the target Sphinx router. If the encoded ephemeral key isn't on the // target Elliptic Curve, then the packet is rejected. Similarly, if the -// derived shared secret has been seen before the packet is rejected. Finally -// if the MAC doesn't check the packet is again rejected. +// derived shared secret has been seen before the packet is rejected. If the +// blinded point is specified, then it will be used along with the ephemeral key +// in the onion packet to derive the shared secret. Finally, if the MAC doesn't +// check the packet is again rejected. // // In the case of a successful packet processing, and ProcessedPacket struct is // returned which houses the newly parsed packet, along with instructions on // what to do next. func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, - assocData []byte, incomingCltv uint32) error { + assocData []byte, incomingCltv uint32, opts ...ProcessOnionOpt) error { + + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } // Compute the shared secret for this onion packet. sharedSecret, err := t.router.generateSharedSecret( - onionPkt.EphemeralKey, + onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { return err @@ -745,9 +786,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket( - onionPkt, &sharedSecret, assocData, t.router, - ) + packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) if err != nil { return err } diff --git a/sphinx_test.go b/sphinx_test.go index 45c935d..7d99ba3 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -182,7 +182,9 @@ func TestSphinxCorrectness(t *testing.T) { hop := nodes[i] t.Logf("Processing at hop: %v \n", i) - onionPacket, err := hop.ProcessOnionPacket(fwdMsg, nil, uint32(i)+1) + onionPacket, err := hop.ProcessOnionPacket( + fwdMsg, nil, uint32(i)+1, + ) if err != nil { t.Fatalf("Node %v was unable to process the "+ "forwarding message: %v", i, err) @@ -271,14 +273,17 @@ func TestSphinxNodeRelpay(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if _, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1); err != nil { + _, err = nodes[0].ProcessOnionPacket(fwdMsg, nil, 1) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } // Now, force the node to process the packet a second time, this should // fail with a detected replay error. - if _, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1); err != ErrReplayedPacket { - t.Fatalf("sphinx packet replay should be rejected, instead error is %v", err) + _, err = nodes[0].ProcessOnionPacket(fwdMsg, nil, 1) + if err != ErrReplayedPacket { + t.Fatalf("sphinx packet replay should be rejected, instead "+ + "error is %v", err) } } @@ -298,14 +303,14 @@ func TestSphinxNodeRelpaySameBatch(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(0, fwdMsg, nil, 1); err != nil { + if err := tx.ProcessOnionPacket(0, fwdMsg, nil, 1, nil); err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } // Now, force the node to process the packet a second time, this call // should not fail, even though the batch has internally recorded this // as a duplicate. - err = tx.ProcessOnionPacket(1, fwdMsg, nil, 1) + err = tx.ProcessOnionPacket(1, fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("adding duplicate sphinx packet to batch should not "+ "result in an error, instead got: %v", err) @@ -344,7 +349,8 @@ func TestSphinxNodeRelpayLaterBatch(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1); err != nil { + err = tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -357,7 +363,7 @@ func TestSphinxNodeRelpayLaterBatch(t *testing.T) { // Now, force the node to process the packet a second time, this should // fail with a detected replay error. - err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1) + err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("sphinx packet replay should not have been rejected, "+ "instead error is %v", err) @@ -389,7 +395,8 @@ func TestSphinxNodeReplayBatchIdempotency(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1); err != nil { + err = tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -402,7 +409,7 @@ func TestSphinxNodeReplayBatchIdempotency(t *testing.T) { // Now, force the node to process the packet a second time, this should // not fail with a detected replay error. - err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1) + err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("sphinx packet replay should not have been rejected, "+ "instead error is %v", err) diff --git a/testdata/onion-route-blinding-test.json b/testdata/onion-route-blinding-test.json new file mode 100644 index 0000000..93cbba2 --- /dev/null +++ b/testdata/onion-route-blinding-test.json @@ -0,0 +1,183 @@ +{ + "comment": "test vector for a payment onion sent to a partially blinded route", + "generate": { + "comment": "This section contains test data for creating a payment onion that sends to the provided blinded route.", + "session_key": "0303030303030303030303030303030303030303030303030303030303030303", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "final_amount_msat": 100000, + "final_cltv": 749000, + "blinded_payinfo": { + "comment": "total costs for using the blinded path", + "fee_base_msat": 10100, + "fee_proportional_millionths": 251, + "cltv_expiry_delta": 150 + }, + "blinded_route": { + "comment": "This section contains a blinded route that the sender will use for his payment, usually obtained from a Bolt 12 invoice.", + "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "hops": [ + { + "alias": "Bob", + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", + "encrypted_data": "cd7b00ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c499a2888b49f2e72b19446f7e60a818aa2938d8c625415b992b8928a7321edb8f7cea40de362bed082ad51acc6156dca5532fb68" + }, + { + "alias": "Carol", + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "encrypted_data": "cc0f16524fd7f8bb0f4e8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f570f656a5aaecaf1ee8dc9d0fa1d424759be1932a8f29fac08bc2d2a1ed7159f28b" + }, + { + "alias": "Dave", + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "encrypted_data": "0fa1a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9e49895fd4bcebf6f58d6f61a6d41a9bf5aa4b0453437856632e8255c351873143ddf2bb2b0832b091e1b4" + }, + { + "alias": "Eve", + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "encrypted_data": "da1c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63c688768042ade22f2c22f5724767d171fd221d3e579e43b354cc72e3ef146ada91a892d95fc48662f5b158add0af457da" + } + ] + }, + "full_route": { + "comment": "The sender adds one normal hop through Alice, who doesn't support blinded payments (and doesn't charge a fee). The sender provides the initial blinding point in Bob's onion payload, and encrypted_data for each node in the blinded route.", + "hops": [ + { + "alias": "Alice", + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "14020301ae2d04030b6e5e0608000000000000000a", + "tlvs": { + "outgoing_channel_id": "0x0x10", + "amt_to_forward": 110125, + "outgoing_cltv_value": 749150 + } + }, + { + "alias": "Bob", + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "740a4fcd7b00ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c499a2888b49f2e72b19446f7e60a818aa2938d8c625415b992b8928a7321edb8f7cea40de362bed082ad51acc6156dca5532fb680c21024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "tlvs": { + "current_blinding_point": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "encrypted_recipient_data": { + "padding": "0000000000000000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x1", + "payment_relay": { + "cltv_expiry_delta": 50, + "fee_proportional_millionths": 0, + "fee_base_msat": 10000 + }, + "payment_constraints": { + "max_cltv_expiry": 750150, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + }, + { + "alias": "Carol", + "pubkey": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "payload": "510a4fcc0f16524fd7f8bb0f4e8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f570f656a5aaecaf1ee8dc9d0fa1d424759be1932a8f29fac08bc2d2a1ed7159f28b", + "tlvs": { + "encrypted_recipient_data": { + "short_channel_id": "0x0x2", + "next_blinding_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "payment_relay": { + "cltv_expiry_delta": 75, + "fee_proportional_millionths": 150, + "fee_base_msat": 100 + }, + "payment_constraints": { + "max_cltv_expiry": 750100, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + }, + { + "alias": "Dave", + "pubkey": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "payload": "510a4f0fa1a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9e49895fd4bcebf6f58d6f61a6d41a9bf5aa4b0453437856632e8255c351873143ddf2bb2b0832b091e1b4", + "tlvs": { + "encrypted_recipient_data": { + "padding": "00000000000000000000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x3", + "payment_relay": { + "cltv_expiry_delta": 25, + "fee_proportional_millionths": 100 + }, + "payment_constraints": { + "max_cltv_expiry": 750025, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + }, + { + "alias": "Eve", + "pubkey": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "payload": "6002030186a004030b6dc80a4fda1c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63c688768042ade22f2c22f5724767d171fd221d3e579e43b354cc72e3ef146ada91a892d95fc48662f5b158add0af457da12030249f0", + "tlvs": { + "amt_to_forward": 100000, + "total_amount_msat": 150000, + "outgoing_cltv_value": 749000, + "encrypted_recipient_data": { + "padding": "00000000000000000000000000000000000000000000000000000000", + "path_id": "c9cf92f45ade68345bc20ae672e2012f4af487ed4415", + "payment_constraints": { + "max_cltv_expiry": 750000, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + } + ] + }, + "onion": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337dadf610256c6ab518495dce9cdedf9391e21a71dada75be905267ba82f326c0513dda706908cfee834996700f881b2aed106585d61a2690de4ebe5d56ad2013b520af2a3c49316bc590ee83e8c31b1eb11ff766dad27ca993326b1ed582fb451a2ad87fbf6601134c6341c4a2deb6850e25a355be68dbb6923dc89444fdd74a0f700433b667bda345926099f5547b07e97ad903e8a01566a78ae177366239e793dac719de805565b6d0a1d290e273f705cfc56873f8b5e28225f7ded7a1d4ceffae63f91e477be8c917c786435976102a924ba4ba3de6150c829ce01c25428f2f5d05ef023be7d590ecdf6603730db3948f80ca1ed3d85227e64ef77200b9b557f427b6e1073cfa0e63e4485441768b98ab11ba8104a6cee1d7af7bb5ee9c05cf9cf4718901e92e09dfe5cb3af336a953072391c1e91fc2f4b92e124b38e0c6d17ef6ba7bbe93f02046975bb01b7f766fcfc5a755af11a90cc7eb3505986b56e07a7855534d03b79f0dfbfe645b0d6d4185c038771fd25b800aa26b2ed2e30b1e713659468618a2fea04fcd0473284598f76b11b0d159d343bc9711d3bea8d561547bcc8fff12317c0e7b1ee75bcb8082d762b6417f99d0f71ff7c060f6b564ad6827edaffa72eefcc4ce633a8da8d41c19d8f6aebd8878869eb518ccc16dccae6a94c690957598ce0295c1c46af5d7a2f0955b5400526bfd1430f554562614b5d00feff3946427be520dee629b76b6a9c2b1da6701c8ca628a69d6d40e20dd69d6e879d7a052d9c16f544b49738c7ff3cdd0613e9ed00ead7707702d1a6a0b88de1927a50c36beb78f4ff81e3dd97b706307596eebb363d418a891e1cb4589ce86ce81cdc0e1473d7a7dd5f6bb6e147c1f7c46fa879b4512c25704da6cdbb3c123a72e3585dc07b3e5cbe7fecf3a08426eee8c70ddc46ebf98b0bcb14a08c469cb5cfb6702acc0befd17640fa60244eca491280a95fbbc5833d26e4be70fcf798b55e06eb9fcb156942dcf108236f32a5a6c605687ba4f037eddbb1834dcbcd5293a0b66c621346ca5d893d239c26619b24c71f25cecc275e1ab24436ac01c80c0006fab2d95e82e3a0c3ea02d08ec5b24eb39205c49f4b549dcab7a88962336c4624716902f4e08f2b23cfd324f18405d66e9da3627ac34a6873ba2238386313af20d5a13bbd507fdc73015a17e3bd38fae1145f7f70d7cb8c5e1cdf9cf06d1246592a25d56ec2ae44cd7f75aa7f5f4a2b2ee49a41a26be4fab3f3f2ceb7b08510c5e2b7255326e4c417325b333cafe96dde1314a15dd6779a7d5a8a40622260041e936247eec8ec39ca29a1e18161db37497bdd4447a7d5ef3b8d22a2acd7f486b152bb66d3a15afc41dc9245a8d75e1d33704d4471e417ccc8d31645fdd647a2c191692675cf97664951d6ce98237d78b0962ad1433b5a3e49ddddbf57a391b14dcce00b4d7efe5cbb1e78f30d5ef53d66c381a45e275d2dcf6be559acb3c42494a9a2156eb8dcf03dd92b2ebaa697ea628fa0f75f125e4a7daa10f8dcf56ebaf7814557708c75580fad2bbb33e66ad7a4788a7aaac792aaae76138d7ff09df6a1a1920ddcf22e5e7007b15171b51ff81799355232ce39f7d5ceeaf704255d790041d6390a69f42816cba641ec81faa3d7c0fdec59dfe4ca41f31a692eaffc66b083995d86c575aea4514a3e09e8b3a1fa4d1591a2505f253ad0b6bfd9d87f063d2be414d3a427c0506a88ac5bdbef9b50d73bce876f85c196dca435e210e1d6713695b529ddda3350fb5065a6a8288abd265380917bac8ebbc7d5ced564587471dddf90c22ce6dbadea7e7a6723438d4cf6ac6dae27d033a8cadd77ab262e8defb33445ddb2056ec364c7629c33745e2338" + }, + "decrypt": { + "comment": "This section contains the internal values generated by intermediate nodes when decrypting their payload.", + "hops": [ + { + "alias": "Alice", + "onion": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337dadf610256c6ab518495dce9cdedf9391e21a71dada75be905267ba82f326c0513dda706908cfee834996700f881b2aed106585d61a2690de4ebe5d56ad2013b520af2a3c49316bc590ee83e8c31b1eb11ff766dad27ca993326b1ed582fb451a2ad87fbf6601134c6341c4a2deb6850e25a355be68dbb6923dc89444fdd74a0f700433b667bda345926099f5547b07e97ad903e8a01566a78ae177366239e793dac719de805565b6d0a1d290e273f705cfc56873f8b5e28225f7ded7a1d4ceffae63f91e477be8c917c786435976102a924ba4ba3de6150c829ce01c25428f2f5d05ef023be7d590ecdf6603730db3948f80ca1ed3d85227e64ef77200b9b557f427b6e1073cfa0e63e4485441768b98ab11ba8104a6cee1d7af7bb5ee9c05cf9cf4718901e92e09dfe5cb3af336a953072391c1e91fc2f4b92e124b38e0c6d17ef6ba7bbe93f02046975bb01b7f766fcfc5a755af11a90cc7eb3505986b56e07a7855534d03b79f0dfbfe645b0d6d4185c038771fd25b800aa26b2ed2e30b1e713659468618a2fea04fcd0473284598f76b11b0d159d343bc9711d3bea8d561547bcc8fff12317c0e7b1ee75bcb8082d762b6417f99d0f71ff7c060f6b564ad6827edaffa72eefcc4ce633a8da8d41c19d8f6aebd8878869eb518ccc16dccae6a94c690957598ce0295c1c46af5d7a2f0955b5400526bfd1430f554562614b5d00feff3946427be520dee629b76b6a9c2b1da6701c8ca628a69d6d40e20dd69d6e879d7a052d9c16f544b49738c7ff3cdd0613e9ed00ead7707702d1a6a0b88de1927a50c36beb78f4ff81e3dd97b706307596eebb363d418a891e1cb4589ce86ce81cdc0e1473d7a7dd5f6bb6e147c1f7c46fa879b4512c25704da6cdbb3c123a72e3585dc07b3e5cbe7fecf3a08426eee8c70ddc46ebf98b0bcb14a08c469cb5cfb6702acc0befd17640fa60244eca491280a95fbbc5833d26e4be70fcf798b55e06eb9fcb156942dcf108236f32a5a6c605687ba4f037eddbb1834dcbcd5293a0b66c621346ca5d893d239c26619b24c71f25cecc275e1ab24436ac01c80c0006fab2d95e82e3a0c3ea02d08ec5b24eb39205c49f4b549dcab7a88962336c4624716902f4e08f2b23cfd324f18405d66e9da3627ac34a6873ba2238386313af20d5a13bbd507fdc73015a17e3bd38fae1145f7f70d7cb8c5e1cdf9cf06d1246592a25d56ec2ae44cd7f75aa7f5f4a2b2ee49a41a26be4fab3f3f2ceb7b08510c5e2b7255326e4c417325b333cafe96dde1314a15dd6779a7d5a8a40622260041e936247eec8ec39ca29a1e18161db37497bdd4447a7d5ef3b8d22a2acd7f486b152bb66d3a15afc41dc9245a8d75e1d33704d4471e417ccc8d31645fdd647a2c191692675cf97664951d6ce98237d78b0962ad1433b5a3e49ddddbf57a391b14dcce00b4d7efe5cbb1e78f30d5ef53d66c381a45e275d2dcf6be559acb3c42494a9a2156eb8dcf03dd92b2ebaa697ea628fa0f75f125e4a7daa10f8dcf56ebaf7814557708c75580fad2bbb33e66ad7a4788a7aaac792aaae76138d7ff09df6a1a1920ddcf22e5e7007b15171b51ff81799355232ce39f7d5ceeaf704255d790041d6390a69f42816cba641ec81faa3d7c0fdec59dfe4ca41f31a692eaffc66b083995d86c575aea4514a3e09e8b3a1fa4d1591a2505f253ad0b6bfd9d87f063d2be414d3a427c0506a88ac5bdbef9b50d73bce876f85c196dca435e210e1d6713695b529ddda3350fb5065a6a8288abd265380917bac8ebbc7d5ced564587471dddf90c22ce6dbadea7e7a6723438d4cf6ac6dae27d033a8cadd77ab262e8defb33445ddb2056ec364c7629c33745e2338", + "node_privkey": "4141414141414141414141414141414141414141414141414141414141414141" + }, + { + "alias": "Bob", + "onion": "000280caa47c2a0ea677f6a77529e46caa04212153a8d5f829bee1e7339b17e2e2a9a3461d10472364a4ff12344beb6df96fb0c38ec47d1e956ddff5a665190fcca5ed02c3a3903fd8bbd4a4b95b197867c378b67b08f0624cfe80734ba512869c0fa22099beb1f6f1ea325b07ce7449736d7ffad79178b428d8ea2d7bc6578f12dbd788ef933f3b5ba352797c41f6786c3820c96726acf8bddf2cfa5d9c617d2b0bd5ab7b93f7964c98f44cf47db8422f47d11100236a29579f1cafcd38bd979814e1d2bf6d625edf50e1e21bfaf6268e3180dd7aafd3892da281c6dd53c1c366d0fdaf670b6ad84a38d6e8a3f4a80d132d686fd3b7443bc2250023bdb9303190f74c9220481cf99da30b5ec2bdb5a49028f5014e3eaeaa48429a0c78ebd3bb7c7d582c22b7d547cd269f0c4490373a81bf92687e73dac2075b4bda189ce0be225f5f510655e37a6e724a1415bede0a076b92a882cc2a82878ba67aaedf71454eb42b7f8638df8e21d5f708006e5112e2dc0a4afbcfed9f2c7959be812853ca8e313fbc99a0f38f1ee4479c96ccb836632b0808401db159bd2637f7a664013241e4664e994a0a9a3940115a702c60381e66d291e1ade1be2802e1226e311e3201a7c9682b6bc4354caff3d439adb1dfee53ad3fb3dd5e169d64796853bb323129f41213b166a7cac00f728c3e33bd7e59aa2ac0d1341cdb1532b507a0f446e51022a882ac16405442347b70f78c9b6e122f8e70096a4fae4c0405db5b869e0b7b59b09519c4dbf4d4980483906e837da0bee93f668ffaad37d6a4764211a02f95ad2dc2d942c198796741c20a3baf8efb5a53bd9c1a0148318d60a97d0013ab63269097ea295d62c1426d064f0b31c02e74a348ee0509998e701069f5a1e0c1086aed38d2ec87da69fb57a992d88ace3b4a16b0960f5a94936e2e684a9926cf4f911969a2a5d31fed0c7616d30197848253170e51274278873b11f3f5cc1b04b14aa5812524e4d86cbf08306c2aa671288324d7a009b2be533b1d7d0ce6defeeb630b86a9655f1e6424fcb559ed67457c115fba0d0719374802ea68fab299fd3f273be86fa3d2e7456020db2f47c6ec16c21ce6ec65de495e20af1941a5dcd65d910c1cb93f22e1318c173c645c81aed681c9704a8a541ac3d6ff604f46d0260468acbfec1b771b9eb8cd49a2124468dae786571895a569aae18438eaee6343ab2634823119fa2439634645d12e3b4a748b9cc0398b8416a834eb5d9e5cf619bbfaba4894d1c574c738caf530d0862f4cc75eb52bd3921d2d9edb09940edb1e3776423b0046d870ccdcc5d61f72e0440b97a93eeef21fb246a779d339be301a5971400749d6cc9911dfbf9de8ae86fac83c860fdd0e2bfa40af37c99d50e50fd6e5ae86597a201112ed404042b55e132f243dec481a2adc1d5e4b71e1efdea806ea900b2907ce877742d5ecf700ff3640f737863d0dd7207e462ee8d0e17d52047a88ae7446f419560d23968bf64957949e36953155b0ac2511c66be2890b4036329a21e132efb635297a64431899e0c351e50c6682c9b4d79b5d122466d02cd84f206369417d9c194a9349d3c631d72eb7857a9cd542906fc02ad6cdcf9bcf25ace3d826b6623fa5164351e14d3f0de5c8445a2ba3aae26595d0e31c3e307c1d56d4274f61f056145c1b8d6880872b9b10a8bfa4a923cad2edbcf5c50eba48936ed2bcc0be60eb721a74b46704aaae5ad24e2797852195dfacbb30a777d33b63d4dc4f35cfbe5e88fd1944c55a54fd53581446ea061ad29f4671da819ad7488c5dfc700f5f7a1b2af0d6a6e9d9ffc570a6d3209614ab4dc43728f3f0cd7eb4ce36ccd98936bbcbd32627384434bd01e9c0f93b2a5173fba184685e19b9af78afe876aa4e4b4242382b293133771d95a2bd83fa9c62", + "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "next_blinding": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" + }, + { + "alias": "Carol", + "onion": "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58dbd4bb7c2714870529410735f0951e72cbe981e2e167c0d8f3de33a36e39e78465aea2acad1e23c78b6fd342d63e37d214c912b4a0be344618f779138edc1b42a5ca3218ca2fea4be427f6cd0d387160db2bf6c2ba8e82941c8cf3626bd6bed7187f633012ef49df38f6b12963cb639e9eed1b9d269dcebcbd0b25287aa536ec85e7320b02e193122199a745ccbaaebd37f5d4b71f52f9b50feeb793eeef56924a046bc5e7003f6253e0284a8d3fe2e42c3564050f1e753cd32cc258ac0ffa6e05eecad5ba1286f78252e60dd884a65405ab673a85ba52adfa65c1086d4bb37ba2e0848adb2b04379775ad798492b14e8997f30ffa9cf5d432bdf5b246fce008fd876399beed827db58195f4f6192f6ff4ec63cb17fdcb497cb7aec26846a71dd8dca02fc3bb14dd7231a4d62a981bec54b71eb20331096dfa214a0ff4489ee96db663826ae8c850e9f06baa52a47b8eb576363f97e742aab2dc616acc6e74588e1d2ac16694febc90abaf5b1c684163c0e615a68d32633f01934adc8c6bf91fa3fd7aad033b7596d60402494e45e2c1632c40f7bfbd88a81a896a1d28ed6338c83e1eeaa467945d59998eb456c95f94bf1892e8f326ec2d5e0196b7073f106febc6ab8ca5bcc23f77ffc819bc1b5debce418ccc7d8391bbf33bceee6110beba170121bd99f54c956e64970bdab31227b03ee0ea3f01fbd9bd74015f6f82d04fab072e8f85f4370d09f41ee3e48eb959767bd989abb4eea42c4daa0437a7f747d7f9b70eb87b9f9b0b6f283b8205912601a432999b8869fd9fe5bad3572edac24da7184f9298f21ff60923db277264d29c846dd2f228f6fc53b6b60364237de64773f803f174ed10229c374f603ccc5fd3a62cb413ffe6f5630dc646bb33f231b2350537ec39e5d3f2fe1a1cb019ed0b18ad14019cad27afcca8ad70387ca110394c0432774f1aa1fa404b2e086c84a55388d3bd102501c78ef925cce89d76fa04c3f20f2d1f0ce507ac8b37b7913e3949ba12bbc5a4f6bac37c2415622d365bc8b83709a28e3d46f3850c89a3ff4d027fef6e3e4ce5c6c85f663c7eaec3c9730106fb82f53249a905533cfabee812aae51965b24b42f7ab471967bc8e73354e69141ee26a1f03684d5fb9c256a34de8257210e0390dd3962db521ae0a3bdab28300610ab2a634b699e5f092da5a061609ef6414bd805c8171f54ad6f285fb64ce0becca0b61188badcf8ef21190dad629e3fb3e89f55ebba829919540ebf5f8ae4283836d3c9133c1ca3365f6b9394916730411650686e0c2ab9c53b6cda9efdd5cfcb53ba9b6962bb6aa49d0a83a87460b60a9c7d2643ee99afe652883795f14014ec5df61b1e30c041c1fa6487f3c82f1ded5f83ffbef5017e197b7fb77be3b36e284a15e57d45bf9316dcaf97eb78ee4642b731ba05c5063bce1333fab4af6da97c80a96ee599b4df823efbedc250c0abba9783da7ddf2414b2a4774ff2880a7dc6791103e18b8631e39743cf9e87aed71700daa5dc72fdae520324741f92ea3d510ff555dea5e45f15cda87272d4559a12d4777680acb06993840e3c748da82c16cae556015fb2acd0335da11a3388575394048ab71199793ab706abc9d68add2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983577c1a0386c6d5d9a2223438e321b0fedfdee58fa452d57dc11a256834bb49ac9deeec88e4bf563c7340f44a240caec941c7e50f09cf", + "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "next_blinding": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "alias": "Dave", + "onion": "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b967c498efc246bdb8e060a6d1cf8dd0d4d732e33311fb96c9e9f1274005fa3d08b41704a1b7224c6300a7caead7baa0a8263eba2e0de6956ee8e4a1958264f47e4cf20d194eb576f5bd249ee4fece563f80fd76dc3eaca8f956188406d83195752b5c90c4b2a5e7ac3a8d5c62b17b551aff48ef6842a7e9326832c9a4a2fd415011150a9e71beb901fd9747bac8add1c694b612730dc86b5b19a0bbbc675947a953316e3303d7b30c182f94def9206671edac9a3ec3e52d28fc28247a1c73ab751bf61c82c3950f617e758f79bd0ba294defb20466eaf1e801462046baad3aec3e5b8868a7b037f23d73a47a7e74c77107334f37388cff863e452820c61d89728fa75c84bc7cdfc06dcdd1911f5f803353926d073efd65251380e174913aae03318ea5b6f0ec83998c55ab99bef62803ea2da9f6d1ea892b90efc4f8ffb685a5201a781da2e6ac5923645638c9709ae32171a00c0cd3d8c7eedfb06b4eedc7d3e566987e2e3805a038f21d78ded5d6c7137a5e8e592f3180ee4d5f4e1289176f67fc38690d0958bc82e240b72b10577f340f1e14b8633f0b6d9729ff4618be2a972400a015a871ba33be70335f652a8d70f2bd32421d6ac2af781d667dad787d6aef4505a15d046579e46eebe757444cffca6d0610f0dd36a7ce57af969bd0c3f7006298ef406a25f689daf58f875d44d2423ebf195b503f11c37c506ea6abe50a463f7bb5e9b964604d832724de768513f6b38bf71715e1feea8a6e86797788d487146891564919da1372016ed8f08c7fcbff66a4a65a3d0fcd8e3daac6eba41f5d65ef2d8075364a9e78b3a273549f6eac4abb72e912e237990367e0d43e89945f8ac3907be5a6c662139485a50cb5ce3f0ba08586c39f6c515368ec3f91b72295f1b7a73a9df322ae9a45d363d6c616be3300083764cbdee31221f25a318f095feacb09f957c96db30fccca47a0215b576c3ed925a0bad05d6400abe318c11f36628c387a4ee38832182cd44b3cd48e5422c1f1e3b57218dfe72c611f5415127720e60f6e2400607e61841b76de1704bcbeb0daf1377ccb2253916de2b6d490bb71ba0a44fea2e94f2423d723934557d5905e01b2b80232a884e258d46dc92ea11e0818d0ece5b914f02049866e151801ab8c9aea155479b354dc91151fb9ba43277458f9760dd859faaa139e3b9ab36a1dbc36a93ef2c90598b20cb30ef3c4f23a2d6178b4d1da668fb328a25d84d30a132d9f2a6a988cbe2e5c2be01cb6db4b4725a50d6cdacf5fb083e7d650a25bec1407fbc047d26076c7596429a29606ad527e97ef0824ad6c1b05831a3e5b71c63a528918a3301cdd4061fc1fcce3da601961f2602a2b002ac8404125c2d52666263858a923e197efcda873c32d86897352e4f2264ad6a1b48acc0fe78ff55cb442cb2bb5fa2880810e1d00aa0247057fb80b7ed36cf9647af41b44ee4a63ee2d6f652526404572520a7d2d9dcde4e62df0c3be89f8471550594cdd16a51a9cacc58729c092c68506162fe65edc2314055d389f724ced189d826a546b5c4d08a43d977b3cf033de5760b71a7cc38ee5851592031aafb467a89b3b6c7ed67b15d44c48d6baedce3e95e08ec7c55038f3eba90ccb900895734f0fb7efe54961ce493369cc56416898a9bed7c2482871c15a7f1eb5ed17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bca955510e2126b3bb989a4ac21cf948f965e48bc363d2997437797b4f770e8b65", + "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "next_blinding": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" + }, + { + "alias": "Eve", + "onion": "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130adfd06eb26201baee8866af2d1b7a7ab26595349dad002af0590193aaa8f400ab394f5994ec831aeeecb64421c566e3556cbdd7e7e50deb1fc49fd5e007308ab6494415514abff978899623f9b6065ca1e243bb78170118e8b8c8b53b750b59cc1ec017d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f25e386202a4b6edff2a71e700229df7230c8ca31bd5588f04799e9640c9c20a47cba713f3cc5ad3202e14bb520880f2a8409d8e7835cae21b48a651c2d47fe6af785889ab98f1416f6e4ad67a66ae681e9a8828bad3f9b6890221c4a7ec80531d6b63eb30843f613ce644795bc8bcee60e8f7b36f3fd04de762f103c52efaf36a2f3bbbaac482d6271dc4180c10bcc076c04d06ea7fd8fb6a647e0e10523b05da2d89e4139fb55c2315cd01bdcbd57587fef8442d7ff5620630fd2d2e79739d90be811bf2cba60415d6cba2cea14ba1859f3122cd905c4e12e3e2a1ab6fab54b2ec40e434626e2d3c3195c02c82a8bd64d226c2328ac72ca12197d9908eaf54333717448ce6ed73adc0ac05e2ee1d735131d87918beb8995993dc8f63fe10f2c8eba2be7ab8bb44d9f78f59ef3e4c180bd75e4eef2381450c6f0480d543997305f1d07815993b5aca8d88d474966d9abec93bb069a16aa2da75b87f94576e01d08a17d3e0e3d0370f010733a7d7affb12cdf94c259a62607fce71003535c4727305de5ff7bba3840922844b3a45f62c29715fccf440517ef121450f6962396fba9b07036d085582405dcae6ee95964b66bc7c85b8d02d90091500db3cebf6de584f86b7b55335a8c9aa26381b00747f055cc458a2cadfccf9c29702bf941447beaca6583cca09492a57d4b03b2ca00dbaf41dfd6a9b249381626a7debe475735a7e39e77a363eccf14669046f656cc09ad448da8d8b545e6a604f46dc481786d09a94c63cf23f49ba367d2929466364dbce2a8ffce3dadf8f4cef8a56e1fefa1a3304a953fe83018e57d8a95694b02d994fea2630a9a3d5f1e2f6d6142d503ec4152871f7122d7e566a03261f554639e7a759e0e73846f71d5cace37d91336fc9ca9396bf64ca2cf45fa2db779b3b5c63b04f1c0c1fb79fdfcf5a82b0202df934ae1720a7ce1e047cbec3f82737b50168c974f4623cacce87e3f5bd5232caca7956d28ffedcf11ac5998662c5f6b13c6126584ca2e894d3fcbad4d130bbe22e88a135e0020cdd43853e0b3af3800e9544854d211e873cf68ab683578d501d69ec5dc7fce42ac436d58243880c1b88227b0681c6c9dd8a8ad0793202b15ab63b787b748e258da3e68d0e649fc4ac081a71de8adbc891c113d5f722686b6ac4ed9e3cc247bc4a4643416f480627e9de20f7307f434a499f5c6951c2e8b3ff51d455bf65ceb5ee3dee47b968ac2642e13d8a68f903b73627c2e75788fecca5836371a908eea4f1ea44db2315bc185f77e478efeaaa4da2da13fe7aeaa79ed1d04876a8b2b7b333c5de8c4c9a50274c2eb7b9bd2a3630c57173174781fc9785235f830cefa1c82080eaffdef257f18eedc9ddfd25a696a11a3dc56cd836be72f5f4a2cbb6316d5d3b1ad91a7ec7d877f28d2c29a5525b0b24362699281b0e3b48f38caf1085045fe9089f9e6fb29e4b47aa4cecf68c9bf72073469bd9beeea5e88bfe554cb6a81231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9cbe9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd879fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f1232950cef398ad3e057a5db964bd3e5c8a5717b30b41601a4f11ad63afe404cb6f1e8ea5fd7a8e085b65ca5136146febf4d47928dcc9a9e0", + "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", + "next_blinding": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" + } + ] + } +} \ No newline at end of file From a800c9f9c55f8e01c1537e481eec685578bfb141 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 20 Dec 2022 11:23:58 +0200 Subject: [PATCH 8/9] sphinx: add route blinding helper methods to Router In this commit, DecryptBlindHopData and NextEphemeral methods are added to Router which makes use of the Router's onion key in order to unblind blinded data and compute the next ephermal key respectively. --- sphinx.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sphinx.go b/sphinx.go index a262c46..501c061 100644 --- a/sphinx.go +++ b/sphinx.go @@ -611,6 +611,22 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, return processOnionPacket(onionPkt, &sharedSecret, assocData) } +// DecryptBlindedHopData uses the router's private key to decrypt data encrypted +// by the creator of the blinded route. +func (r *Router) DecryptBlindedHopData(ephemPub *btcec.PublicKey, + encryptedData []byte) ([]byte, error) { + + return decryptBlindedHopData(r.onionKey, ephemPub, encryptedData) +} + +// NextEphemeral computes the next ephemeral key given the current ephemeral +// key and the router's private key. +func (r *Router) NextEphemeral(ephemPub *btcec.PublicKey) (*btcec.PublicKey, + error) { + + return NextEphemeral(r.onionKey, ephemPub) +} + // unwrapPacket wraps a layer of the passed onion packet using the specified // shared secret and associated data. The associated data will be used to check // the HMAC at each hop to ensure the same data is passed along with the onion From f971c75afd4ae677212947874707915520247f09 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 7 May 2023 13:44:44 +0200 Subject: [PATCH 9/9] cmd: add blinded path helper commands Add a blinded-key option to the parse command so that it can be used to test parsing of an onion for hops in a blinded route. Also add a helper nextephemeral command. --- cmd/example-data/onion-blinded.json | 6 ++ cmd/example-data/onion-blinded2.json | 6 ++ cmd/example-data/onion-blinded3.json | 6 ++ cmd/main.go | 88 ++++++++++++++++++++++++---- 4 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 cmd/example-data/onion-blinded.json create mode 100644 cmd/example-data/onion-blinded2.json create mode 100644 cmd/example-data/onion-blinded3.json diff --git a/cmd/example-data/onion-blinded.json b/cmd/example-data/onion-blinded.json new file mode 100644 index 0000000..9db81a6 --- /dev/null +++ b/cmd/example-data/onion-blinded.json @@ -0,0 +1,6 @@ +{ + "session_key": "4343434343434343434343434343434343434343434343434343434343434343", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinding_point": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "onion": "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58dbd4bb7c2714870529410735f0951e72cbe981e2e167c0d8f3de33a36e39e78465aea2acad1e23c78b6fd342d63e37d214c912b4a0be344618f779138edc1b42a5ca3218ca2fea4be427f6cd0d387160db2bf6c2ba8e82941c8cf3626bd6bed7187f633012ef49df38f6b12963cb639e9eed1b9d269dcebcbd0b25287aa536ec85e7320b02e193122199a745ccbaaebd37f5d4b71f52f9b50feeb793eeef56924a046bc5e7003f6253e0284a8d3fe2e42c3564050f1e753cd32cc258ac0ffa6e05eecad5ba1286f78252e60dd884a65405ab673a85ba52adfa65c1086d4bb37ba2e0848adb2b04379775ad798492b14e8997f30ffa9cf5d432bdf5b246fce008fd876399beed827db58195f4f6192f6ff4ec63cb17fdcb497cb7aec26846a71dd8dca02fc3bb14dd7231a4d62a981bec54b71eb20331096dfa214a0ff4489ee96db663826ae8c850e9f06baa52a47b8eb576363f97e742aab2dc616acc6e74588e1d2ac16694febc90abaf5b1c684163c0e615a68d32633f01934adc8c6bf91fa3fd7aad033b7596d60402494e45e2c1632c40f7bfbd88a81a896a1d28ed6338c83e1eeaa467945d59998eb456c95f94bf1892e8f326ec2d5e0196b7073f106febc6ab8ca5bcc23f77ffc819bc1b5debce418ccc7d8391bbf33bceee6110beba170121bd99f54c956e64970bdab31227b03ee0ea3f01fbd9bd74015f6f82d04fab072e8f85f4370d09f41ee3e48eb959767bd989abb4eea42c4daa0437a7f747d7f9b70eb87b9f9b0b6f283b8205912601a432999b8869fd9fe5bad3572edac24da7184f9298f21ff60923db277264d29c846dd2f228f6fc53b6b60364237de64773f803f174ed10229c374f603ccc5fd3a62cb413ffe6f5630dc646bb33f231b2350537ec39e5d3f2fe1a1cb019ed0b18ad14019cad27afcca8ad70387ca110394c0432774f1aa1fa404b2e086c84a55388d3bd102501c78ef925cce89d76fa04c3f20f2d1f0ce507ac8b37b7913e3949ba12bbc5a4f6bac37c2415622d365bc8b83709a28e3d46f3850c89a3ff4d027fef6e3e4ce5c6c85f663c7eaec3c9730106fb82f53249a905533cfabee812aae51965b24b42f7ab471967bc8e73354e69141ee26a1f03684d5fb9c256a34de8257210e0390dd3962db521ae0a3bdab28300610ab2a634b699e5f092da5a061609ef6414bd805c8171f54ad6f285fb64ce0becca0b61188badcf8ef21190dad629e3fb3e89f55ebba829919540ebf5f8ae4283836d3c9133c1ca3365f6b9394916730411650686e0c2ab9c53b6cda9efdd5cfcb53ba9b6962bb6aa49d0a83a87460b60a9c7d2643ee99afe652883795f14014ec5df61b1e30c041c1fa6487f3c82f1ded5f83ffbef5017e197b7fb77be3b36e284a15e57d45bf9316dcaf97eb78ee4642b731ba05c5063bce1333fab4af6da97c80a96ee599b4df823efbedc250c0abba9783da7ddf2414b2a4774ff2880a7dc6791103e18b8631e39743cf9e87aed71700daa5dc72fdae520324741f92ea3d510ff555dea5e45f15cda87272d4559a12d4777680acb06993840e3c748da82c16cae556015fb2acd0335da11a3388575394048ab71199793ab706abc9d68add2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983577c1a0386c6d5d9a2223438e321b0fedfdee58fa452d57dc11a256834bb49ac9deeec88e4bf563c7340f44a240caec941c7e50f09cf" +} diff --git a/cmd/example-data/onion-blinded2.json b/cmd/example-data/onion-blinded2.json new file mode 100644 index 0000000..29e1025 --- /dev/null +++ b/cmd/example-data/onion-blinded2.json @@ -0,0 +1,6 @@ +{ + "session_key": "4444444444444444444444444444444444444444444444444444444444444444", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinding_point": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "onion": "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b967c498efc246bdb8e060a6d1cf8dd0d4d732e33311fb96c9e9f1274005fa3d08b41704a1b7224c6300a7caead7baa0a8263eba2e0de6956ee8e4a1958264f47e4cf20d194eb576f5bd249ee4fece563f80fd76dc3eaca8f956188406d83195752b5c90c4b2a5e7ac3a8d5c62b17b551aff48ef6842a7e9326832c9a4a2fd415011150a9e71beb901fd9747bac8add1c694b612730dc86b5b19a0bbbc675947a953316e3303d7b30c182f94def9206671edac9a3ec3e52d28fc28247a1c73ab751bf61c82c3950f617e758f79bd0ba294defb20466eaf1e801462046baad3aec3e5b8868a7b037f23d73a47a7e74c77107334f37388cff863e452820c61d89728fa75c84bc7cdfc06dcdd1911f5f803353926d073efd65251380e174913aae03318ea5b6f0ec83998c55ab99bef62803ea2da9f6d1ea892b90efc4f8ffb685a5201a781da2e6ac5923645638c9709ae32171a00c0cd3d8c7eedfb06b4eedc7d3e566987e2e3805a038f21d78ded5d6c7137a5e8e592f3180ee4d5f4e1289176f67fc38690d0958bc82e240b72b10577f340f1e14b8633f0b6d9729ff4618be2a972400a015a871ba33be70335f652a8d70f2bd32421d6ac2af781d667dad787d6aef4505a15d046579e46eebe757444cffca6d0610f0dd36a7ce57af969bd0c3f7006298ef406a25f689daf58f875d44d2423ebf195b503f11c37c506ea6abe50a463f7bb5e9b964604d832724de768513f6b38bf71715e1feea8a6e86797788d487146891564919da1372016ed8f08c7fcbff66a4a65a3d0fcd8e3daac6eba41f5d65ef2d8075364a9e78b3a273549f6eac4abb72e912e237990367e0d43e89945f8ac3907be5a6c662139485a50cb5ce3f0ba08586c39f6c515368ec3f91b72295f1b7a73a9df322ae9a45d363d6c616be3300083764cbdee31221f25a318f095feacb09f957c96db30fccca47a0215b576c3ed925a0bad05d6400abe318c11f36628c387a4ee38832182cd44b3cd48e5422c1f1e3b57218dfe72c611f5415127720e60f6e2400607e61841b76de1704bcbeb0daf1377ccb2253916de2b6d490bb71ba0a44fea2e94f2423d723934557d5905e01b2b80232a884e258d46dc92ea11e0818d0ece5b914f02049866e151801ab8c9aea155479b354dc91151fb9ba43277458f9760dd859faaa139e3b9ab36a1dbc36a93ef2c90598b20cb30ef3c4f23a2d6178b4d1da668fb328a25d84d30a132d9f2a6a988cbe2e5c2be01cb6db4b4725a50d6cdacf5fb083e7d650a25bec1407fbc047d26076c7596429a29606ad527e97ef0824ad6c1b05831a3e5b71c63a528918a3301cdd4061fc1fcce3da601961f2602a2b002ac8404125c2d52666263858a923e197efcda873c32d86897352e4f2264ad6a1b48acc0fe78ff55cb442cb2bb5fa2880810e1d00aa0247057fb80b7ed36cf9647af41b44ee4a63ee2d6f652526404572520a7d2d9dcde4e62df0c3be89f8471550594cdd16a51a9cacc58729c092c68506162fe65edc2314055d389f724ced189d826a546b5c4d08a43d977b3cf033de5760b71a7cc38ee5851592031aafb467a89b3b6c7ed67b15d44c48d6baedce3e95e08ec7c55038f3eba90ccb900895734f0fb7efe54961ce493369cc56416898a9bed7c2482871c15a7f1eb5ed17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bca955510e2126b3bb989a4ac21cf948f965e48bc363d2997437797b4f770e8b65" +} diff --git a/cmd/example-data/onion-blinded3.json b/cmd/example-data/onion-blinded3.json new file mode 100644 index 0000000..5693440 --- /dev/null +++ b/cmd/example-data/onion-blinded3.json @@ -0,0 +1,6 @@ +{ + "session_key": "4545454545454545454545454545454545454545454545454545454545454545", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinding_point": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "onion": "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130adfd06eb26201baee8866af2d1b7a7ab26595349dad002af0590193aaa8f400ab394f5994ec831aeeecb64421c566e3556cbdd7e7e50deb1fc49fd5e007308ab6494415514abff978899623f9b6065ca1e243bb78170118e8b8c8b53b750b59cc1ec017d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f25e386202a4b6edff2a71e700229df7230c8ca31bd5588f04799e9640c9c20a47cba713f3cc5ad3202e14bb520880f2a8409d8e7835cae21b48a651c2d47fe6af785889ab98f1416f6e4ad67a66ae681e9a8828bad3f9b6890221c4a7ec80531d6b63eb30843f613ce644795bc8bcee60e8f7b36f3fd04de762f103c52efaf36a2f3bbbaac482d6271dc4180c10bcc076c04d06ea7fd8fb6a647e0e10523b05da2d89e4139fb55c2315cd01bdcbd57587fef8442d7ff5620630fd2d2e79739d90be811bf2cba60415d6cba2cea14ba1859f3122cd905c4e12e3e2a1ab6fab54b2ec40e434626e2d3c3195c02c82a8bd64d226c2328ac72ca12197d9908eaf54333717448ce6ed73adc0ac05e2ee1d735131d87918beb8995993dc8f63fe10f2c8eba2be7ab8bb44d9f78f59ef3e4c180bd75e4eef2381450c6f0480d543997305f1d07815993b5aca8d88d474966d9abec93bb069a16aa2da75b87f94576e01d08a17d3e0e3d0370f010733a7d7affb12cdf94c259a62607fce71003535c4727305de5ff7bba3840922844b3a45f62c29715fccf440517ef121450f6962396fba9b07036d085582405dcae6ee95964b66bc7c85b8d02d90091500db3cebf6de584f86b7b55335a8c9aa26381b00747f055cc458a2cadfccf9c29702bf941447beaca6583cca09492a57d4b03b2ca00dbaf41dfd6a9b249381626a7debe475735a7e39e77a363eccf14669046f656cc09ad448da8d8b545e6a604f46dc481786d09a94c63cf23f49ba367d2929466364dbce2a8ffce3dadf8f4cef8a56e1fefa1a3304a953fe83018e57d8a95694b02d994fea2630a9a3d5f1e2f6d6142d503ec4152871f7122d7e566a03261f554639e7a759e0e73846f71d5cace37d91336fc9ca9396bf64ca2cf45fa2db779b3b5c63b04f1c0c1fb79fdfcf5a82b0202df934ae1720a7ce1e047cbec3f82737b50168c974f4623cacce87e3f5bd5232caca7956d28ffedcf11ac5998662c5f6b13c6126584ca2e894d3fcbad4d130bbe22e88a135e0020cdd43853e0b3af3800e9544854d211e873cf68ab683578d501d69ec5dc7fce42ac436d58243880c1b88227b0681c6c9dd8a8ad0793202b15ab63b787b748e258da3e68d0e649fc4ac081a71de8adbc891c113d5f722686b6ac4ed9e3cc247bc4a4643416f480627e9de20f7307f434a499f5c6951c2e8b3ff51d455bf65ceb5ee3dee47b968ac2642e13d8a68f903b73627c2e75788fecca5836371a908eea4f1ea44db2315bc185f77e478efeaaa4da2da13fe7aeaa79ed1d04876a8b2b7b333c5de8c4c9a50274c2eb7b9bd2a3630c57173174781fc9785235f830cefa1c82080eaffdef257f18eedc9ddfd25a696a11a3dc56cd836be72f5f4a2cbb6316d5d3b1ad91a7ec7d877f28d2c29a5525b0b24362699281b0e3b48f38caf1085045fe9089f9e6fb29e4b47aa4cecf68c9bf72073469bd9beeea5e88bfe554cb6a81231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9cbe9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd879fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f1232950cef398ad3e057a5db964bd3e5c8a5717b30b41601a4f11ad63afe404cb6f1e8ea5fd7a8e085b65ca5136146febf4d47928dcc9a9e0" +} diff --git a/cmd/main.go b/cmd/main.go index b78ec30..8d17c30 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,6 +42,23 @@ func main() { }, }, }, + { + Name: "nextephemeral", + Usage: "A helper to compute the next ephemeral key " + + "given the current ephemeral key and a " + + "private key", + Action: nextEphemeral, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "priv", + Required: true, + }, + cli.StringFlag{ + Name: "pub", + Required: true, + }, + }, + }, { Name: "generate", Usage: "Build a new onion.", @@ -207,44 +224,58 @@ func generate(ctx *cli.Context) error { type onionInfo struct { SessionKey string `json:"session_key"` AssociatedData string `json:"associated_data"` + BlindingPoint string `json:"blinding_point"` Onion string `json:"onion"` } func parseOnionInfo(info *onionInfo) (*sphinx.OnionPacket, *btcec.PrivateKey, - []byte, error) { + []byte, *btcec.PublicKey, error) { sessionKeyBytes, err := hex.DecodeString(info.SessionKey) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to decode the "+ + return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ "sessionKey %v: %v", info.SessionKey, err) } if len(sessionKeyBytes) != 32 { - return nil, nil, nil, fmt.Errorf("session priv key must be " + - "32 bytes long") + return nil, nil, nil, nil, fmt.Errorf("session priv key must " + + "be 32 bytes long") } sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) assocData, err := hex.DecodeString(info.AssociatedData) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to decode the "+ + return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ "associate data %v: %v", info.AssociatedData, err) } onion, err := hex.DecodeString(info.Onion) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to decode the "+ + return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ "onion %v: %v", info.Onion, err) } var packet sphinx.OnionPacket err = packet.Decode(bytes.NewBuffer(onion)) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } - return &packet, sessionKey, assocData, nil + var blindingPoint *btcec.PublicKey + if info.BlindingPoint != "" { + bpBytes, err := hex.DecodeString(info.BlindingPoint) + if err != nil { + return nil, nil, nil, nil, err + } + + blindingPoint, err = btcec.ParsePubKey(bpBytes) + if err != nil { + return nil, nil, nil, nil, err + } + } + + return &packet, sessionKey, assocData, blindingPoint, nil } func peel(ctx *cli.Context) error { @@ -260,7 +291,9 @@ func peel(ctx *cli.Context) error { return err } - packet, sessionKey, assocData, err := parseOnionInfo(&info) + packet, sessionKey, assocData, blindingPoint, err := parseOnionInfo( + &info, + ) if err != nil { return err } @@ -272,7 +305,9 @@ func peel(ctx *cli.Context) error { s.Start() defer s.Stop() - p, err := s.ProcessOnionPacket(packet, assocData, 10) + p, err := s.ProcessOnionPacket( + packet, assocData, 10, sphinx.WithBlindingPoint(blindingPoint), + ) if err != nil { return err } @@ -286,3 +321,36 @@ func peel(ctx *cli.Context) error { return nil } + +func nextEphemeral(ctx *cli.Context) error { + privKeyByte, err := hex.DecodeString(ctx.String("priv")) + if err != nil { + return err + } + if len(privKeyByte) != 32 { + return fmt.Errorf("private key must be 32 bytes") + } + + privKey, _ := btcec.PrivKeyFromBytes(privKeyByte) + + pubKeyBytes, err := hex.DecodeString(ctx.String("pub")) + if err != nil { + return err + } + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + if err != nil { + return err + } + + nextBlindedKey, err := sphinx.NextEphemeral( + &sphinx.PrivKeyECDH{PrivKey: privKey}, pubKey, + ) + if err != nil { + return err + } + + fmt.Printf("%x\n", nextBlindedKey.SerializeCompressed()) + + return nil +}