├── .gitignore ├── .travis.yml ├── tools ├── Dockerfile └── go.mod ├── go.mod ├── LICENSE ├── .gemini ├── config.yaml └── styleguide.md ├── log.go ├── cmd ├── example-data │ ├── hop-data.json │ ├── onion.json │ ├── onion2.json │ ├── onion3.json │ ├── onion4.json │ ├── onion5.json │ ├── onion-blinded.json │ ├── onion-blinded2.json │ └── onion-blinded3.json └── main.go ├── .github └── workflows │ └── main.yml ├── scripts ├── check-go-version-dockerfile.sh └── check-go-version-yaml.sh ├── bench_test.go ├── replay_set.go ├── error.go ├── packetfiller.go ├── varint.go ├── README.md ├── obfuscation.go ├── batch.go ├── replaylog_test.go ├── hornet.go ├── go.sum ├── testdata ├── onion-test-multi-frame.json ├── onion-test.json ├── route-blinding-test.json └── blinded-onion-message-onion-test.json ├── Makefile ├── .golangci.yml ├── replaylog.go ├── path.go ├── payload.go ├── crypto.go └── obfuscation_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea 3 | .aider* 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | cache: 3 | directories: 4 | - $GOCACHE 5 | - $GOPATH/pkg/mod 6 | - $GOPATH/src/github.com/btcsuite 7 | - $GOPATH/src/github.com/golang 8 | 9 | go: 10 | - "1.21.x" 11 | 12 | sudo: required 13 | 14 | script: 15 | - export PATH=$PATH:$HOME/gopath/bin 16 | - export GO111MODULE=on 17 | - go test -v 18 | -------------------------------------------------------------------------------- /tools/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.6-alpine 2 | 3 | ENV GOFLAGS="-buildvcs=false" 4 | 5 | RUN apk update && apk add --no-cache git 6 | 7 | WORKDIR /build 8 | 9 | COPY tools/go.mod tools/go.sum ./ 10 | 11 | RUN go install -trimpath \ 12 | github.com/golangci/golangci-lint/v2/cmd/golangci-lint \ 13 | github.com/rinchsan/gosimports/cmd/gosimports \ 14 | && rm -rf /go/pkg/mod \ 15 | && rm -rf /root/.cache/go-build \ 16 | && rm -rf /go/src \ 17 | && rm -rf /tmp/* 18 | 19 | RUN chmod -R 777 /build \ 20 | && git config --global --add safe.directory /build 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lightningnetwork/lightning-onion 2 | 3 | require ( 4 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da 5 | github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 6 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 7 | github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 10 | github.com/stretchr/testify v1.10.0 11 | github.com/urfave/cli v1.22.9 12 | golang.org/x/crypto v0.33.0 13 | ) 14 | 15 | require ( 16 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect 17 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 18 | github.com/kr/pretty v0.3.1 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/rogpeppe/go-internal v1.13.1 // indirect 21 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 22 | golang.org/x/sys v0.30.0 // indirect 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | 27 | go 1.24.6 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2016 The Lightning Network Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.gemini/config.yaml: -------------------------------------------------------------------------------- 1 | # Config for the Gemini Pull Request Review Bot. 2 | # https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github 3 | 4 | # Enables fun features such as a poem in the initial pull request summary. 5 | # Type: boolean, default: false. 6 | have_fun: false 7 | 8 | code_review: 9 | # Disables Gemini from acting on PRs. 10 | # Type: boolean, default: false. 11 | disable: false 12 | 13 | # Minimum severity of comments to post (LOW, MEDIUM, HIGH, CRITICAL). 14 | # Type: string, default: MEDIUM. 15 | comment_severity_threshold: MEDIUM 16 | 17 | # Max number of review comments (-1 for unlimited). 18 | # Type: integer, default: -1. 19 | max_review_comments: -1 20 | 21 | pull_request_opened: 22 | # Post helpful instructions when PR is opened. 23 | # Type: boolean, default: false. 24 | help: false 25 | 26 | # Post PR summary when opened. 27 | # Type boolean, default: true. 28 | summary: true 29 | 30 | # Post code review on PR open. 31 | # Type boolean, default: true. 32 | code_review: true 33 | 34 | # List of glob patterns to ignore (files and directories). 35 | # Type: array of string, default: []. 36 | ignore_patterns: [] 37 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import "github.com/btcsuite/btclog" 4 | 5 | // sphxLog is a logger that is initialized with no output filters. This 6 | // means the package will not perform any logging by default until the caller 7 | // requests it. 8 | var sphxLog btclog.Logger 9 | 10 | // The default amount of logging is none. 11 | func init() { 12 | DisableLog() 13 | } 14 | 15 | // DisableLog disables all library log output. Logging output is disabled 16 | // by default until UseLogger is called. 17 | func DisableLog() { 18 | sphxLog = btclog.Disabled 19 | } 20 | 21 | // UseLogger uses a specified Logger to output package logging info. 22 | // This should be used in preference to SetLogWriter if the caller is also 23 | // using btclog. 24 | func UseLogger(logger btclog.Logger) { 25 | sphxLog = logger 26 | } 27 | 28 | // logClosure is used to provide a closure over expensive logging operations 29 | // so don't have to be performed when the logging level doesn't warrant it. 30 | type logClosure func() string 31 | 32 | // String invokes the underlying function and returns the result. 33 | func (c logClosure) String() string { 34 | return c() 35 | } 36 | 37 | // newLogClosure returns a new closure over a function that returns a string 38 | // which itself provides a Stringer interface so that it can be used with the 39 | // logging system. 40 | func newLogClosure(c func() string) logClosure { 41 | return logClosure(c) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/example-data/hop-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4141414141414141414141414141414141414141414141414141414141414141", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "hops": [ 5 | { 6 | "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", 7 | "payload": "1202023a98040205dc06080000000000000001" 8 | }, 9 | { 10 | "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 11 | "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" 12 | }, 13 | { 14 | "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", 15 | "payload": "12020230d4040204e206080000000000000003" 16 | }, 17 | { 18 | "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", 19 | "payload": "1202022710040203e806080000000000000004" 20 | }, 21 | { 22 | "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", 23 | "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: '1.24.6' 24 | 25 | - name: Check formatting 26 | run: make fmt-check 27 | 28 | - name: Run linter 29 | run: make lint 30 | 31 | build: 32 | name: Build 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v4 38 | 39 | - name: Set up Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version: '1.24.6' 43 | 44 | - name: Build 45 | run: make build 46 | 47 | - name: Build sphinx-cli 48 | run: make sphinx-cli 49 | 50 | unit-tests: 51 | name: Unit Tests 52 | runs-on: ubuntu-latest 53 | strategy: 54 | matrix: 55 | unit_type: 56 | - unit 57 | - unit-cover 58 | 59 | steps: 60 | - name: Checkout code 61 | uses: actions/checkout@v4 62 | 63 | - name: Set up Go 64 | uses: actions/setup-go@v5 65 | with: 66 | go-version: '1.24.6' 67 | 68 | - name: Run unit tests 69 | run: make ${{ matrix.unit_type }} 70 | 71 | - name: Send coverage 72 | uses: coverallsapp/github-action@v2 73 | if: matrix.unit_type == 'unit-cover' 74 | continue-on-error: true 75 | with: 76 | github-token: ${{ secrets.GITHUB_TOKEN }} 77 | file: coverage.txt 78 | format: golang 79 | -------------------------------------------------------------------------------- /scripts/check-go-version-dockerfile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if the Dockerfile contains only the specified Go version. 4 | check_go_version() { 5 | local dockerfile="$1" 6 | local required_go_version="$2" 7 | 8 | # Use grep to find lines with 'FROM golang:' 9 | local go_lines=$(grep -i '^FROM golang:' "$dockerfile") 10 | 11 | # Check if all lines have the required Go version. 12 | if [ -z "$go_lines" ]; then 13 | # No Go version found in the file. Skip the check. 14 | return 15 | elif echo "$go_lines" | grep -q -v "$required_go_version"; then 16 | echo "$go_lines" 17 | echo "Error: $dockerfile does not use Go version $required_go_version exclusively." 18 | exit 1 19 | else 20 | echo "$dockerfile is using Go version $required_go_version." 21 | fi 22 | } 23 | 24 | # Check if the target Go version argument is provided. 25 | if [ $# -eq 0 ]; then 26 | echo "Usage: $0 " 27 | exit 1 28 | fi 29 | 30 | target_go_version="$1" 31 | 32 | # File paths to be excluded from the check. 33 | exception_list=( 34 | # Exclude the tools Dockerfile as otherwise the linter may need to be 35 | # considered every time the Go version is updated. 36 | "./tools/Dockerfile" 37 | ) 38 | 39 | # is_exception checks if a file is in the exception list. 40 | is_exception() { 41 | local file="$1" 42 | for exception in "${exception_list[@]}"; do 43 | if [ "$file" == "$exception" ]; then 44 | return 0 45 | fi 46 | done 47 | return 1 48 | } 49 | 50 | # Search for Dockerfiles in the current directory and its subdirectories. 51 | dockerfiles=$(find . -type f -name "*.Dockerfile" -o -name "Dockerfile") 52 | 53 | # Check each Dockerfile 54 | for file in $dockerfiles; do 55 | # Skip the file if it is in the exception list. 56 | if is_exception "$file"; then 57 | echo "Skipping $file" 58 | continue 59 | fi 60 | 61 | check_go_version "$file" "$target_go_version" 62 | done 63 | 64 | echo "All Dockerfiles pass the Go version check for Go version $target_go_version." 65 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | ) 9 | 10 | var ( 11 | s *OnionPacket 12 | p *ProcessedPacket 13 | ) 14 | 15 | func BenchmarkPathPacketConstruction(b *testing.B) { 16 | b.StopTimer() 17 | 18 | var ( 19 | err error 20 | sphinxPacket *OnionPacket 21 | route PaymentPath 22 | ) 23 | 24 | for i := 0; i < 20; i++ { 25 | privKey, err := btcec.NewPrivateKey() 26 | if err != nil { 27 | b.Fatalf("unable to generate key: %v", privKey) 28 | } 29 | 30 | hopData := HopData{ 31 | ForwardAmount: uint64(i), 32 | OutgoingCltv: uint32(i), 33 | } 34 | copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) 35 | 36 | hopPayload, err := NewLegacyHopPayload(&hopData) 37 | if err != nil { 38 | b.Fatalf("unable to create new hop payload: %v", err) 39 | } 40 | 41 | route[i] = OnionHop{ 42 | NodePub: *privKey.PubKey(), 43 | HopPayload: hopPayload, 44 | } 45 | } 46 | 47 | d, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{'A'}, 32)) 48 | 49 | b.ReportAllocs() 50 | 51 | b.StartTimer() 52 | 53 | for i := 0; i < b.N; i++ { 54 | sphinxPacket, err = NewOnionPacket( 55 | &route, d, nil, BlankPacketFiller, 56 | ) 57 | if err != nil { 58 | b.Fatalf("unable to create packet: %v", err) 59 | } 60 | } 61 | 62 | s = sphinxPacket 63 | } 64 | 65 | func BenchmarkProcessPacket(b *testing.B) { 66 | b.StopTimer() 67 | path, _, _, sphinxPacket, err := newTestRoute(1) 68 | if err != nil { 69 | b.Fatalf("unable to create test route: %v", err) 70 | } 71 | b.ReportAllocs() 72 | path[0].log.Start() 73 | defer path[0].log.Stop() 74 | b.StartTimer() 75 | 76 | var ( 77 | pkt *ProcessedPacket 78 | ) 79 | for i := 0; i < b.N; i++ { 80 | pkt, err = path[0].ProcessOnionPacket( 81 | sphinxPacket, nil, uint32(i), 82 | ) 83 | if err != nil { 84 | b.Fatalf("unable to process packet %d: %v", i, err) 85 | } 86 | 87 | b.StopTimer() 88 | router := path[0] 89 | router.log.Stop() 90 | path[0] = &Router{ 91 | onionKey: router.onionKey, 92 | log: NewMemoryReplayLog(), 93 | } 94 | path[0].log.Start() 95 | b.StartTimer() 96 | } 97 | 98 | p = pkt 99 | } 100 | -------------------------------------------------------------------------------- /replay_set.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // ReplaySet is a data structure used to efficiently record the occurrence of 9 | // replays, identified by sequence number, when processing a Batch. Its primary 10 | // functionality includes set construction, membership queries, and merging of 11 | // replay sets. 12 | type ReplaySet struct { 13 | replays map[uint16]struct{} 14 | } 15 | 16 | // NewReplaySet initializes an empty replay set. 17 | func NewReplaySet() *ReplaySet { 18 | return &ReplaySet{ 19 | replays: make(map[uint16]struct{}), 20 | } 21 | } 22 | 23 | // Size returns the number of elements in the replay set. 24 | func (rs *ReplaySet) Size() int { 25 | return len(rs.replays) 26 | } 27 | 28 | // Add inserts the provided index into the replay set. 29 | func (rs *ReplaySet) Add(idx uint16) { 30 | rs.replays[idx] = struct{}{} 31 | } 32 | 33 | // Contains queries the contents of the replay set for membership of a 34 | // particular index. 35 | func (rs *ReplaySet) Contains(idx uint16) bool { 36 | _, ok := rs.replays[idx] 37 | return ok 38 | } 39 | 40 | // Merge adds the contents of the provided replay set to the receiver's set. 41 | func (rs *ReplaySet) Merge(rs2 *ReplaySet) { 42 | for seqNum := range rs2.replays { 43 | rs.Add(seqNum) 44 | } 45 | } 46 | 47 | // Encode serializes the replay set into an io.Writer suitable for storage. The 48 | // replay set can be recovered using Decode. 49 | func (rs *ReplaySet) Encode(w io.Writer) error { 50 | for seqNum := range rs.replays { 51 | err := binary.Write(w, binary.BigEndian, seqNum) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // Decode reconstructs a replay set given a io.Reader. The byte 61 | // slice is assumed to be even in length, otherwise resulting in failure. 62 | func (rs *ReplaySet) Decode(r io.Reader) error { 63 | for { 64 | // seqNum provides to buffer to read the next uint16 index. 65 | var seqNum uint16 66 | 67 | err := binary.Read(r, binary.BigEndian, &seqNum) 68 | switch err { 69 | case nil: 70 | // Successful read, proceed. 71 | case io.EOF: 72 | return nil 73 | default: 74 | // Can return ErrShortBuffer or ErrUnexpectedEOF. 75 | return err 76 | } 77 | 78 | // Add this decoded sequence number to the set. 79 | rs.Add(seqNum) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrReplayedPacket is an error returned when a packet is rejected 7 | // during processing due to being an attempted replay or probing 8 | // attempt. 9 | ErrReplayedPacket = errors.New("sphinx packet replay attempted") 10 | 11 | // ErrInvalidOnionVersion is returned during decoding of the onion 12 | // packet, when the received packet has an unknown version byte. 13 | ErrInvalidOnionVersion = errors.New("invalid onion packet version") 14 | 15 | // ErrInvalidOnionHMAC is returned during onion parsing process, when 16 | // received mac does not corresponds to the generated one. 17 | ErrInvalidOnionHMAC = errors.New("invalid mismatched mac") 18 | 19 | // ErrInvalidOnionKey is returned during onion parsing process, when 20 | // onion key is invalid. 21 | ErrInvalidOnionKey = errors.New("invalid onion key: pubkey isn't on " + 22 | "secp256k1 curve") 23 | 24 | // ErrLogEntryNotFound is an error returned when a packet lookup in a 25 | // replay log fails because it is missing. 26 | ErrLogEntryNotFound = errors.New("sphinx packet is not in log") 27 | 28 | // ErrPayloadSizeExceeded is returned when the payload size exceeds the 29 | // configured payload size of the onion packet. 30 | ErrPayloadSizeExceeded = errors.New("max payload size exceeded") 31 | 32 | // ErrSharedSecretDerivation is returned when we fail to derive the 33 | // shared secret for a hop. 34 | ErrSharedSecretDerivation = errors.New("error generating shared secret") 35 | 36 | // ErrMissingHMAC is returned when the onion packet is too small to 37 | // contain a valid HMAC. 38 | ErrMissingHMAC = errors.New("onion packet is too small, missing HMAC") 39 | 40 | // ErrNegativeRoutingInfoSize is returned when a negative routing info 41 | // size is specified in the Sphinx configuration. 42 | ErrNegativeRoutingInfoSize = errors.New("routing info size must be " + 43 | "non-negative") 44 | 45 | // ErrNegativePayloadSize is returned when a negative payload size is 46 | // specified in the Sphinx configuration. 47 | ErrNegativePayloadSize = errors.New("payload size must be " + 48 | "non-negative") 49 | 50 | // ErrZeroHops is returned when attempting to create a route with zero 51 | // hops. 52 | ErrZeroHops = errors.New("route of length zero passed in") 53 | 54 | // ErrIOReadFull is returned when an io read full operation fails. 55 | ErrIOReadFull = errors.New("io read full error") 56 | ) 57 | -------------------------------------------------------------------------------- /packetfiller.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "crypto/rand" 5 | 6 | "github.com/aead/chacha20" 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | ) 9 | 10 | // PacketFiller is a function type to be specified by the caller to provide a 11 | // stream of random bytes derived from a CSPRNG to fill out the starting packet 12 | // in order to ensure we don't leak information on the true route length to the 13 | // receiver. The packet filler may also use the session key to generate a set 14 | // of filler bytes if it wishes to be deterministic. 15 | type PacketFiller func(*btcec.PrivateKey, []byte) error 16 | 17 | // RandPacketFiller is a packet filler that reads a set of random bytes from a 18 | // CSPRNG. 19 | func RandPacketFiller(_ *btcec.PrivateKey, mixHeader []byte) error { 20 | // Read out random bytes to fill out the rest of the starting packet 21 | // after the hop payload for the final node. This mitigates a privacy 22 | // leak that may reveal a lower bound on the true path length to the 23 | // receiver. 24 | _, err := rand.Read(mixHeader) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | // BlankPacketFiller is a packet filler that doesn't attempt to fill out the 33 | // packet at all. It should ONLY be used for generating test vectors or other 34 | // instances that required deterministic packet generation. 35 | func BlankPacketFiller(_ *btcec.PrivateKey, _ []byte) error { 36 | return nil 37 | } 38 | 39 | // DeterministicPacketFiller is a packet filler that generates a deterministic 40 | // set of filler bytes by using chacha20 with a key derived from the session 41 | // key. 42 | func DeterministicPacketFiller(sessionKey *btcec.PrivateKey, 43 | mixHeader []byte) error { 44 | 45 | // First, we'll generate a new key that'll be used to generate some 46 | // random bytes for our padding purposes. To derive this new key, we 47 | // essentially calculate: HMAC("pad", sessionKey). 48 | var sessionKeyBytes Hash256 49 | copy(sessionKeyBytes[:], sessionKey.Serialize()) 50 | paddingKey := generateKey("pad", &sessionKeyBytes) 51 | 52 | // Now that we have our target key, we'll use chacha20 to generate a 53 | // series of random bytes directly into the passed mixHeader packet. 54 | var nonce [8]byte 55 | padCipher, err := chacha20.NewCipher(nonce[:], paddingKey[:]) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | padCipher.XORKeyStream(mixHeader, mixHeader) 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /cmd/example-data/onion.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4141414141414141414141414141414141414141414141414141414141414141", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f6e34152f83f6bc03769e265421e9615471ab9e10bace8e7b7eb4f846df42abb9a31b9d8a0b0b009cbf93fd14da6a1a6cb448b57cf9783025943e97407e52d8783f7b8854ad661941ceb446b5fe5814c7353354282f4cc3c47a3c8a9b75e4f436fbd25df99e608c4e53f76d752f4bd077dc62660f59edcdb55949856ea1ae07ccece645e2d2f995d95cecc6bf796d1208573ae0985bba253883e9cfdc62be6865829435c732e9c2d793c8c036d23248a4aa15024ca3bb960425a83b91ccd60c6e68d3c6a5bdd96832f8757fb39b26a6c3065a2f7e98b3f594ab5afd95286dd54658df649c500a3db88456999ecb3a8c8746082b191ced3571b88a1a16972bfc25fa2dbde775bb8e6299356e51a862eedd28c574e202668f134337374908c24f145ca0112deda44d020bdfc6e6d0ca5ee863f5ae79c849b0e34801fc785a3348e658abe57fd07a87525b5061f2afc5f0726754d2f97243fe36aef1e0c9fbcd41157f482d9c3348fae5a055ce2218f50f1a7e804451cd5ed55b36e3f72306b4b62fa1b31682ccb3c90ea24a33465da7c414fa022ce9c83b4e47c112f7dcf377282b148a02298d08d0ab5a560ba883452acfbd120be623e169dd7cc7d4dd1441074b21d33d4522c17c381f26084d811c612a337a47d44c9023867869f09da5b4ffd29c1674e8689e542f45cc864710a97073cd7b18039515425e66109b38a3a9ca6a14bab3e6e8c1dc5d28324f4920745d0a162644193f87011eb0edf5cb977508c943043d93f7dedf81a366ef6467df908decf4e55236171d17c6fe6995a982fefaa13a43be1ba5a9399be638b01864685b94e1e5e340e9444c11dbdf47681839d7134a9e498b5f53cc062682661d47ddde6a86e76eb0db9fdf1142f0bf4c30e512360967a3f3fd4c770b00d15236e52cb807769c770f3246be54563e57b3a243315d39aafff336c6883c23e2caa8405aec3129187f9f810f76e9bcabd5e93af911a88396f77d41b0b3ae2b3ce0884a0a34434577a48cf332bf844eac35493e33e423780a3c70376fc2c9ac8c403c568424e7691f35c48d1f28f37dd7da0b7168534ad819e14f7a09292b077e69e2edae9b73f40b8ad2ac116665465ec142c79594ec5169a890b3de310b26d38e77268a067c7c621b428e986b51a05aa9f2e8a8d88cfc3fd097e96f4aeb08b9de518e42f543c85a8827e55f59efddcd0b0a024fa44ed0617cc582e33555c5e503f2853754f6bc66339d007d8d9292387275e44d04af368baf56eb88ab9dae9ba9b79400aed0457ab277411a534e330ac8c5822f9c7fb50c05c34c43c4e7e38138bad20cb2142ae067b344d243c84fb4f37ce68111292dc40a2f5e99090e8929cf2cb5796d3c67c2b9dc49517a9d47617ed2962bb8aba03e022ef4fb68932ebeefc7341089294caa6e03a64e552897f8e7a60fc7b841a31a7d120fa64109f5f72cf20f96086cea8bdfc5646adecb547b85ca3c414860f674143fa1608e84393e99214f970ba2208ddd32ca8ee0c5d64d8de5ace4e9e47e9f6fb782eb7b6512c9a2740dcb086e82f4068d2677e866b9bc5e744d14e480268b10cdfb8c784c5ff1bdf9f3dd453e76b98694f0910bf81d57752421354823efc5caa5066ea9e63d4834149b1076f0eee0283a5539ec78421730190535e333ea062e0abf244ccd9ae1ce9c0dd856b37d6e3224f6fe4fe01bbd8d568605827bb6a4148b4554593cf173899714495ec3d19e939bd69fa5a259edca0aff4a34e2381d74a62e6277ee869a9f17131be3c7a24e684f129ad7fc746b7f4cbda16d7e36915c8a69177dc8e33390caf159c1f06aa75390e72db37d6cd0d88214e7e929e8ac5f7362be1eb1f2ce6139fd104eccef828" 5 | } -------------------------------------------------------------------------------- /cmd/example-data/onion2.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4242424242424242424242424242424242424242424242424242424242424242", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "onion": "00028f9438bfbf7feac2e108d677e3a82da596be706cc1cf342b75c7b7e22bf4e6e25017528605ca89bc5823fc2ff825f3a9ae4f431d927817917965078c79bb82b59ab4974eea9bc47204d2caa345bd67a12d5bb56c19eb5d93227e23fa995ad13342c067e3ce6cd55d11ef65ec2bde846a7a0598b99c0dc552157271b069bb323e9d9b0d076850d6288f76a8de29aac690d66fd97f994472dc456cf8a68f6120ef6821d3ed092dd1e4c704c93a4f3fcef0f78a156da42a80f2c1ae19935030ce0507e905a93900079ecb2a1c9e233a92be8e97a557f063a59c95cae29c791ad7550f62f5f1888be1b247e6d55a6de26b136c2896d6758314108a429c82c683968493c807933d6b93e30f09c92e1da6401e3af4d774f775ff84b9536192720637cfdab6aa434c1e6c08baf075a8a3b55c13fb9a36ff59fa9ef8cfcca35fa811c3db716509aaeb5a2979dd6933b5fd5fefec35ebf294c779fbf700bac91fa52cdfaf511265354dd0225b15f72fd761280297f620866fa45f92f61fededb7f024a89643ffdd5f57097daf6202091ba8b77ac7e862f0cdbc5ec03daf0968e11237860004e572e26164ace8d2f7e4f50ce6a5a008934b072749c07358c4016738a994476ea215d3623079692e9f3b68b470e4af4c6985a397ceef8a7ac09490b6f43c440c7f83413c1c468d5f09e3cba23d74c94899a32b382dcb8d3994a16c1e7aa9c5b7135b7b3cfde277f68a0e2c1742a53422e5acaac5218eba290ead01561893c2bd6375f5dd0c24e84f820ab6ccaddae5159b3d2b6372c5b865117f3f003b6ceab16b189a8183e0be79e4bc872825bdf2ac56278ac7f971121c221245c6dd31a3fc04945393ef0880ff6cc22bf5e261e8240591997cd3648da417b0a4842fdadc8f24c08e5af67aa19335343830805919e9d4378900eae26fa5e21d7b9fc54111d2410625004f7baa9afc0d0f1099156db4b43bb87074c5e3beac5a97d4b6d9b84f565186792a18cdf42f44b1f080770d1a4f311f597c8a50d29497414641a1c031a2b561cf347f0a907b0af8d8fdc843ae762880af61e64cef17d7aa13a0f72a0ef0b19489f2a90167dc7795a4dd02c5358fafb05945d86ad638d04d2b8ed8189ec5d299b6f17cedd307c1cf6a2462af84dcf2a404e9a7279bbacab908685a587f118a232bd646dfefd60addbfe36a2b1f7e8ff949052d8ea4c0d236d01bee53962eb718ede8a94ff29cb5e58397d7d32b3e6e63220461539858fa8ed0930463df4277b569c6ca579649c8db11d5a631a0000e722ebd657afc973086bb06dc8a73f6464728b6884e950a68b60c86f88184dd260fda8f0b962c417595632bc547fd46417092a77fdabfedd190882f1e52d48323c4345e9542d723f930a253967923ec8d27e71bbbd0af9aa31544c0b4d6da5eef06be3c3c9049457a371712fff06626f4480e0cd082e933104f12dd23bd01ac8464b21623f542b85b6f5fdbc1daba3318c8dc9803506e666dbe13f7424f0279273a687fe89a1228608c249c5abfee75ac742db27ed68b19312a99651ce947fa98e3945e4f3bdb449c2eec79e9efb746731a0f5d5a1974bec7f60d872e207ef2905ba044ba5118ea2f1b116e1dc1aff9975a3eeff8f2ec5b0d72459bb7af222e586869a87fff3da331520a7d34c4fd58c090bb31e00f1309af259ddee14b94473e4517785d673d79a6e71b3e6ee3fc887ace2ab48c19744b0a3b302fa6613a23c0b18bc35a8dad53a6cbe62f23cf1773414331bd6e99beab684b6ccb839dbf0397e85d106e243d760197a364d586989115ade30d309374fea8435815418038534d12e4ffe88b91406a71d89d5a083e3b8224d86b2be11be32169afb04b9ea997854bb72493084f22105fea7b5181eb70fce6ff3d92bffee12f7f19ba453f6bdfc1ca" 5 | } 6 | -------------------------------------------------------------------------------- /cmd/example-data/onion3.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4343434343434343434343434343434343434343434343434343434343434343", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "onion": "0003bfd8225241ea71cd0843db7709f4c222f62ff2d4516fd38b39914ab6b83e0da0f13ca6418876711541d1adcd84fae578e80ed4c2aba02acbf1fb3716fd7345e78c0affb24613c2ad822bc7c0e1ea8c9aecfe8305ab1400421c900edf4b4d461df181853cf77799945528b4f40ae3f98ec572ffdffd5d8962bf4d4d8c5780529991c9de9b33176841e7613250616146452e0158206106731b6d1f98cc91bfbd109db6dccf8fb141ee350a99ba0889f011eab62b4b65fa65e2f44d0d5f9adb851e633e3d3654fff80067e594efb67123b5f7343244aff9d457efbad718af0ed16a8788870e3e7b8fde17ef27719e1ddd41e4335c5dcbd14615753b165ac992b9fac7df34ed13bcdbff085d9ec2edec3149307be22ce1b6da70303906139e39840d837ab6d8dd335eb66523637bdeb2b446170cb02994f96d3639b2bc66f1eb46f6c614b70ce6e7f539fb1fe2278035e409fe9f8797bb6fc3604b58c835725bd4e48689e224ff784c6678e54fba9701609b7d3157ea05a7dfb6092b97c362ddca2101f3c1c953a5ef92037d268c0c94ef5f5c082748fd068f2a5b8b8a4c130687591577ddf3d864665a5aded674c33a370e1dda6be247b1ad2885f06c10dc9c1784c869445a7cd0ce5aee125bde1958d11ed3aaa10fdd00d490a58441ed7cc0bfd79d82075929f3b9b93f1f9f61b472c4864787c770a908332b494dac06496d0589d13f61e57b291eb52d70b0f0d431714f842f41b291e469a57989329ea9ac50ae903f51395a51ef554cf4c2cda103f01ca0cde08ed2ad721a25e303bc33d66ebab2d43710378a7e9c5dc211786983f3fc845806f4beaee5833a6b0182396c740285037cce48be4b9552280c3dce3e6f0770f0f2139567441c6d311523632b11348a22cb8c3622acbfb0febc38bebc0d2d260998c13dacb9c90ae1740515f1b21cb6ed53dc0c0aa239f4fc6fde90fc89ec7ab7a113d8ceddfcd3bfe6406a9998b66ce144530b39165e3f05106b85ad99cfcf36fc55abf48cbf388d3331c9961ea20484b72d8d2f831cfb9dbea5c63512c3abbca4148fa238304f698142983777e8cb7806380e37c2c71888bf7bbd8bf1ada837acf18388db21666abfbb6e386dbbcd454a09f0c3ab94ca1b48d9d3b0d7c394f1ce89bb048f9ec3a62c9534262fa989b68719f6b4653c4609614d606030b39d4aca551078b302f1dc0b4fabb1bc9d11f8b3950044cd64befb42075f84337288859bb2e575c8ee0285daf7b2fdb35da2e764e7ea05d30b63e534050fea7111f4712f82e62e9e811c43f1ca0a232e86edc874a0498320bebf60c5dc413f5a7d2a54cd59f0383ca3123ebe51af4e507b8588c23b276813e893ec65aeb795c8b9dc4185acb2cb91fdf39955e55fdeb1865f4192f8fba589fe2cef5c934ab80716d0d3e7e1869d06735a457508b88c795aae03f1b38b4aa3ff36cd20284318048d5fb2ec47ec999a2512ec6dbdfce24276c12ee6bc41eb8e0f53c193a02b1882fe5b4ce61dba6727180bd9ce65a718b8d40471344e8e2a90008d2ebcb65c8cbd6b30c7f8ee57c27460113b29b5c0c1e0bc12a26d2251998ee146235d361579124164b7566d9218dc116f50fab939715074bba73a8cb953e171c187c70022364e140c704a78f93085577f27bf41f9e02f55c155ec186133cfa8b1dd40cd4c773e735a8f15291fccc8c4f55bdcc8ba563004597b5b3de5d54be23671bc9477805ba10d03afb0715782845d2ab45df012f6644207cc5fa4739aa3eaf6bf84e790128aa08aede33bf30c6be2b264b33fac566209e73715a254047517e6aa05e31afb8bd076a3e56fb1c5bbc5dd44f6f095d7f927939ab97b6899cb7bd8bbc5610db5e2556a698c01ffaeda20c8daf398a339af8e2ba28d4b14eb916b64a91a09113431f38193d0" 5 | } 6 | -------------------------------------------------------------------------------- /cmd/example-data/onion4.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4444444444444444444444444444444444444444444444444444444444444444", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "onion": "00031dde6926381289671300239ea8e57ffaf9bebd05b9a5b95beaf07af05cd4359526cc8aa7d0f6392e039a9f2846827fc997c3312fa06cac98e5199c529e328427e7bca6533bfecbbc53af0e076057c647a07d93a056ea323538d4feafbc057a4f654e7732292561b85f349c7ed03c891b888a78050869ae4008c4f0eec16e1d902424c2b3a0a3f7d37684cd5e0c41b841b3449e32af7e3412cddb772701dd91feb687edb7bc26d8b530b32b93465b906001ae610384d3c2a39626e5905340ec5706a7b1f0c8f3cf4221a44649e90d64a9f7cd7bde3fe269782e22a58ee3ab3cc3aae66387003a54ac70a464fffb474b17f591e7d6ca03d8fc6319e2ac03977e6320477ba64272cce10acded883e472d5cb9c151e9d25a6dab669d6b228a96d28dd8a1b745f87f8d4e21851fa26da8c9dd7354a73160e014249ba64bce9a2fa55b67bfd86a05187a15b3989a114aac102c6cf1fa1306b06dcb7f4d3e1c292f09eb52ae2c6295307e43bc373d70a32c1aad7731d624e2a7160e3fdb6e3baa516aa5ce1cbc0227fca581242b0bd6ac2f37213d3d8b68cdcd37fb6a53151467a736f98cebe6dddf2a97a9b6f04155a1f41464d42dec10c9bfbed84c6fbf7859af049f17dcb69cc4e642e243c3fdb9359bf70d0390688cce1d297b584cbd7fc8439ed4366bf1f7a64274a1f0917e0c36f1b3600a87fa4c450bb4fa9d884d0c5bfea6a51b7e1d25157330d72e171a4b7b994dc7c391ae79e63507d86ab07215c9264fab83303c8d0084ab00bc1b8e00a9608eb58278f55b765c5bcb4e4baab79283560859fd63538c80f8b58ccbd2f30623c29b4bc177aab641150496c2ba56bc8d4d6d9630d74692712c9f6a9e4efb3091cff9b7fb0bd285d558e55781183a19c2faa47c32674638750dfab3cdd5998514f8d57c46bd341f1d0ad2b6f08ac93ae1e6acd71c49509a5ced3427adfc4a6b0216bc83171c071a45b2a612ed296d312a7701aa0521319000748d3c1cbd6b3ead745875e08c04b95e7bf3b3cef6008decb530ebef1cb5e8f2e54c8b77b419123b23582899823ddb6c16736be4259c933eeb50d0f456e6c60564fd75490b1485faa2781ffa6fb6aa74599ac3a86ce24ec93d0e281ded02cb851f7730424911a5e59b72c35488e181a874af550915c69219c08386942b1d210bc5ecd7c0f4e9750846e880d36c06e0abccd4431b6aa2da300adcc5413ae323a19d8c3ec390ecc4f382b10c9b2c1d75e580fcae94160ba2fd2ca7fcf7a2ff5d02235319ed5835c28994654e37329d5e9fdbd1d47062e75ef089e6f4a6e822e1badbe620e83346545be79a0ed25be303c60e966c9f630b7ad3b85aa7be800e16edc6a8f9d012969bfad73523f4f3149a3ac8db49931d631dcee7c29d6f43d9882371c79a1eb608c570bc7d49126a0c456951f583cb06fdd45976e6f1ca4bac239202f7b52acbaf62f26ee24778039c855cdb9b0e7649072a142f14cf14c3cec4ee0f76a5bc16bbc50e6bcf726ca01904b0f4e7154ef06791260be1127259d4eae5044665d07329e18a6308f8028c0d3dea773b0b905ba29ae6f2f683cce6040a4391c0fb03731ce7912edaeac97df5b44bce0a1d82cb3c9a21d48869f10bb11572b7ce1e051d164900a2b2b3bb6c36626f410b9e5b65e57a206983c779b77ce6ab15fa0f9078b4ac9ee578731018fc981341d5a95103767c7cf24b93cdbe1f8644622fe4f5472522b2d63760f504954c63e496d353acd777f9aea905b65b9bb419a67cc967b0fac6872389b7f737f9038fabb526cbbdee38238d51953f372fdd9ebc260b0ee2e391420c4b11145bd439954834d9a79e78abc57e03d3ee20d239d8a13014976e3f057ab3c38ca79ee82ebd2f9fd2aa46f2a36d6fffe4715c2bdfcc1958136742946bc5993d5b51e947" 5 | } 6 | -------------------------------------------------------------------------------- /cmd/example-data/onion5.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4545454545454545454545454545454545454545454545454545454545454545", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "onion": "0003a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f40bacffa7597e025eea9286b58117e0cd9250c5800ba52f9342e43477eac9c03bed26ea0d0e31f62c1ee71220d71ed638f95b2c2fe332371c59abd7e934d423437744e6237aa9eb7d373ac72d44c33e5e7e53f5c7e09264d712a3cc1d757e6f48c0604f08297d12839030b09e0a69137cb717260b590edbffd98b2dc31a87cf21bb1d5dfb3e2abcc89846ce5eac9c404d6ada0c84bb68bb3fcb585aef8f689ee206ce12587a4a0ead0cbf445c13f04e8f76c62a9c3d3312185bca81bdf0d46e8733645912c0ae31d8707a772a6bc179f62724461553acdc4e54f4b90d6c583889e07d0ec6c27a11d090122a1d075e5094063d6696078a37f34dd621847cf8965baecbfff2094c31a5da449e4cfea9fcd0438ed29fb8c1c3006b7c047055b12407925ea74c2bb09ca21f4bc9d39dc4a7237ef6facbb15f2c10ccfeefc52524019ad0c33bf62729e98130384c2f5ec4e68fae784f71674ba9fd2a89c328695e582d3bc850dc0e1fe86ddb88138a56df283dc380f5114e3a3970f71eac063e5447ec37777691e56e0f79e372eeed6bfb7884cf9c5dd6495c2d7fe56bd1c6d112a69b078e0f82b9096ec322a7951318a23c2b06e78c3c0bd4aa76f8da1f7b285159d3f89bc47993d9a4d9759d7db1f2b1ca9b3629da41d647d7a4451801e66fd44670d4881fecce3bc6e9c09d4f7a4c5c6ffbb248c3e29cb9a5d7a75490f2f5ed28a7241139588b64bb692f92241e312de035d21f3a52269693cd985aecf6e80eb80779ac73bb3814809b4434291e423c2caeb91b3f0f744425a7f2640c7cd481b9808e6251f963020ae3403c0c719aa8a06207b3d92e65ea39f9b1a6637b9f71a4ec8d9b38cf42a6bcb02ee771fbba765e852d21398b1612328a54c50f165efebc5d258edbc02e1deaaaffb201c61bda6a85eb802aaec66d71a8121c7520127680d11a6a3017722af48b0bf796fa39c37bfde2217f896f764eded018b096671f63f066dbc7018d23966be95b244b5eaf81cbea0edbbb75bfc31c12a63c5262b8b1468ae4282112f8531bb8dfa45ac5b62dd01039737e808074ebdf0a49c73a30462bba152c778ae63c90413333e508225c1890983798b1bbd2fe75da1236e8752db4ad8abba0161255fbc47dcc07e1b213f8f78a75b8d5ba7041e294a30f6cf558f73ac6ec48046a087946dbc2612116d1fe2deaeacc24eda2277d5bf92892468590f5d2e467cff584aad2efaa601f30e1958afc1d84efca83e2bc55ec6bbe3b7f0981c5f46b3bbc39f71754dcc965b1b3e6972f7aa18914cb27675af3137b8421cded725a2cb1e73ec6a6a149ab6647babeae10f26d34fce7ef961c2790124cdb2d9f23aa3caaf3d52cac0b1bfc1b9c8ae7df401bd051ad89b5a0ae7352dd9e8f029ff080d856d07b44a5b430875f89849bac2aeea155573f59f09be57de93f1169964647d58b21c1d925d0ee2277cd11e129090a019a4a193563c2051ed1ca1b29bfa72612293713110939392056abcd296e54dc6dfcfedaea1c4d7358293e9dc6cf381cc8dea6cf90705700ce7a0555ef2142b061c02ead628fc8f0ddc2409ebda4fbb275d4c44fc8644a4431628f4afcb2afdc8de0e0e1ecfe7fc81d65ee4a1554587be83b82f391e61be12c18ab7b695d3f2284291b56bae638d098411cdfa27915f128d39727e5a8f315fea61937efb1a5f41f2a5b74e0ce78c401e5f55fa3451ed4f60469a135a3dea7dad750f39082b3d9a7477d74963aaefcf22c9c02d50e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065cf435b4dc1968a05e5471b20147684e96c4de4894a0543647a046d8c92abebd08" 5 | } 6 | -------------------------------------------------------------------------------- /varint.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | // ErrVarIntNotCanonical signals that the decoded varint was not minimally encoded. 10 | var ErrVarIntNotCanonical = errors.New("decoded varint is not canonical") 11 | 12 | // ReadVarInt reads a variable length integer from r and returns it as a uint64. 13 | func ReadVarInt(r io.Reader, buf *[8]byte) (uint64, error) { 14 | _, err := io.ReadFull(r, buf[:1]) 15 | if err != nil { 16 | return 0, err 17 | } 18 | discriminant := buf[0] 19 | 20 | var rv uint64 21 | switch { 22 | case discriminant < 0xfd: 23 | rv = uint64(discriminant) 24 | 25 | case discriminant == 0xfd: 26 | _, err := io.ReadFull(r, buf[:2]) 27 | switch { 28 | case err == io.EOF: 29 | return 0, io.ErrUnexpectedEOF 30 | case err != nil: 31 | return 0, err 32 | } 33 | rv = uint64(binary.BigEndian.Uint16(buf[:2])) 34 | 35 | // The encoding is not canonical if the value could have been 36 | // encoded using fewer bytes. 37 | if rv < 0xfd { 38 | return 0, ErrVarIntNotCanonical 39 | } 40 | 41 | case discriminant == 0xfe: 42 | _, err := io.ReadFull(r, buf[:4]) 43 | switch { 44 | case err == io.EOF: 45 | return 0, io.ErrUnexpectedEOF 46 | case err != nil: 47 | return 0, err 48 | } 49 | rv = uint64(binary.BigEndian.Uint32(buf[:4])) 50 | 51 | // The encoding is not canonical if the value could have been 52 | // encoded using fewer bytes. 53 | if rv <= 0xffff { 54 | return 0, ErrVarIntNotCanonical 55 | } 56 | 57 | default: 58 | _, err := io.ReadFull(r, buf[:]) 59 | switch { 60 | case err == io.EOF: 61 | return 0, io.ErrUnexpectedEOF 62 | case err != nil: 63 | return 0, err 64 | } 65 | rv = binary.BigEndian.Uint64(buf[:]) 66 | 67 | // The encoding is not canonical if the value could have been 68 | // encoded using fewer bytes. 69 | if rv <= 0xffffffff { 70 | return 0, ErrVarIntNotCanonical 71 | } 72 | } 73 | 74 | return rv, nil 75 | } 76 | 77 | // WriteVarInt serializes val to w using a variable number of bytes depending 78 | // on its value. 79 | func WriteVarInt(w io.Writer, val uint64, buf *[8]byte) error { 80 | var length int 81 | switch { 82 | case val < 0xfd: 83 | buf[0] = uint8(val) 84 | length = 1 85 | 86 | case val <= 0xffff: 87 | buf[0] = uint8(0xfd) 88 | binary.BigEndian.PutUint16(buf[1:3], uint16(val)) 89 | length = 3 90 | 91 | case val <= 0xffffffff: 92 | buf[0] = uint8(0xfe) 93 | binary.BigEndian.PutUint32(buf[1:5], uint32(val)) 94 | length = 5 95 | 96 | default: 97 | buf[0] = uint8(0xff) 98 | _, err := w.Write(buf[:1]) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | binary.BigEndian.PutUint64(buf[:], uint64(val)) 104 | length = 8 105 | } 106 | 107 | _, err := w.Write(buf[:length]) 108 | return err 109 | } 110 | -------------------------------------------------------------------------------- /cmd/example-data/onion-blinded.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4343434343434343434343434343434343434343434343434343434343434343", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "blinding_point": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", 5 | "onion": "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58dbd4bb7c2714870529410735f0951e72cbe981e2e167c0d8f3de33a36e39e78465aea2acad1e23c78b6fd342d63e37d214c912b4a0be344618f779138edc1b42a5ca3218ca2fea4be427f6cd0d387160db2bf6c2ba8e82941c8cf3626bd6bed7187f633012ef49df38f6b12963cb639e9eed1b9d269dcebcbd0b25287aa536ec85e7320b02e193122199a745ccbaaebd37f5d4b71f52f9b50feeb793eeef56924a046bc5e7003f6253e0284a8d3fe2e42c3564050f1e753cd32cc258ac0ffa6e05eecad5ba1286f78252e60dd884a65405ab673a85ba52adfa65c1086d4bb37ba2e0848adb2b04379775ad798492b14e8997f30ffa9cf5d432bdf5b246fce008fd876399beed827db58195f4f6192f6ff4ec63cb17fdcb497cb7aec26846a71dd8dca02fc3bb14dd7231a4d62a981bec54b71eb20331096dfa214a0ff4489ee96db663826ae8c850e9f06baa52a47b8eb576363f97e742aab2dc616acc6e74588e1d2ac16694febc90abaf5b1c684163c0e615a68d32633f01934adc8c6bf91fa3fd7aad033b7596d60402494e45e2c1632c40f7bfbd88a81a896a1d28ed6338c83e1eeaa467945d59998eb456c95f94bf1892e8f326ec2d5e0196b7073f106febc6ab8ca5bcc23f77ffc819bc1b5debce418ccc7d8391bbf33bceee6110beba170121bd99f54c956e64970bdab31227b03ee0ea3f01fbd9bd74015f6f82d04fab072e8f85f4370d09f41ee3e48eb959767bd989abb4eea42c4daa0437a7f747d7f9b70eb87b9f9b0b6f283b8205912601a432999b8869fd9fe5bad3572edac24da7184f9298f21ff60923db277264d29c846dd2f228f6fc53b6b60364237de64773f803f174ed10229c374f603ccc5fd3a62cb413ffe6f5630dc646bb33f231b2350537ec39e5d3f2fe1a1cb019ed0b18ad14019cad27afcca8ad70387ca110394c0432774f1aa1fa404b2e086c84a55388d3bd102501c78ef925cce89d76fa04c3f20f2d1f0ce507ac8b37b7913e3949ba12bbc5a4f6bac37c2415622d365bc8b83709a28e3d46f3850c89a3ff4d027fef6e3e4ce5c6c85f663c7eaec3c9730106fb82f53249a905533cfabee812aae51965b24b42f7ab471967bc8e73354e69141ee26a1f03684d5fb9c256a34de8257210e0390dd3962db521ae0a3bdab28300610ab2a634b699e5f092da5a061609ef6414bd805c8171f54ad6f285fb64ce0becca0b61188badcf8ef21190dad629e3fb3e89f55ebba829919540ebf5f8ae4283836d3c9133c1ca3365f6b9394916730411650686e0c2ab9c53b6cda9efdd5cfcb53ba9b6962bb6aa49d0a83a87460b60a9c7d2643ee99afe652883795f14014ec5df61b1e30c041c1fa6487f3c82f1ded5f83ffbef5017e197b7fb77be3b36e284a15e57d45bf9316dcaf97eb78ee4642b731ba05c5063bce1333fab4af6da97c80a96ee599b4df823efbedc250c0abba9783da7ddf2414b2a4774ff2880a7dc6791103e18b8631e39743cf9e87aed71700daa5dc72fdae520324741f92ea3d510ff555dea5e45f15cda87272d4559a12d4777680acb06993840e3c748da82c16cae556015fb2acd0335da11a3388575394048ab71199793ab706abc9d68add2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983577c1a0386c6d5d9a2223438e321b0fedfdee58fa452d57dc11a256834bb49ac9deeec88e4bf563c7340f44a240caec941c7e50f09cf" 6 | } 7 | -------------------------------------------------------------------------------- /cmd/example-data/onion-blinded2.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4444444444444444444444444444444444444444444444444444444444444444", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "blinding_point": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 5 | "onion": "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b967c498efc246bdb8e060a6d1cf8dd0d4d732e33311fb96c9e9f1274005fa3d08b41704a1b7224c6300a7caead7baa0a8263eba2e0de6956ee8e4a1958264f47e4cf20d194eb576f5bd249ee4fece563f80fd76dc3eaca8f956188406d83195752b5c90c4b2a5e7ac3a8d5c62b17b551aff48ef6842a7e9326832c9a4a2fd415011150a9e71beb901fd9747bac8add1c694b612730dc86b5b19a0bbbc675947a953316e3303d7b30c182f94def9206671edac9a3ec3e52d28fc28247a1c73ab751bf61c82c3950f617e758f79bd0ba294defb20466eaf1e801462046baad3aec3e5b8868a7b037f23d73a47a7e74c77107334f37388cff863e452820c61d89728fa75c84bc7cdfc06dcdd1911f5f803353926d073efd65251380e174913aae03318ea5b6f0ec83998c55ab99bef62803ea2da9f6d1ea892b90efc4f8ffb685a5201a781da2e6ac5923645638c9709ae32171a00c0cd3d8c7eedfb06b4eedc7d3e566987e2e3805a038f21d78ded5d6c7137a5e8e592f3180ee4d5f4e1289176f67fc38690d0958bc82e240b72b10577f340f1e14b8633f0b6d9729ff4618be2a972400a015a871ba33be70335f652a8d70f2bd32421d6ac2af781d667dad787d6aef4505a15d046579e46eebe757444cffca6d0610f0dd36a7ce57af969bd0c3f7006298ef406a25f689daf58f875d44d2423ebf195b503f11c37c506ea6abe50a463f7bb5e9b964604d832724de768513f6b38bf71715e1feea8a6e86797788d487146891564919da1372016ed8f08c7fcbff66a4a65a3d0fcd8e3daac6eba41f5d65ef2d8075364a9e78b3a273549f6eac4abb72e912e237990367e0d43e89945f8ac3907be5a6c662139485a50cb5ce3f0ba08586c39f6c515368ec3f91b72295f1b7a73a9df322ae9a45d363d6c616be3300083764cbdee31221f25a318f095feacb09f957c96db30fccca47a0215b576c3ed925a0bad05d6400abe318c11f36628c387a4ee38832182cd44b3cd48e5422c1f1e3b57218dfe72c611f5415127720e60f6e2400607e61841b76de1704bcbeb0daf1377ccb2253916de2b6d490bb71ba0a44fea2e94f2423d723934557d5905e01b2b80232a884e258d46dc92ea11e0818d0ece5b914f02049866e151801ab8c9aea155479b354dc91151fb9ba43277458f9760dd859faaa139e3b9ab36a1dbc36a93ef2c90598b20cb30ef3c4f23a2d6178b4d1da668fb328a25d84d30a132d9f2a6a988cbe2e5c2be01cb6db4b4725a50d6cdacf5fb083e7d650a25bec1407fbc047d26076c7596429a29606ad527e97ef0824ad6c1b05831a3e5b71c63a528918a3301cdd4061fc1fcce3da601961f2602a2b002ac8404125c2d52666263858a923e197efcda873c32d86897352e4f2264ad6a1b48acc0fe78ff55cb442cb2bb5fa2880810e1d00aa0247057fb80b7ed36cf9647af41b44ee4a63ee2d6f652526404572520a7d2d9dcde4e62df0c3be89f8471550594cdd16a51a9cacc58729c092c68506162fe65edc2314055d389f724ced189d826a546b5c4d08a43d977b3cf033de5760b71a7cc38ee5851592031aafb467a89b3b6c7ed67b15d44c48d6baedce3e95e08ec7c55038f3eba90ccb900895734f0fb7efe54961ce493369cc56416898a9bed7c2482871c15a7f1eb5ed17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bca955510e2126b3bb989a4ac21cf948f965e48bc363d2997437797b4f770e8b65" 6 | } 7 | -------------------------------------------------------------------------------- /cmd/example-data/onion-blinded3.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_key": "4545454545454545454545454545454545454545454545454545454545454545", 3 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 4 | "blinding_point": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", 5 | "onion": "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130adfd06eb26201baee8866af2d1b7a7ab26595349dad002af0590193aaa8f400ab394f5994ec831aeeecb64421c566e3556cbdd7e7e50deb1fc49fd5e007308ab6494415514abff978899623f9b6065ca1e243bb78170118e8b8c8b53b750b59cc1ec017d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f25e386202a4b6edff2a71e700229df7230c8ca31bd5588f04799e9640c9c20a47cba713f3cc5ad3202e14bb520880f2a8409d8e7835cae21b48a651c2d47fe6af785889ab98f1416f6e4ad67a66ae681e9a8828bad3f9b6890221c4a7ec80531d6b63eb30843f613ce644795bc8bcee60e8f7b36f3fd04de762f103c52efaf36a2f3bbbaac482d6271dc4180c10bcc076c04d06ea7fd8fb6a647e0e10523b05da2d89e4139fb55c2315cd01bdcbd57587fef8442d7ff5620630fd2d2e79739d90be811bf2cba60415d6cba2cea14ba1859f3122cd905c4e12e3e2a1ab6fab54b2ec40e434626e2d3c3195c02c82a8bd64d226c2328ac72ca12197d9908eaf54333717448ce6ed73adc0ac05e2ee1d735131d87918beb8995993dc8f63fe10f2c8eba2be7ab8bb44d9f78f59ef3e4c180bd75e4eef2381450c6f0480d543997305f1d07815993b5aca8d88d474966d9abec93bb069a16aa2da75b87f94576e01d08a17d3e0e3d0370f010733a7d7affb12cdf94c259a62607fce71003535c4727305de5ff7bba3840922844b3a45f62c29715fccf440517ef121450f6962396fba9b07036d085582405dcae6ee95964b66bc7c85b8d02d90091500db3cebf6de584f86b7b55335a8c9aa26381b00747f055cc458a2cadfccf9c29702bf941447beaca6583cca09492a57d4b03b2ca00dbaf41dfd6a9b249381626a7debe475735a7e39e77a363eccf14669046f656cc09ad448da8d8b545e6a604f46dc481786d09a94c63cf23f49ba367d2929466364dbce2a8ffce3dadf8f4cef8a56e1fefa1a3304a953fe83018e57d8a95694b02d994fea2630a9a3d5f1e2f6d6142d503ec4152871f7122d7e566a03261f554639e7a759e0e73846f71d5cace37d91336fc9ca9396bf64ca2cf45fa2db779b3b5c63b04f1c0c1fb79fdfcf5a82b0202df934ae1720a7ce1e047cbec3f82737b50168c974f4623cacce87e3f5bd5232caca7956d28ffedcf11ac5998662c5f6b13c6126584ca2e894d3fcbad4d130bbe22e88a135e0020cdd43853e0b3af3800e9544854d211e873cf68ab683578d501d69ec5dc7fce42ac436d58243880c1b88227b0681c6c9dd8a8ad0793202b15ab63b787b748e258da3e68d0e649fc4ac081a71de8adbc891c113d5f722686b6ac4ed9e3cc247bc4a4643416f480627e9de20f7307f434a499f5c6951c2e8b3ff51d455bf65ceb5ee3dee47b968ac2642e13d8a68f903b73627c2e75788fecca5836371a908eea4f1ea44db2315bc185f77e478efeaaa4da2da13fe7aeaa79ed1d04876a8b2b7b333c5de8c4c9a50274c2eb7b9bd2a3630c57173174781fc9785235f830cefa1c82080eaffdef257f18eedc9ddfd25a696a11a3dc56cd836be72f5f4a2cbb6316d5d3b1ad91a7ec7d877f28d2c29a5525b0b24362699281b0e3b48f38caf1085045fe9089f9e6fb29e4b47aa4cecf68c9bf72073469bd9beeea5e88bfe554cb6a81231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9cbe9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd879fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f1232950cef398ad3e057a5db964bd3e5c8a5717b30b41601a4f11ad63afe404cb6f1e8ea5fd7a8e085b65ca5136146febf4d47928dcc9a9e0" 6 | } 7 | -------------------------------------------------------------------------------- /scripts/check-go-version-yaml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if the YAML file contains the specified Go version after 4 | # field 'go:'. 5 | check_go_version_yaml() { 6 | local yamlfile="$1" 7 | local required_go_version="$2" 8 | 9 | # Use grep to find lines with 'go:'. The grep exist status is ignored. 10 | local go_lines=$(grep -i '^\s*go:\s*"[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?"' "$yamlfile" || true) 11 | 12 | # Check if any lines specify the Go version. 13 | if [ -n "$go_lines" ]; then 14 | # Extract the Go version from the file's lines. Example matching strings: 15 | # go: "1.21.0" 16 | local extracted_go_version=$(echo "$go_lines" | sed -n 's/.*go: "\([^"]*\)".*/\1/p') 17 | 18 | # Check if the extracted Go version matches the required version. 19 | if [ "$extracted_go_version" != "$required_go_version" ]; then 20 | echo "Error finding pattern 'go:': $yamlfile specifies Go version '$extracted_go_version', but required version is '$required_go_version'." 21 | exit 1 22 | else 23 | echo "$yamlfile specifies Go version $required_go_version." 24 | fi 25 | fi 26 | } 27 | 28 | # Function to check if the YAML file contains the specified Go version after 29 | # environment variable 'GO_VERSION:'. 30 | check_go_version_env_variable() { 31 | local yamlfile="$1" 32 | local required_go_version="$2" 33 | 34 | # Use grep to find lines with 'GO_VERSION:'. The grep exist status is 35 | # ignored. 36 | local go_lines=$(grep -i 'GO_VERSION:' "$yamlfile" || true) 37 | 38 | # Check if any lines specify the Go version. 39 | if [ -n "$go_lines" ]; then 40 | # Extract the Go version from the file's lines. Example matching strings: 41 | # GO_VERSION: "1.21.0" 42 | # GO_VERSION: '1.21.0' 43 | # GO_VERSION: 1.21.0 44 | # GO_VERSION:1.21.0 45 | # GO_VERSION:1.21.0 46 | local extracted_go_version=$(echo "$go_lines" | sed -n 's/.*GO_VERSION[: ]*["'\'']*\([0-9.]*\).*/\1/p') 47 | 48 | # Check if the extracted Go version matches the required version. 49 | if [ "$extracted_go_version" != "$required_go_version" ]; then 50 | echo "Error finding pattern 'GO_VERSION:': $yamlfile specifies Go version '$extracted_go_version', but required version is '$required_go_version'." 51 | exit 1 52 | else 53 | echo "$yamlfile specifies Go version $required_go_version." 54 | fi 55 | fi 56 | } 57 | 58 | # Check if the target Go version argument is provided. 59 | if [ $# -eq 0 ]; then 60 | echo "Usage: $0 " 61 | exit 1 62 | fi 63 | 64 | target_go_version="$1" 65 | 66 | # Search for YAML files in the current directory and its subdirectories. 67 | yaml_files=$(find . -type f \( -name "*.yaml" -o -name "*.yml" \)) 68 | 69 | # Check each YAML file. 70 | for file in $yaml_files; do 71 | check_go_version_yaml "$file" "$target_go_version" 72 | check_go_version_env_variable "$file" "$target_go_version" 73 | done 74 | 75 | echo "All YAML files pass the Go version check for Go version $target_go_version." -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lightning-onion 2 | This repository houses an implementation of the [Lightning 3 | Network's](lightning.network) onion routing protocol. The Lightning Network 4 | uses onion routing to securely, and privately route HTLC's 5 | (Hash-Time-Locked-Contracts, basically a conditional payment) within the 6 | network. (A full specification of the protocol can be found amongst the 7 | lighting-rfc repository, specifically within 8 | [BOLT#04](https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md). 9 | 10 | The Lightning Network is composed of a series of "payment channels" which are 11 | essentially tubes of money whose balances can instantaneous be reallocated 12 | between two participants. By linking these payment channels in a pair-wise 13 | manner, a network of connect payment channels are created. 14 | 15 | Within the Lightning Network, 16 | [source-routing](https://en.wikipedia.org/wiki/Source_routing) is utilized in 17 | order to give nodes _full_ control over the route their payment follows within 18 | the network. This level of control is highly desirable as with it, senders are 19 | able to fully specify: the total number of hops in their routes, the total 20 | cumulative fee they'll pay to send the payment, and finally the total 21 | worst-case time-lock period enforced by the conditional payment contract. 22 | 23 | In line with Bitcoin's spirit of decentralization and censorship resistance, we 24 | employ an onion routing scheme within the [Lightning 25 | protocol](https://github.com/lightningnetwork/lightning-rfc) to prevent the 26 | ability of participants on the network to easily censor payments, as the 27 | participants are not aware of the final destination of any given payment. 28 | Additionally, by encoding payment routes within a mix-net like packet, we are 29 | able to achieve the following security and privacy features: 30 | 31 | * Participants in a route don't know their exact position within the route 32 | * Participants within a route don't know the source of the payment, nor the 33 | ultimate destination of the payment 34 | * Participants within a route aren't aware _exactly_ how many other 35 | participants were involved in the payment route 36 | * Each new payment route is computationally indistinguishable from any other 37 | payment route 38 | 39 | Our current onion routing protocol utilizes a message format derived from 40 | [Sphinx](http://www.cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf). In order 41 | to cater Sphinx's mix-format to our specification application, we've made the 42 | following modifications: 43 | 44 | * We've added a MAC over the entire mix-header as we have no use for SURB's 45 | (single-use-reply-blocks) in our protocol. 46 | * Additionally, the end-to-end payload to the destination has been removed in 47 | order to cut down on the packet-size, and also as we don't currently have a 48 | use for a large message from payment sender to recipient. 49 | * We've dropped usage of LIONESS (as we don't need SURB's), and instead 50 | utilize chacha20 uniformly throughout as a stream cipher. 51 | * Finally, the mix-header has been extended with a per-hop-payload which 52 | provides each hops with exact instructions as to how and where to forward 53 | the payment. This includes the amount to forward, the destination chain, 54 | and the time-lock value to attach to the outgoing HTLC. 55 | 56 | 57 | For further information see these resources: 58 | 59 | * [Olaoluwa's original post to the lightning-dev mailing 60 | list](http://lists.linuxfoundation.org/pipermail/lightning-dev/2015-December/000384.html). 61 | * [Privacy Preserving Decentralized Micropayments](https://scalingbitcoin.org/milan2016/presentations/D1%20-%206%20-%20Olaoluwa%20Osuntokun.pdf) -- presented at Scaling Bitcoin Hong Kong. 62 | 63 | 64 | In the near future, this repository will be extended to also includes a 65 | application specific version of 66 | [HORNET](https://www.scion-architecture.net/pdf/2015-HORNET.pdf). 67 | -------------------------------------------------------------------------------- /obfuscation.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/btcsuite/btcd/btcec/v2" 7 | ) 8 | 9 | // OnionErrorEncrypter is a struct that's used to implement onion error 10 | // encryption as defined within BOLT0004. 11 | type OnionErrorEncrypter struct { 12 | sharedSecret Hash256 13 | } 14 | 15 | // NewOnionErrorEncrypter creates new instance of the onion encrypter backed by 16 | // the passed router, with encryption to be done using the passed ephemeralKey. 17 | func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey, 18 | opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) { 19 | 20 | cfg := &processOnionCfg{} 21 | for _, o := range opts { 22 | o(cfg) 23 | } 24 | 25 | sharedSecret, err := router.generateSharedSecret( 26 | ephemeralKey, cfg.blindingPoint, 27 | ) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &OnionErrorEncrypter{ 33 | sharedSecret: sharedSecret, 34 | }, nil 35 | } 36 | 37 | // Encode writes the encrypter's shared secret to the provided io.Writer. 38 | func (o *OnionErrorEncrypter) Encode(w io.Writer) error { 39 | _, err := w.Write(o.sharedSecret[:]) 40 | return err 41 | } 42 | 43 | // Decode restores the encrypter's share secret from the provided io.Reader. 44 | func (o *OnionErrorEncrypter) Decode(r io.Reader) error { 45 | _, err := io.ReadFull(r, o.sharedSecret[:]) 46 | return err 47 | } 48 | 49 | // Circuit is used encapsulate the data which is needed for data deobfuscation. 50 | type Circuit struct { 51 | // SessionKey is the key which have been used during generation of the 52 | // shared secrets. 53 | SessionKey *btcec.PrivateKey 54 | 55 | // PaymentPath is the pub keys of the nodes in the payment path. 56 | PaymentPath []*btcec.PublicKey 57 | } 58 | 59 | // Decode initializes the circuit from the byte stream. 60 | func (c *Circuit) Decode(r io.Reader) error { 61 | var keyLength [1]byte 62 | if _, err := r.Read(keyLength[:]); err != nil { 63 | return err 64 | } 65 | 66 | sessionKeyData := make([]byte, uint8(keyLength[0])) 67 | if _, err := r.Read(sessionKeyData[:]); err != nil { 68 | return err 69 | } 70 | 71 | c.SessionKey, _ = btcec.PrivKeyFromBytes(sessionKeyData) 72 | var pathLength [1]byte 73 | if _, err := r.Read(pathLength[:]); err != nil { 74 | return err 75 | } 76 | c.PaymentPath = make([]*btcec.PublicKey, uint8(pathLength[0])) 77 | 78 | for i := 0; i < len(c.PaymentPath); i++ { 79 | var pubKeyData [btcec.PubKeyBytesLenCompressed]byte 80 | if _, err := r.Read(pubKeyData[:]); err != nil { 81 | return err 82 | } 83 | 84 | pubKey, err := btcec.ParsePubKey(pubKeyData[:]) 85 | if err != nil { 86 | return err 87 | } 88 | c.PaymentPath[i] = pubKey 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // Encode writes converted circuit in the byte stream. 95 | func (c *Circuit) Encode(w io.Writer) error { 96 | var keyLength [1]byte 97 | keyLength[0] = uint8(len(c.SessionKey.Serialize())) 98 | if _, err := w.Write(keyLength[:]); err != nil { 99 | return err 100 | } 101 | 102 | if _, err := w.Write(c.SessionKey.Serialize()); err != nil { 103 | return err 104 | } 105 | 106 | var pathLength [1]byte 107 | pathLength[0] = uint8(len(c.PaymentPath)) 108 | if _, err := w.Write(pathLength[:]); err != nil { 109 | return err 110 | } 111 | 112 | for _, pubKey := range c.PaymentPath { 113 | if _, err := w.Write(pubKey.SerializeCompressed()); err != nil { 114 | return err 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // OnionErrorDecrypter is a struct that's used to decrypt onion errors in 122 | // response to failed HTLC routing attempts according to BOLT#4. 123 | type OnionErrorDecrypter struct { 124 | circuit *Circuit 125 | } 126 | 127 | // NewOnionErrorDecrypter creates new instance of onion decrypter. 128 | func NewOnionErrorDecrypter(circuit *Circuit) *OnionErrorDecrypter { 129 | return &OnionErrorDecrypter{ 130 | circuit: circuit, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import "errors" 4 | 5 | // ErrAlreadyCommitted signals that an entry could not be added to the 6 | // batch because it has already been persisted. 7 | var ErrAlreadyCommitted = errors.New("cannot add to batch after committing") 8 | 9 | // Batch is an object used to incrementally construct a set of entries to add to 10 | // the replay log. After construction is completed, it can be added to the log 11 | // using the PutBatch method. 12 | type Batch struct { 13 | // IsCommitted denotes whether or not this batch has been successfully 14 | // written to disk. 15 | IsCommitted bool 16 | 17 | // ID is a unique, caller chosen identifier for this batch. 18 | ID []byte 19 | 20 | // ReplaySet contains the sequence numbers of all entries that were 21 | // detected as replays. The set is finalized upon writing the batch to 22 | // disk, and merges replays detected by the replay cache and on-disk 23 | // replay log. 24 | ReplaySet *ReplaySet 25 | 26 | // entries stores the set of all potential entries that might get 27 | // written to the replay log. Some entries may be skipped after 28 | // examining the on-disk content at the time of commit.. 29 | entries map[uint16]batchEntry 30 | 31 | // replayCache is an in memory lookup-table, which stores the hash 32 | // prefix of entries already added to this batch. This allows a quick 33 | // mechanism for intra-batch duplicate detection. 34 | replayCache map[HashPrefix]struct{} 35 | } 36 | 37 | // NewBatch initializes an object for constructing a set of entries to 38 | // atomically add to a replay log. Batches are identified by byte slice, which 39 | // allows the caller to safely process the same batch twice and get an 40 | // idempotent result. 41 | func NewBatch(id []byte) *Batch { 42 | return &Batch{ 43 | ID: id, 44 | ReplaySet: NewReplaySet(), 45 | entries: make(map[uint16]batchEntry), 46 | replayCache: make(map[HashPrefix]struct{}), 47 | } 48 | } 49 | 50 | // Put inserts a hash-prefix/CLTV pair into the current batch. This method only 51 | // returns an error in the event that the batch was already committed to disk. 52 | // Decisions regarding whether or not a particular sequence number is a replay 53 | // is ultimately reported via the batch's ReplaySet after committing to disk. 54 | func (b *Batch) Put(seqNum uint16, hashPrefix *HashPrefix, cltv uint32) error { 55 | // Abort if this batch was already written to disk. 56 | if b.IsCommitted { 57 | return ErrAlreadyCommitted 58 | } 59 | 60 | // Check to see if this hash prefix is already included in this batch. 61 | // If so, we will opportunistically mark this index as replayed. 62 | if _, ok := b.replayCache[*hashPrefix]; ok { 63 | b.ReplaySet.Add(seqNum) 64 | return nil 65 | } 66 | 67 | // Otherwise, this is a distinct hash prefix for this batch. Add it to 68 | // our list of entries that we will try to write to disk. Each of these 69 | // entries will be checked again during the commit to see if any other 70 | // on-disk entries contain the same hash prefix. 71 | b.entries[seqNum] = batchEntry{ 72 | hashPrefix: *hashPrefix, 73 | cltv: cltv, 74 | } 75 | 76 | // Finally, add this hash prefix to our in-memory replay cache, this 77 | // will be consulted upon further adds to check for duplicates in the 78 | // same batch. 79 | b.replayCache[*hashPrefix] = struct{}{} 80 | 81 | return nil 82 | } 83 | 84 | // ForEach iterates through each entry in the batch and calls the provided 85 | // function with the sequence number and entry contents as arguments. 86 | func (b *Batch) ForEach(fn func(seqNum uint16, hashPrefix *HashPrefix, cltv uint32) error) error { 87 | for seqNum, entry := range b.entries { 88 | if err := fn(seqNum, &entry.hashPrefix, entry.cltv); err != nil { 89 | return err 90 | } 91 | } 92 | return nil 93 | } 94 | 95 | // batchEntry is a tuple of a secret's hash prefix and the corresponding CLTV at 96 | // which the onion blob from which the secret was derived expires. 97 | type batchEntry struct { 98 | hashPrefix HashPrefix 99 | cltv uint32 100 | } 101 | -------------------------------------------------------------------------------- /replaylog_test.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // TestMemoryReplayLogStorageAndRetrieval tests that the non-batch methods on 8 | // MemoryReplayLog work as expected. 9 | func TestMemoryReplayLogStorageAndRetrieval(t *testing.T) { 10 | rl := NewMemoryReplayLog() 11 | rl.Start() 12 | defer rl.Stop() 13 | 14 | var hashPrefix HashPrefix 15 | hashPrefix[0] = 1 16 | 17 | var cltv1 uint32 = 1 18 | 19 | // Attempt to lookup unknown sphinx packet. 20 | _, err := rl.Get(&hashPrefix) 21 | if err == nil { 22 | t.Fatalf("Expected ErrLogEntryNotFound") 23 | } 24 | if err != ErrLogEntryNotFound { 25 | t.Fatalf("Get failed - received unexpected error upon Get: %v", err) 26 | } 27 | 28 | // Log incoming sphinx packet. 29 | err = rl.Put(&hashPrefix, cltv1) 30 | if err != nil { 31 | t.Fatalf("Put failed - received unexpected error upon Put: %v", err) 32 | } 33 | 34 | // Attempt to replay sphinx packet. 35 | err = rl.Put(&hashPrefix, cltv1) 36 | if err == nil { 37 | t.Fatalf("Expected ErrReplayedPacket") 38 | } 39 | if err != ErrReplayedPacket { 40 | t.Fatalf("Put failed - received unexpected error upon Put: %v", err) 41 | } 42 | 43 | // Lookup logged sphinx packet. 44 | cltv, err := rl.Get(&hashPrefix) 45 | if err != nil { 46 | t.Fatalf("Get failed - received unexpected error upon Get: %v", err) 47 | } 48 | if cltv != cltv1 { 49 | t.Fatalf("Get returned wrong value: expected %v, got %v", cltv1, cltv) 50 | } 51 | 52 | // Delete sphinx packet from log. 53 | err = rl.Delete(&hashPrefix) 54 | if err != nil { 55 | t.Fatalf("Delete failed - received unexpected error upon Delete: %v", err) 56 | } 57 | 58 | // Attempt to lookup deleted sphinx packet. 59 | _, err = rl.Get(&hashPrefix) 60 | if err == nil { 61 | t.Fatalf("Expected ErrLogEntryNotFound") 62 | } 63 | if err != ErrLogEntryNotFound { 64 | t.Fatalf("Get failed - received unexpected error upon Get: %v", err) 65 | } 66 | 67 | // Reinsert incoming sphinx packet into the log. 68 | var cltv2 uint32 = 2 69 | err = rl.Put(&hashPrefix, cltv2) 70 | if err != nil { 71 | t.Fatalf("Put failed - received unexpected error upon Put: %v", err) 72 | } 73 | 74 | // Lookup logged sphinx packet. 75 | cltv, err = rl.Get(&hashPrefix) 76 | if err != nil { 77 | t.Fatalf("Get failed - received unexpected error upon Get: %v", err) 78 | } 79 | if cltv != cltv2 { 80 | t.Fatalf("Get returned wrong value: expected %v, got %v", cltv2, cltv) 81 | } 82 | } 83 | 84 | // TestMemoryReplayLogPutBatch tests that the batch adding of packets to a log 85 | // works as expected. 86 | func TestMemoryReplayLogPutBatch(t *testing.T) { 87 | rl := NewMemoryReplayLog() 88 | rl.Start() 89 | defer rl.Stop() 90 | 91 | var hashPrefix1, hashPrefix2 HashPrefix 92 | hashPrefix1[0] = 1 93 | hashPrefix2[0] = 2 94 | 95 | // Create a batch with a duplicated packet. 96 | batch1 := NewBatch([]byte{1}) 97 | err := batch1.Put(1, &hashPrefix1, 1) 98 | if err != nil { 99 | t.Fatalf("Unexpected error adding entry to batch: %v", err) 100 | } 101 | err = batch1.Put(1, &hashPrefix1, 1) 102 | if err != nil { 103 | t.Fatalf("Unexpected error adding entry to batch: %v", err) 104 | } 105 | 106 | replays, err := rl.PutBatch(batch1) 107 | if replays.Size() != 1 || !replays.Contains(1) { 108 | t.Fatalf("Unexpected replay set after adding batch 1 to log: %v", err) 109 | } 110 | 111 | // Create a batch with one replayed packet and one valid one. 112 | batch2 := NewBatch([]byte{2}) 113 | err = batch2.Put(1, &hashPrefix1, 1) 114 | if err != nil { 115 | t.Fatalf("Unexpected error adding entry to batch: %v", err) 116 | } 117 | err = batch2.Put(2, &hashPrefix2, 2) 118 | if err != nil { 119 | t.Fatalf("Unexpected error adding entry to batch: %v", err) 120 | } 121 | 122 | replays, err = rl.PutBatch(batch2) 123 | if replays.Size() != 1 || !replays.Contains(1) { 124 | t.Fatalf("Unexpected replay set after adding batch 2 to log: %v", err) 125 | } 126 | 127 | // Reprocess batch 2, which should be idempotent. 128 | replays, err = rl.PutBatch(batch2) 129 | if replays.Size() != 1 || !replays.Contains(1) { 130 | t.Fatalf("Unexpected replay set after adding batch 2 to log: %v", err) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /hornet.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/btcec/v2" 5 | "golang.org/x/crypto/ripemd160" 6 | ) 7 | 8 | // TODO(roasbeef): Might need to change? due to the PRG* requirements? 9 | const fSLength = 48 10 | 11 | // Hmm appears that they use k = 128 throughout the paper? 12 | 13 | // HMAC -> SHA-256 14 | // * or could use Poly1035: https://godoc.org/golang.org/x/crypto/poly1305 15 | // * but, the paper specs: {0, 1}^k x {0, 1}* -> {0, 1}^k 16 | // * Poly1035 is actually: {0, 1}^k x {0, 1}* -> {0, 1}^(2/k) 17 | // * Also with Poly, I guess the key is treated as a nonce, tagging two messages 18 | // with the same key allows an attacker to forge message or something like that 19 | 20 | // Size of a forwarding segment is 32 bytes, the MAC is 16 bytes, so c = 48 bytes 21 | // * NOTE: this doesn't include adding R to the forwarding segment, and w/e esle 22 | 23 | // Hmmm since each uses diff key, just use AES-CTR with blank nonce, given key, 24 | // encrypt plaintext of all zeros, this'll give us our len(plaintext) rand bytes. 25 | // PRG0 -> {0, 1}^k -> {0, 1}^r(c+k) or {0, 1}^1280 (assuming 20 hops, like rusty, but, is that too large? maybe, idk) 26 | // PRG1 -> {0, 1}^k -> {0, 1}^r(c+k) or {0, 1}^1280 (assuming 20 hops) 27 | // PRG2 -> {0, 1}^k -> {0, 1}^rc or {0, 1}^960 (assuming 20 hops, c=48) 28 | // * NOTE: in second version of paper (accepted to CCS'15), all the PRG*'s are like PRG2 29 | // * so makes it simpler 30 | 31 | // PRP -> AES? or 32 | // * {0, 1}^k x {0, 1}^a -> {0, 1}^a 33 | 34 | // Do we need AEAD for the below? Or are is the per-hop MAC okay? 35 | // ENC: AES-CTR or CHACHA20? 36 | 37 | // DEC: AES-CTR or CHACHA20? 38 | 39 | // h_op: G^* -> {0, 1}^k 40 | // * op (elem of) {MAC, PRGO, PRG!, PRP, ENC, DEC} 41 | // * key gen for the above essentially 42 | 43 | // RoutingSegment... 44 | // NOTE: Length of routing segment in the paper is 8 bytes (enough for their 45 | // imaginary network, I guess). But, looking like they'll be (20 + 33 bytes) 46 | // 53 bytes. Or 52 if we use curve25519 47 | type routingSegment struct { 48 | nextHop *btcec.PublicKey // NOTE: or, is this a LN addr? w/e that is? 49 | // nextHop [32]byte 50 | rCommitment [ripemd160.Size]byte 51 | 52 | // stuff perhaps? 53 | } 54 | 55 | // SphinxPayload... 56 | type sphinxPayload struct { 57 | } 58 | 59 | // ForwardingSegment.... 60 | type forwardingSegment struct { 61 | // Here's hash(R), attempt to make an HTLC with the next hop. If 62 | // successful, then pass along the onion so we can finish getting the 63 | // payment circuit set up. 64 | // TODO(roasbeef): Do we create HTLC's with the minimum amount 65 | // possible? 1 satoshi or is it 1 mili-satoshi? 66 | rs routingSegment 67 | 68 | // To defend against replay attacks. Intermediate nodes will drop the 69 | // FS if it deems it's expired. 70 | expiration uint64 71 | 72 | // Key shared by intermediate node with the source, used to peel a layer 73 | // off the onion for the next hop. 74 | sharedSymmetricKey [32]byte // TODO(roasbeef): or, 16? 75 | } 76 | 77 | // AnonymousHeader... 78 | type anonymousHeader struct { 79 | // Forwarding info for the current hop. When serialized, it'll be 80 | // encrypted with SV, the secret key for this node known to no-one but 81 | // the node. It also contains a secret key shared with this node and the 82 | // source, so it can peel off a layer of the onion for the next hop. 83 | fs forwardingSegment 84 | 85 | mac [32]byte // TODO(roasbeef): or, 16? 86 | } 87 | 88 | // CommonHeader... 89 | type commonHeader struct { 90 | // TODO(roasbeef): maybe can use this to extend HORNET with additiona control signals 91 | // for LN nodes? 92 | controlType uint8 93 | hops uint8 94 | nonce [8]byte // either interpreted as EXP or nonce, little-endian? idk 95 | } 96 | 97 | // DataPacket... 98 | type dataPacket struct { 99 | chdr commonHeader 100 | ahdr anonymousHeader // TODO(roasbeef): MAC in ahdr includes the chdr? 101 | onion [fSLength * NumMaxHops]byte // TODO(roasbeef): or, is it NumMaxHops - 1? 102 | } 103 | 104 | type sphinxHeader struct { 105 | } 106 | 107 | // SessionSetupPacket... 108 | type sessionSetupPacket struct { 109 | chdr commonHeader 110 | shdr sphinxHeader 111 | sp sphinxPayload 112 | fsPayload [fSLength * NumMaxHops]byte // ? r*c 113 | // TODO(roabeef): hmm does this implcitly mean messages are a max of 48 bytes? 114 | } 115 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 3 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 4 | github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= 5 | github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= 6 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= 7 | github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 8 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= 9 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 10 | github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= 11 | github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 15 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 20 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 25 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 26 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 27 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 31 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 32 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 33 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 34 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 35 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 36 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 37 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 38 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw= 40 | github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 41 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 42 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 43 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 44 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 48 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 49 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 50 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | -------------------------------------------------------------------------------- /testdata/onion-test-multi-frame.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "A testcase for a variable length hop_payload. The third payload is 256 bytes long.", 3 | "generate": { 4 | "session_key": "4141414141414141414141414141414141414141414141414141414141414141", 5 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 6 | "hops": [ 7 | { 8 | "type": "legacy", 9 | "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", 10 | "payload": "0000000000000000000000000000000000000000" 11 | }, 12 | { 13 | "type": "tlv", 14 | "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 15 | "payload": "0101010101010101000000000000000100000001" 16 | }, 17 | { 18 | "type": "raw", 19 | "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", 20 | "payload": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" 21 | }, 22 | { 23 | "type": "tlv", 24 | "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", 25 | "payload": "0303030303030303000000000000000300000003" 26 | }, 27 | { 28 | "type": "legacy", 29 | "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", 30 | "payload": "0404040404040404000000000000000400000004" 31 | } 32 | ] 33 | }, 34 | "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a710f8eaf9ccc768f66bb5dec1f7827f33c43fe2ddd05614c8283aa78e9e7573f87c50f7d61ab590531cf08000178a333a347f8b4072e1cea42da7552402b10765adae3f581408f35ff0a71a34b78b1d8ecae77df96c6404bae9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf057f5995d9731c4bf796fb0e41c885d488dcbc68eb742e27f44310b276edc6f652658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d710ee5c193921909bdd75db331cd9d7581a39fca50814ed8d9d402b86e7f8f6ac2f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa3379834bc2380d58e9d23237821475a1874484783a15d68f47d3dc339f38d9bf925655d5c946778680fd6d1f062f84128895aff09d35d6c92cca63d3f95a9ee8f2a84f383b4d6a087533e65de12fc8dcaf85777736a2088ff4b22462265028695b37e70963c10df8ef2458756c73007dc3e544340927f9e9f5ea4816a9fd9832c311d122e9512739a6b4714bba590e31caa143ce83cb84b36c738c60c3190ff70cd9ac286a9fd2ab619399b68f1f7447be376ce884b5913c8496d01cbf7a44a60b6e6747513f69dc538f340bc1388e0fde5d0c1db50a4dcb9cc0576e0e2474e4853af9623212578d502757ffb2e0e749695ed70f61c116560d0d4154b64dcf3cbf3c91d89fb6dd004dc19588e3479fcc63c394a4f9e8a3b8b961fce8a532304f1337f1a697a1bb14b94d2953f39b73b6a3125d24f27fcd4f60437881185370bde68a5454d816e7a70d4cea582effab9a4f1b730437e35f7a5c4b769c7b72f0346887c1e63576b2f1e2b3706142586883f8cf3a23595cc8e35a52ad290afd8d2f8bcd5b4c1b891583a4159af7110ecde092079209c6ec46d2bda60b04c519bb8bc6dffb5c87f310814ef2f3003671b3c90ddf5d0173a70504c2280d31f17c061f4bb12a978122c8a2a618bb7d1edcf14f84bf0fa181798b826a254fca8b6d7c81e0beb01bd77f6461be3c8647301d02b04753b0771105986aa0cbc13f7718d64e1b3437e8eef1d319359914a7932548c91570ef3ea741083ca5be5ff43c6d9444d29df06f76ec3dc936e3d180f4b6d0fbc495487c7d44d7c8fe4a70d5ff1461d0d9593f3f898c919c363fa18341ce9dae54f898ccf3fe792136682272941563387263c51b2a2f32363b804672cc158c9230472b554090a661aa81525d11876eefdcc45442249e61e07284592f1606491de5c0324d3af4be035d7ede75b957e879e9770cdde2e1bbc1ef75d45fe555f1ff6ac296a2f648eeee59c7c08260226ea333c285bcf37a9bbfa57ba2ab8083c4be6fc2ebe279537d22da96a07392908cf22b233337a74fe5c603b51712b43c3ee55010ee3d44dd9ba82bba3145ec358f863e04bbfa53799a7a9216718fd5859da2f0deb77b8e315ad6868fdec9400f45a48e6dc8ddbaeb3", 35 | "decode": [ 36 | "4141414141414141414141414141414141414141414141414141414141414141", 37 | "4242424242424242424242424242424242424242424242424242424242424242", 38 | "4343434343434343434343434343434343434343434343434343434343434343", 39 | "4444444444444444444444444444444444444444444444444444444444444444", 40 | "4545454545454545454545454545454545454545454545454545454545454545" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /testdata/onion-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.", 3 | "generate": { 4 | "session_key": "4141414141414141414141414141414141414141414141414141414141414141", 5 | "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", 6 | "hops": [ 7 | { 8 | "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", 9 | "payload": "1202023a98040205dc06080000000000000001" 10 | }, 11 | { 12 | "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 13 | "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" 14 | }, 15 | { 16 | "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", 17 | "payload": "12020230d4040204e206080000000000000003" 18 | }, 19 | { 20 | "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", 21 | "payload": "1202022710040203e806080000000000000004" 22 | }, 23 | { 24 | "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", 25 | "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" 26 | } 27 | ] 28 | }, 29 | "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20", 30 | "decode": [ 31 | "4141414141414141414141414141414141414141414141414141414141414141", 32 | "4242424242424242424242424242424242424242424242424242424242424242", 33 | "4343434343434343434343434343434343434343434343434343434343434343", 34 | "4444444444444444444444444444444444444444444444444444444444444444", 35 | "4545454545454545454545454545454545454545454545454545454545454545" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG := github.com/lightningnetwork/lightning-onion 2 | TOOLS_DIR := tools 3 | 4 | GOBUILD := GO111MODULE=on go build -v 5 | GOINSTALL := GO111MODULE=on go install -v 6 | 7 | GOFILES = $(shell find . -type f -name '*.go') 8 | 9 | RM := rm -f 10 | CP := cp 11 | MAKE := make 12 | XARGS := xargs -L 1 13 | 14 | # GO_VERSION is the Go version used for the release build, docker files, and 15 | # GitHub Actions. This is the reference version for the project. All other Go 16 | # versions are checked against this version. 17 | GO_VERSION = 1.24.6 18 | 19 | # Linting uses a lot of memory, so keep it under control by limiting the number 20 | # of workers if requested. 21 | ifneq ($(workers),) 22 | LINT_WORKERS = --concurrency=$(workers) 23 | endif 24 | 25 | DOCKER_TOOLS = docker run \ 26 | --rm \ 27 | -v $(shell bash -c "mkdir -p /tmp/go-build-cache; echo /tmp/go-build-cache"):/root/.cache/go-build \ 28 | -v $$(pwd):/build lightning-onion-tools 29 | 30 | GREEN := "\\033[0;32m" 31 | NC := "\\033[0m" 32 | define print 33 | echo $(GREEN)$1$(NC) 34 | endef 35 | 36 | #? default: Run `make build` 37 | default: build 38 | 39 | #? all: Run `make build` and `make check` 40 | all: build check 41 | 42 | # ============ 43 | # INSTALLATION 44 | # ============ 45 | 46 | #? build: Compile and build lightning-onion 47 | build: 48 | @$(call print, "Compiling lightning-onion.") 49 | $(GOBUILD) $(PKG)/... 50 | 51 | #? sphinx-cli: Build the sphinx-cli binary 52 | sphinx-cli: 53 | @$(call print, "Building sphinx-cli.") 54 | $(GOBUILD) -o sphinx-cli ./cmd/main.go 55 | 56 | # ======= 57 | # TESTING 58 | # ======= 59 | 60 | #? check: Run `make unit` 61 | check: unit 62 | 63 | #? unit: Run unit tests 64 | unit: 65 | @$(call print, "Running unit tests.") 66 | go test -v ./... 67 | 68 | #? unit-cover: Run unit coverage tests 69 | unit-cover: 70 | @$(call print, "Running unit coverage tests.") 71 | go test -coverprofile=coverage.txt -covermode=atomic ./... 72 | 73 | #? unit-race: Run unit race tests 74 | unit-race: 75 | @$(call print, "Running unit race tests.") 76 | env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" go test -race ./... 77 | 78 | # ========= 79 | # UTILITIES 80 | # ========= 81 | 82 | #? fmt: Fix imports and format source code 83 | fmt: docker-tools 84 | @$(call print, "Fixing imports.") 85 | $(DOCKER_TOOLS) gosimports -w $(GOFILES) 86 | @$(call print, "Formatting source.") 87 | $(DOCKER_TOOLS) gofmt -l -w -s $(GOFILES) 88 | 89 | #? check-go-version-yaml: Verify that the Go version is correct in all YAML files 90 | check-go-version-yaml: 91 | @$(call print, "Checking for target Go version (v$(GO_VERSION)) in YAML files (*.yaml, *.yml)") 92 | ./scripts/check-go-version-yaml.sh $(GO_VERSION) 93 | 94 | #? check-go-version-dockerfile: Verify that the Go version is correct in all Dockerfile files 95 | check-go-version-dockerfile: 96 | @$(call print, "Checking for target Go version (v$(GO_VERSION)) in Dockerfile files (*Dockerfile)") 97 | ./scripts/check-go-version-dockerfile.sh $(GO_VERSION) 98 | 99 | #? check-go-version: Verify that the Go version is correct in all project files 100 | check-go-version: check-go-version-dockerfile check-go-version-yaml 101 | 102 | #? fmt-check: Make sure source code is formatted and imports are correct 103 | fmt-check: fmt 104 | @$(call print, "Checking fmt results.") 105 | if test -n "$$(git status --porcelain)"; then echo "code not formatted correctly, please run `make fmt` again!"; git status; git diff; exit 1; fi 106 | 107 | #? lint-config-check: Verify golangci-lint configuration 108 | lint-config-check: docker-tools 109 | @$(call print, "Verifying golangci-lint configuration.") 110 | $(DOCKER_TOOLS) golangci-lint config verify -v 111 | 112 | #? lint: Lint source and check errors 113 | lint: check-go-version lint-config-check 114 | @$(call print, "Linting source.") 115 | $(DOCKER_TOOLS) golangci-lint run -v $(LINT_WORKERS) 116 | 117 | #? docker-tools: Build tools docker image 118 | docker-tools: 119 | @$(call print, "Building tools docker image.") 120 | docker build -q -t lightning-onion-tools -f $(TOOLS_DIR)/Dockerfile . 121 | 122 | #? clean: Clean source 123 | clean: 124 | @$(call print, "Cleaning source.$(NC)") 125 | $(RM) coverage.txt 126 | 127 | #? tidy-module: Run 'go mod tidy' for all modules 128 | tidy-module: 129 | @$(call print, "Running 'go mod tidy' for main module") 130 | go mod tidy 131 | @$(call print, "Running 'go mod tidy' for tools module") 132 | cd $(TOOLS_DIR) && go mod tidy 133 | 134 | #? tidy-module-check: Run 'go mod tidy' for all modules and check results 135 | tidy-module-check: tidy-module 136 | if test -n "$$(git status --porcelain)"; then echo "modules not updated, please run `make tidy-module` again!"; git status; exit 1; fi 137 | 138 | .PHONY: all \ 139 | default \ 140 | build \ 141 | sphinx-cli \ 142 | check \ 143 | unit \ 144 | unit-cover \ 145 | unit-race \ 146 | fmt \ 147 | fmt-check \ 148 | tidy-module \ 149 | tidy-module-check \ 150 | lint \ 151 | lint-config-check \ 152 | docker-tools \ 153 | clean 154 | 155 | #? help: Get more info on make commands 156 | help: Makefile 157 | @echo " Choose a command run in lightning-onion:" 158 | @sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /' 159 | 160 | .PHONY: help 161 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | # If you change this please run `make lint` to see where else it needs to be 4 | # updated as well. 5 | go: "1.24.6" 6 | 7 | # timeout for analysis 8 | timeout: 10m 9 | 10 | linters: 11 | default: all 12 | disable: 13 | # Global variables are used in many places throughout the code base. 14 | - gochecknoglobals 15 | 16 | # We want to allow short variable names. 17 | - varnamelen 18 | 19 | # We want to allow TODOs. 20 | - godox 21 | 22 | # Init functions are used by loggers throughout the codebase. 23 | - gochecknoinits 24 | 25 | # Allow using default empty values. 26 | - exhaustruct 27 | 28 | # Allow tests to be put in the same package. 29 | - testpackage 30 | 31 | # Disable tagalign. 32 | - tagalign 33 | 34 | # TODO(yy): create a list of allowed packages to import before enabling 35 | # this linter. 36 | - depguard 37 | 38 | # Deprecated - this is replaced by wsl_v5. 39 | - wsl 40 | 41 | # All available settings of specific linters. 42 | settings: 43 | nlreturn: 44 | # Size of the block (including return statement that is still "OK") 45 | # so no return split required. 46 | block-size: 3 47 | 48 | funlen: 49 | # Checks the number of lines in a function. 50 | # If lower than 0, disable the check. 51 | lines: 100 52 | # Checks the number of statements in a function. 53 | statements: 50 54 | 55 | tagliatelle: 56 | case: 57 | rules: 58 | json: snake 59 | 60 | wsl_v5: 61 | # We adopt a more relaxed cuddling rule by enabling 62 | # `allow-whole-block`. This allows a variable declaration to be 63 | # "cuddled" with a following block if the variable is used anywhere 64 | # within that block, not just as the first statement. 65 | allow-whole-block: true 66 | allow-first-in-block: false 67 | 68 | # Disable the leading-whitespace check to resolve a conflict with the 69 | # whitespace linter, which requires a blank line after a function 70 | # signature. This is the standard Go style. 71 | disable: 72 | - leading-whitespace 73 | 74 | lll: 75 | # Max line length, lines longer will be reported. 76 | # '\t' is counted as 1 character by default, and can be changed with the 77 | # tab-width option. 78 | # Default: 120. 79 | line-length: 80 80 | # Tab width in spaces. 81 | # Default: 1 82 | tab-width: 8 83 | 84 | whitespace: 85 | multi-func: true 86 | multi-if: true 87 | 88 | # Defines a set of rules to ignore issues. 89 | # It does not skip the analysis, and so does not ignore "typecheck" errors. 90 | exclusions: 91 | # Mode of the generated files analysis. 92 | # 93 | # - `strict`: sources are excluded by strictly following the Go generated file convention. 94 | # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` 95 | # This line must appear before the first non-comment, non-blank text in the file. 96 | # https://go.dev/s/generatedcode 97 | # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. 98 | # - `disable`: disable the generated files exclusion. 99 | # 100 | # Default: strict 101 | generated: lax 102 | 103 | # Which file paths to exclude: they will be analyzed, but issues from them won't be reported. 104 | # "/" will be replaced by the current OS file path separator to properly work on Windows. 105 | # Default: [] 106 | paths: 107 | - rpc/legacyrpc/ 108 | - wallet/deprecated.go 109 | 110 | rules: 111 | # Exclude gosec from running for tests so that tests with weak randomness 112 | # (math/rand) will pass the linter. 113 | - path: _test\.go 114 | linters: 115 | - gosec 116 | - funlen 117 | - revive 118 | # Allow duplications in tests so it's easier to follow a single unit 119 | # test. 120 | - dupl 121 | # Allow returning unwrapped errors in tests. 122 | - wrapcheck 123 | 124 | - path: mock* 125 | linters: 126 | - revive 127 | # forcetypeassert is skipped for the mock because the test would fail if 128 | # the returned value doesn't match the type, so there's no need to check 129 | # the convert. 130 | - forcetypeassert 131 | 132 | # Allow fmt.Printf() in commands. 133 | - path: cmd/commands/* 134 | linters: 135 | - forbidigo 136 | 137 | # Allow fmt.Printf() in config parsing. 138 | - path: config\.go 139 | linters: 140 | - forbidigo 141 | 142 | issues: 143 | # Show only new issues created after git revision `REV`. 144 | # Default: "" 145 | new-from-rev: 49d94f09c3382dcc9e5c0f61089109d54213b7c5 146 | # Maximum issues count per one linter. 147 | # Set to 0 to disable. 148 | # Default: 50 149 | max-issues-per-linter: 0 150 | # Maximum count of issues with the same text. 151 | # Set to 0 to disable. 152 | # Default: 3 153 | max-same-issues: 0 154 | # Make issues output unique by line. 155 | # Default: true 156 | uniq-by-line: false 157 | -------------------------------------------------------------------------------- /replaylog.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "crypto/sha256" 5 | "errors" 6 | ) 7 | 8 | const ( 9 | // HashPrefixSize is the size in bytes of the keys we will be storing 10 | // in the ReplayLog. It represents the first 20 bytes of a truncated 11 | // sha-256 hash of a secret generated by ECDH. 12 | HashPrefixSize = 20 13 | ) 14 | 15 | // HashPrefix is a statically size, 20-byte array containing the prefix 16 | // of a Hash256, and is used to detect duplicate sphinx packets. 17 | type HashPrefix [HashPrefixSize]byte 18 | 19 | // errReplayLogAlreadyStarted is an error returned when Start() is called on a 20 | // ReplayLog after it is started and before it is stopped. 21 | var errReplayLogAlreadyStarted error = errors.New( 22 | "Replay log has already been started") 23 | 24 | // errReplayLogNotStarted is an error returned when methods other than Start() 25 | // are called on a ReplayLog before it is started or after it is stopped. 26 | var errReplayLogNotStarted error = errors.New( 27 | "Replay log has not been started") 28 | 29 | // hashSharedSecret Sha-256 hashes the shared secret and returns the first 30 | // HashPrefixSize bytes of the hash. 31 | func hashSharedSecret(sharedSecret *Hash256) *HashPrefix { 32 | // Sha256 hash of sharedSecret 33 | h := sha256.New() 34 | h.Write(sharedSecret[:]) 35 | 36 | var sharedHash HashPrefix 37 | 38 | // Copy bytes to sharedHash 39 | copy(sharedHash[:], h.Sum(nil)) 40 | return &sharedHash 41 | } 42 | 43 | // ReplayLog is an interface that defines a log of incoming sphinx packets, 44 | // enabling strong replay protection. The interface is general to allow 45 | // implementations near-complete autonomy. All methods must be safe for 46 | // concurrent access. 47 | type ReplayLog interface { 48 | // Start starts up the log. It returns an error if one occurs. 49 | Start() error 50 | 51 | // Stop safely stops the log. It returns an error if one occurs. 52 | Stop() error 53 | 54 | // Get retrieves an entry from the log given its hash prefix. It returns the 55 | // value stored and an error if one occurs. It returns ErrLogEntryNotFound 56 | // if the entry is not in the log. 57 | Get(*HashPrefix) (uint32, error) 58 | 59 | // Put stores an entry into the log given its hash prefix and an 60 | // accompanying purposefully general type. It returns ErrReplayedPacket if 61 | // the provided hash prefix already exists in the log. 62 | Put(*HashPrefix, uint32) error 63 | 64 | // Delete deletes an entry from the log given its hash prefix. 65 | Delete(*HashPrefix) error 66 | 67 | // PutBatch stores a batch of sphinx packets into the log given their hash 68 | // prefixes and accompanying values. Returns the set of entries in the batch 69 | // that are replays and an error if one occurs. 70 | PutBatch(*Batch) (*ReplaySet, error) 71 | } 72 | 73 | // MemoryReplayLog is a simple ReplayLog implementation that stores all added 74 | // sphinx packets and processed batches in memory with no persistence. 75 | // 76 | // This is designed for use just in testing. 77 | type MemoryReplayLog struct { 78 | batches map[string]*ReplaySet 79 | entries map[HashPrefix]uint32 80 | } 81 | 82 | // NewMemoryReplayLog constructs a new MemoryReplayLog. 83 | func NewMemoryReplayLog() *MemoryReplayLog { 84 | return &MemoryReplayLog{} 85 | } 86 | 87 | // Start initializes the log and must be called before any other methods. 88 | func (rl *MemoryReplayLog) Start() error { 89 | rl.batches = make(map[string]*ReplaySet) 90 | rl.entries = make(map[HashPrefix]uint32) 91 | return nil 92 | } 93 | 94 | // Stop wipes the state of the log. 95 | func (rl *MemoryReplayLog) Stop() error { 96 | if rl.entries == nil || rl.batches == nil { 97 | return errReplayLogNotStarted 98 | } 99 | 100 | rl.batches = nil 101 | rl.entries = nil 102 | return nil 103 | } 104 | 105 | // Get retrieves an entry from the log given its hash prefix. It returns the 106 | // value stored and an error if one occurs. It returns ErrLogEntryNotFound 107 | // if the entry is not in the log. 108 | func (rl *MemoryReplayLog) Get(hash *HashPrefix) (uint32, error) { 109 | if rl.entries == nil || rl.batches == nil { 110 | return 0, errReplayLogNotStarted 111 | } 112 | 113 | cltv, exists := rl.entries[*hash] 114 | if !exists { 115 | return 0, ErrLogEntryNotFound 116 | } 117 | 118 | return cltv, nil 119 | } 120 | 121 | // Put stores an entry into the log given its hash prefix and an accompanying 122 | // purposefully general type. It returns ErrReplayedPacket if the provided hash 123 | // prefix already exists in the log. 124 | func (rl *MemoryReplayLog) Put(hash *HashPrefix, cltv uint32) error { 125 | if rl.entries == nil || rl.batches == nil { 126 | return errReplayLogNotStarted 127 | } 128 | 129 | _, exists := rl.entries[*hash] 130 | if exists { 131 | return ErrReplayedPacket 132 | } 133 | 134 | rl.entries[*hash] = cltv 135 | return nil 136 | } 137 | 138 | // Delete deletes an entry from the log given its hash prefix. 139 | func (rl *MemoryReplayLog) Delete(hash *HashPrefix) error { 140 | if rl.entries == nil || rl.batches == nil { 141 | return errReplayLogNotStarted 142 | } 143 | 144 | delete(rl.entries, *hash) 145 | return nil 146 | } 147 | 148 | // PutBatch stores a batch of sphinx packets into the log given their hash 149 | // prefixes and accompanying values. Returns the set of entries in the batch 150 | // that are replays and an error if one occurs. 151 | func (rl *MemoryReplayLog) PutBatch(batch *Batch) (*ReplaySet, error) { 152 | if rl.entries == nil || rl.batches == nil { 153 | return nil, errReplayLogNotStarted 154 | } 155 | 156 | // Return the result when the batch was first processed to provide 157 | // idempotence. 158 | replays, exists := rl.batches[string(batch.ID)] 159 | 160 | if !exists { 161 | replays = NewReplaySet() 162 | err := batch.ForEach(func(seqNum uint16, hashPrefix *HashPrefix, cltv uint32) error { 163 | err := rl.Put(hashPrefix, cltv) 164 | if err == ErrReplayedPacket { 165 | replays.Add(seqNum) 166 | return nil 167 | } 168 | 169 | // An error would be bad because we have already updated the entries 170 | // map, but no errors other than ErrReplayedPacket should occur. 171 | return err 172 | }) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | replays.Merge(batch.ReplaySet) 178 | rl.batches[string(batch.ID)] = replays 179 | } 180 | 181 | batch.ReplaySet = replays 182 | batch.IsCommitted = true 183 | 184 | return replays, nil 185 | } 186 | 187 | // A compile time asserting *MemoryReplayLog implements the RelayLog interface. 188 | var _ ReplayLog = (*MemoryReplayLog)(nil) 189 | -------------------------------------------------------------------------------- /.gemini/styleguide.md: -------------------------------------------------------------------------------- 1 | # LND Style Guide 2 | 3 | ## Code Documentation and Commenting 4 | 5 | - Always use the Golang code style described below in this document. 6 | - Readable code is the most important requirement for any commit created. 7 | - Comments must not explain the code 1:1 but instead explain the _why_ behind a 8 | certain block of code, in case it requires contextual knowledge. 9 | - Unit tests must always use the `require` library. Either table driven unit 10 | tests or tests using the `rapid` library are preferred. 11 | - The line length MUST NOT exceed 80 characters, this is very important. 12 | You must count the Golang indentation (tabulator character) as 8 spaces when 13 | determining the line length. Use creative approaches or the wrapping rules 14 | specified below to make sure the line length isn't exceeded. 15 | - Every function must be commented with its purpose and assumptions. 16 | - Function comments must begin with the function name. 17 | - Function comments should be complete sentences. 18 | - Exported functions require detailed comments for the caller. 19 | 20 | **WRONG** 21 | ```go 22 | // generates a revocation key 23 | func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey, 24 | revokePreimage []byte) *btcec.PublicKey { 25 | ``` 26 | **RIGHT** 27 | ```go 28 | // DeriveRevocationPubkey derives the revocation public key given the 29 | // counterparty's commitment key, and revocation preimage derived via a 30 | // pseudo-random-function. In the event that we (for some reason) broadcast a 31 | // revoked commitment transaction, then if the other party knows the revocation 32 | // preimage, then they'll be able to derive the corresponding private key to 33 | // this private key by exploiting the homomorphism in the elliptic curve group. 34 | // 35 | // The derivation is performed as follows: 36 | // 37 | // revokeKey := commitKey + revokePoint 38 | // := G*k + G*h 39 | // := G * (k+h) 40 | // 41 | // Therefore, once we divulge the revocation preimage, the remote peer is able 42 | // to compute the proper private key for the revokeKey by computing: 43 | // revokePriv := commitPriv + revokePreimge mod N 44 | // 45 | // Where N is the order of the sub-group. 46 | func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey, 47 | revokePreimage []byte) *btcec.PublicKey { 48 | ``` 49 | - In-body comments should explain the *intention* of the code. 50 | 51 | **WRONG** 52 | ```go 53 | // return err if amt is less than 546 54 | if amt < 546 { 55 | return err 56 | } 57 | ``` 58 | **RIGHT** 59 | ```go 60 | // Treat transactions with amounts less than the amount which is considered dust 61 | // as non-standard. 62 | if amt < 546 { 63 | return err 64 | } 65 | ``` 66 | 67 | ## Code Spacing and formatting 68 | 69 | - Segment code into logical stanzas separated by newlines. 70 | 71 | **WRONG** 72 | ```go 73 | witness := make([][]byte, 4) 74 | witness[0] = nil 75 | if bytes.Compare(pubA, pubB) == -1 { 76 | witness[1] = sigB 77 | witness[2] = sigA 78 | } else { 79 | witness[1] = sigA 80 | witness[2] = sigB 81 | } 82 | witness[3] = witnessScript 83 | return witness 84 | ``` 85 | **RIGHT** 86 | ```go 87 | witness := make([][]byte, 4) 88 | 89 | // When spending a p2wsh multi-sig script, rather than an OP_0, we add 90 | // a nil stack element to eat the extra pop. 91 | witness[0] = nil 92 | 93 | // When initially generating the witnessScript, we sorted the serialized 94 | // public keys in descending order. So we do a quick comparison in order 95 | // to ensure the signatures appear on the Script Virtual Machine stack in 96 | // the correct order. 97 | if bytes.Compare(pubA, pubB) == -1 { 98 | witness[1] = sigB 99 | witness[2] = sigA 100 | } else { 101 | witness[1] = sigA 102 | witness[2] = sigB 103 | } 104 | 105 | // Finally, add the preimage as the last witness element. 106 | witness[3] = witnessScript 107 | 108 | return witness 109 | ``` 110 | 111 | - Use spacing between `case` and `select` stanzas. 112 | 113 | **WRONG** 114 | ```go 115 | switch { 116 | case a: 117 | 118 | case b: 119 | 120 | case c: 121 | 122 | case d: 123 | 124 | default: 125 | 126 | } 127 | ``` 128 | **RIGHT** 129 | ```go 130 | switch { 131 | // Brief comment detailing instances of this case (repeat below). 132 | case a: 133 | 134 | 135 | case b: 136 | 137 | 138 | case c: 139 | 140 | 141 | case d: 142 | 143 | 144 | default: 145 | 146 | } 147 | ``` 148 | 149 | ## Additional Style Constraints 150 | 151 | ### 80 character line length 152 | 153 | - Wrap columns at 80 characters. 154 | - Tabs are 8 spaces. 155 | 156 | **WRONG** 157 | ```go 158 | myKey := "0214cd678a565041d00e6cf8d62ef8add33b4af4786fb2beb87b366a2e151fcee7" 159 | ``` 160 | 161 | **RIGHT** 162 | ```go 163 | myKey := "0214cd678a565041d00e6cf8d62ef8add33b4af4786fb2beb87b366a2e1" + 164 | "51fcee7" 165 | ``` 166 | 167 | ### Wrapping long function calls 168 | 169 | - If a function call exceeds the column limit, place the closing parenthesis 170 | on its own line and start all arguments on a new line after the opening 171 | parenthesis. 172 | 173 | **WRONG** 174 | ```go 175 | value, err := bar(a, 176 | a, b, c) 177 | ``` 178 | 179 | **RIGHT** 180 | ```go 181 | value, err := bar( 182 | a, a, b, c, 183 | ) 184 | ``` 185 | 186 | - Compact form is acceptable if visual symmetry of parentheses is preserved. 187 | 188 | **ACCEPTABLE** 189 | ```go 190 | response, err := node.AddInvoice( 191 | ctx, &lnrpc.Invoice{ 192 | Memo: "invoice", 193 | ValueMsat: int64(oneUnitMilliSat - 1), 194 | }, 195 | ) 196 | ``` 197 | 198 | **PREFERRED** 199 | ```go 200 | response, err := node.AddInvoice(ctx, &lnrpc.Invoice{ 201 | Memo: "invoice", 202 | ValueMsat: int64(oneUnitMilliSat - 1), 203 | }) 204 | ``` 205 | 206 | ### Exception for log and error message formatting 207 | 208 | - Minimize lines for log and error messages, while adhering to the 209 | 80-character limit. 210 | 211 | **WRONG** 212 | ```go 213 | return fmt.Errorf( 214 | "this is a long error message with a couple (%d) place holders", 215 | len(things), 216 | ) 217 | 218 | log.Debugf( 219 | "Something happened here that we need to log: %v", 220 | longVariableNameHere, 221 | ) 222 | ``` 223 | 224 | **RIGHT** 225 | ```go 226 | return fmt.Errorf("this is a long error message with a couple (%d) place "+ 227 | "holders", len(things)) 228 | 229 | log.Debugf("Something happened here that we need to log: %v", 230 | longVariableNameHere) 231 | ``` 232 | 233 | ### Exceptions and additional styling for structured logging 234 | 235 | - **Static messages:** Use key-value pairs instead of formatted strings for the 236 | `msg` parameter. 237 | - **Key-value attributes:** Use `slog.Attr` helper functions. 238 | - **Line wrapping:** Structured log lines are an exception to the 80-character 239 | rule. Use one line per key-value pair for multiple attributes. 240 | 241 | **WRONG** 242 | ```go 243 | log.DebugS(ctx, fmt.Sprintf("User %d just spent %.8f to open a channel", userID, 0.0154)) 244 | ``` 245 | 246 | **RIGHT** 247 | ```go 248 | log.InfoS(ctx, "Channel open performed", 249 | slog.Int("user_id", userID), 250 | btclog.Fmt("amount", "%.8f", 0.00154)) 251 | ``` 252 | 253 | ### Wrapping long function definitions 254 | 255 | - If function arguments exceed the 80-character limit, maintain indentation 256 | on following lines. 257 | - Do not end a line with an open parenthesis if the function definition is not 258 | finished. 259 | 260 | **WRONG** 261 | ```go 262 | func foo(a, b, c, 263 | ) (d, error) { 264 | 265 | func bar(a, b, c) ( 266 | d, error, 267 | ) { 268 | 269 | func baz(a, b, c) ( 270 | d, error) { 271 | ``` 272 | **RIGHT** 273 | ```go 274 | func foo(a, b, 275 | c) (d, error) { 276 | 277 | func baz(a, b, c) (d, 278 | error) { 279 | 280 | func longFunctionName( 281 | a, b, c) (d, error) { 282 | ``` 283 | 284 | - If a function declaration spans multiple lines, the body should start with an 285 | empty line. 286 | 287 | **WRONG** 288 | ```go 289 | func foo(a, b, c, 290 | d, e) error { 291 | var a int 292 | } 293 | ``` 294 | **RIGHT** 295 | ```go 296 | func foo(a, b, c, 297 | d, e) error { 298 | 299 | var a int 300 | } 301 | ``` 302 | 303 | ## Use of Log Levels 304 | 305 | - Available levels: `trace`, `debug`, `info`, `warn`, `error`, `critical`. 306 | - Only use `error` for internal errors not triggered by external sources. 307 | 308 | ## Testing 309 | 310 | - To run all tests for a specific package: 311 | `make unit pkg=$pkg` 312 | - To run a specific test case within a package: 313 | `make unit pkg=$pkg case=$case` 314 | 315 | ## Git Commit Messages 316 | 317 | - **Subject Line:** 318 | - Format: `subsystem: short description of changes` 319 | - `subsystem` should be the package primarily affected (e.g., `wallet`, `chain`). 320 | - For multiple packages, use `+` or `,` as a delimiter (e.g., `wallet+chain`). 321 | - For widespread changes, use `multi:`. 322 | - Keep it under 50 characters. 323 | - Use the present tense (e.g., "Fix bug", not "Fixed bug"). 324 | 325 | - **Message Body:** 326 | - Separate from the subject with a blank line. 327 | - Explain the "what" and "why" of the change. 328 | - Wrap text to 72 characters. 329 | - Use bullet points for lists. 330 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/btcsuite/btcd/btcec/v2" 12 | sphinx "github.com/lightningnetwork/lightning-onion" 13 | "github.com/urfave/cli" 14 | ) 15 | 16 | const ( 17 | defaultHopDataPath = "cmd/example-data/hop-data.json" 18 | defaultOnionPath = "cmd/example-data/onion.json" 19 | ) 20 | 21 | // main implements a simple command line utility that can be used in order to 22 | // either generate a fresh mix-header or decode and fully process an existing 23 | // one given a private key. 24 | func main() { 25 | app := cli.NewApp() 26 | app.Name = "sphinx-cli" 27 | app.Commands = []cli.Command{ 28 | { 29 | Name: "genkeys", 30 | Usage: "A helper function to generate a random new " + 31 | "private-public key pair.", 32 | Action: genKeys, 33 | Flags: []cli.Flag{ 34 | cli.StringFlag{ 35 | Name: "priv", 36 | Usage: "An optional flag to provide " + 37 | "a private key. In this " + 38 | "case, this command just " + 39 | "calculates and prints the " + 40 | "associated public key", 41 | }, 42 | }, 43 | }, 44 | { 45 | Name: "nextephemeral", 46 | Usage: "A helper to compute the next ephemeral key " + 47 | "given the current ephemeral key and a " + 48 | "private key", 49 | Action: nextEphemeral, 50 | Flags: []cli.Flag{ 51 | cli.StringFlag{ 52 | Name: "priv", 53 | Required: true, 54 | }, 55 | cli.StringFlag{ 56 | Name: "pub", 57 | Required: true, 58 | }, 59 | }, 60 | }, 61 | { 62 | Name: "generate", 63 | Usage: "Build a new onion.", 64 | Action: generate, 65 | Flags: []cli.Flag{ 66 | cli.StringFlag{ 67 | Name: "file", 68 | Usage: "Path to json file containing " + 69 | "the session key and hops " + 70 | "data.", 71 | Value: defaultHopDataPath, 72 | }, 73 | cli.IntFlag{ 74 | Name: "payload-size", 75 | Usage: "The size for a payload for a " + 76 | "single hop. Defaults to the " + 77 | "max routing payload size", 78 | Value: sphinx.MaxRoutingPayloadSize, 79 | }, 80 | }, 81 | }, 82 | { 83 | Name: "peel", 84 | Usage: "Peel the onion.", 85 | Action: peel, 86 | Flags: []cli.Flag{ 87 | cli.StringFlag{ 88 | Name: "file", 89 | Usage: "Path to json file containing " + 90 | "the onion to decode along " + 91 | "with the session key and " + 92 | "associated data.", 93 | Value: defaultOnionPath, 94 | }, 95 | }, 96 | }, 97 | } 98 | 99 | if err := app.Run(os.Args); err != nil { 100 | log.Fatalln(err) 101 | } 102 | } 103 | 104 | func genKeys(cli *cli.Context) error { 105 | var ( 106 | priv *btcec.PrivateKey 107 | pub *btcec.PublicKey 108 | err error 109 | ) 110 | if privKeyStr := cli.String("priv"); privKeyStr != "" { 111 | privBytes, err := hex.DecodeString(privKeyStr) 112 | if err != nil { 113 | return err 114 | } 115 | priv, pub = btcec.PrivKeyFromBytes(privBytes) 116 | 117 | } else { 118 | priv, err = btcec.NewPrivateKey() 119 | if err != nil { 120 | return err 121 | } 122 | 123 | pub = priv.PubKey() 124 | } 125 | 126 | fmt.Printf("Private Key: %x\nPublic Key: %x\n", priv.Serialize(), 127 | pub.SerializeCompressed()) 128 | 129 | return nil 130 | } 131 | 132 | type pathData struct { 133 | SessionKey string `json:"session_key"` 134 | AssociatedData string `json:"associated_data"` 135 | Hops []hopData `json:"hops"` 136 | } 137 | 138 | type hopData struct { 139 | PublicKey string `json:"pubkey"` 140 | Payload string `json:"payload"` 141 | } 142 | 143 | func parsePathData(data pathData) (*sphinx.PaymentPath, *btcec.PrivateKey, 144 | []byte, error) { 145 | 146 | sessionKeyBytes, err := hex.DecodeString(data.SessionKey) 147 | if err != nil { 148 | return nil, nil, nil, fmt.Errorf("unable to decode the "+ 149 | "sessionKey %v: %v", data.SessionKey, err) 150 | } 151 | 152 | if len(sessionKeyBytes) != 32 { 153 | return nil, nil, nil, fmt.Errorf("session priv key must be " + 154 | "32 bytes long") 155 | } 156 | 157 | sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) 158 | 159 | assocData, err := hex.DecodeString(data.AssociatedData) 160 | if err != nil { 161 | return nil, nil, nil, fmt.Errorf("unable to decode the "+ 162 | "associate data %v: %v", data.AssociatedData, err) 163 | } 164 | 165 | var path sphinx.PaymentPath 166 | for i, hop := range data.Hops { 167 | binKey, err := hex.DecodeString(hop.PublicKey) 168 | if err != nil { 169 | return nil, nil, nil, err 170 | } 171 | 172 | pubkey, err := btcec.ParsePubKey(binKey) 173 | if err != nil { 174 | return nil, nil, nil, err 175 | } 176 | 177 | path[i].NodePub = *pubkey 178 | 179 | payload, err := hex.DecodeString(hop.Payload) 180 | if err != nil { 181 | return nil, nil, nil, err 182 | } 183 | 184 | hopPayload, err := sphinx.NewTLVHopPayload(payload) 185 | if err != nil { 186 | return nil, nil, nil, err 187 | } 188 | 189 | path[i].HopPayload = hopPayload 190 | } 191 | 192 | return &path, sessionKey, assocData, nil 193 | } 194 | 195 | func generate(ctx *cli.Context) error { 196 | file := ctx.String("file") 197 | jsonSpec, err := os.ReadFile(file) 198 | if err != nil { 199 | return fmt.Errorf("unable to read JSON onion spec from file "+ 200 | "%v: %v", file, err) 201 | } 202 | 203 | var spec pathData 204 | if err := json.Unmarshal(jsonSpec, &spec); err != nil { 205 | return fmt.Errorf("unable to peel JSON onion spec: %v", err) 206 | } 207 | 208 | path, sessionKey, assocData, err := parsePathData(spec) 209 | if err != nil { 210 | return fmt.Errorf("could not peel onion spec: %v", err) 211 | } 212 | 213 | payloadSize := ctx.Int("payload-size") 214 | msg, err := sphinx.NewOnionPacket( 215 | path, sessionKey, assocData, sphinx.DeterministicPacketFiller, 216 | sphinx.WithMaxPayloadSize(payloadSize), 217 | ) 218 | if err != nil { 219 | return fmt.Errorf("error creating message: %v", err) 220 | } 221 | 222 | w := bytes.NewBuffer([]byte{}) 223 | err = msg.Encode(w) 224 | if err != nil { 225 | return fmt.Errorf("error serializing message: %v", err) 226 | } 227 | 228 | fmt.Printf("%x\n", w.Bytes()) 229 | return nil 230 | } 231 | 232 | type onionInfo struct { 233 | SessionKey string `json:"session_key"` 234 | AssociatedData string `json:"associated_data"` 235 | BlindingPoint string `json:"blinding_point"` 236 | Onion string `json:"onion"` 237 | } 238 | 239 | func parseOnionInfo(info *onionInfo) (*sphinx.OnionPacket, *btcec.PrivateKey, 240 | []byte, *btcec.PublicKey, error) { 241 | 242 | sessionKeyBytes, err := hex.DecodeString(info.SessionKey) 243 | if err != nil { 244 | return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ 245 | "sessionKey %v: %v", info.SessionKey, err) 246 | } 247 | 248 | if len(sessionKeyBytes) != 32 { 249 | return nil, nil, nil, nil, fmt.Errorf("session priv key must " + 250 | "be 32 bytes long") 251 | } 252 | 253 | sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) 254 | 255 | assocData, err := hex.DecodeString(info.AssociatedData) 256 | if err != nil { 257 | return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ 258 | "associate data %v: %v", info.AssociatedData, err) 259 | } 260 | 261 | onion, err := hex.DecodeString(info.Onion) 262 | if err != nil { 263 | return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ 264 | "onion %v: %v", info.Onion, err) 265 | } 266 | 267 | var packet sphinx.OnionPacket 268 | err = packet.Decode(bytes.NewBuffer(onion)) 269 | if err != nil { 270 | return nil, nil, nil, nil, err 271 | } 272 | 273 | var blindingPoint *btcec.PublicKey 274 | if info.BlindingPoint != "" { 275 | bpBytes, err := hex.DecodeString(info.BlindingPoint) 276 | if err != nil { 277 | return nil, nil, nil, nil, err 278 | } 279 | 280 | blindingPoint, err = btcec.ParsePubKey(bpBytes) 281 | if err != nil { 282 | return nil, nil, nil, nil, err 283 | } 284 | } 285 | 286 | return &packet, sessionKey, assocData, blindingPoint, nil 287 | } 288 | 289 | func peel(ctx *cli.Context) error { 290 | file := ctx.String("file") 291 | jsonSpec, err := os.ReadFile(file) 292 | if err != nil { 293 | return fmt.Errorf("unable to read JSON onion spec from file "+ 294 | "%v: %v", file, err) 295 | } 296 | 297 | var info onionInfo 298 | if err := json.Unmarshal(jsonSpec, &info); err != nil { 299 | return err 300 | } 301 | 302 | packet, sessionKey, assocData, blindingPoint, err := parseOnionInfo( 303 | &info, 304 | ) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | s := sphinx.NewRouter( 310 | &sphinx.PrivKeyECDH{PrivKey: sessionKey}, 311 | sphinx.NewMemoryReplayLog(), 312 | ) 313 | s.Start() 314 | defer s.Stop() 315 | 316 | p, err := s.ProcessOnionPacket( 317 | packet, assocData, 10, sphinx.WithBlindingPoint(blindingPoint), 318 | ) 319 | if err != nil { 320 | return err 321 | } 322 | 323 | w := bytes.NewBuffer([]byte{}) 324 | if err = p.NextPacket.Encode(w); err != nil { 325 | return fmt.Errorf("error serializing message: %v", err) 326 | } 327 | 328 | fmt.Printf("%x\n", w.Bytes()) 329 | 330 | return nil 331 | } 332 | 333 | func nextEphemeral(ctx *cli.Context) error { 334 | privKeyByte, err := hex.DecodeString(ctx.String("priv")) 335 | if err != nil { 336 | return err 337 | } 338 | if len(privKeyByte) != 32 { 339 | return fmt.Errorf("private key must be 32 bytes") 340 | } 341 | 342 | privKey, _ := btcec.PrivKeyFromBytes(privKeyByte) 343 | 344 | pubKeyBytes, err := hex.DecodeString(ctx.String("pub")) 345 | if err != nil { 346 | return err 347 | } 348 | 349 | pubKey, err := btcec.ParsePubKey(pubKeyBytes) 350 | if err != nil { 351 | return err 352 | } 353 | 354 | nextBlindedKey, err := sphinx.NextEphemeral( 355 | &sphinx.PrivKeyECDH{PrivKey: privKey}, pubKey, 356 | ) 357 | if err != nil { 358 | return err 359 | } 360 | 361 | fmt.Printf("%x\n", nextBlindedKey.SerializeCompressed()) 362 | 363 | return nil 364 | } 365 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | secp "github.com/decred/dcrd/dcrec/secp256k1/v4" 9 | ) 10 | 11 | const ( 12 | // NumMaxHops is the maximum path length. There is a maximum of 1300 13 | // bytes in the routing info block. Legacy hop payloads are always 65 14 | // bytes, while tlv payloads are at least 47 bytes (tlvlen 1, amt 2, 15 | // timelock 2, nextchan 10, hmac 32) for the intermediate hops and 37 16 | // bytes (tlvlen 1, amt 2, timelock 2, hmac 32) for the exit hop. The 17 | // maximum path length can therefore only be reached by using tlv 18 | // payloads only. With that, the maximum number of intermediate hops 19 | // is: Floor((1300 - 37) / 47) = 26. Including the exit hop, the 20 | // maximum path length is 27 hops. 21 | NumMaxHops = 27 22 | 23 | routeBlindingHMACKey = "blinded_node_id" 24 | ) 25 | 26 | // PaymentPath represents a series of hops within the Lightning Network 27 | // starting at a sender and terminating at a receiver. Each hop contains a set 28 | // of mandatory data which contains forwarding instructions for that hop. 29 | // Additionally, we can also transmit additional data to each hop by utilizing 30 | // the un-used hops (see TrueRouteLength()) to pack in additional data. In 31 | // order to do this, we encrypt the several hops with the same node public key, 32 | // and unroll the extra data into the space used for route forwarding 33 | // information. 34 | type PaymentPath [NumMaxHops]OnionHop 35 | 36 | // OnionHop represents an abstract hop (a link between two nodes) within the 37 | // Lightning Network. A hop is composed of the incoming node (able to decrypt 38 | // the encrypted routing information), and the routing information itself. 39 | // Optionally, the crafter of a route can indicate that additional data aside 40 | // from the routing information is be delivered, which will manifest as 41 | // additional hops to pack the data. 42 | type OnionHop struct { 43 | // NodePub is the target node for this hop. The payload will enter this 44 | // hop, it'll decrypt the routing information, and hand off the 45 | // internal packet to the next hop. 46 | NodePub btcec.PublicKey 47 | 48 | // HopPayload is the opaque payload provided to this node. If the 49 | // HopData above is specified, then it'll be packed into this payload. 50 | HopPayload HopPayload 51 | } 52 | 53 | // IsEmpty returns true if the hop isn't populated. 54 | func (o OnionHop) IsEmpty() bool { 55 | return o.NodePub.X().BitLen() == 0 || o.NodePub.Y().BitLen() == 0 56 | } 57 | 58 | // NodeKeys returns a slice pointing to node keys that this route comprises of. 59 | // The size of the returned slice will be TrueRouteLength(). 60 | func (p *PaymentPath) NodeKeys() []*btcec.PublicKey { 61 | var nodeKeys [NumMaxHops]*btcec.PublicKey 62 | 63 | routeLen := p.TrueRouteLength() 64 | for i := 0; i < routeLen; i++ { 65 | nodeKeys[i] = &p[i].NodePub 66 | } 67 | 68 | return nodeKeys[:routeLen] 69 | } 70 | 71 | // TrueRouteLength returns the "true" length of the PaymentPath. The max 72 | // payment path is NumMaxHops size, but in practice routes are much smaller. 73 | // This method will return the number of actual hops (nodes) involved in this 74 | // route. For references, a direct path has a length of 1, path through an 75 | // intermediate node has a length of 2 (3 nodes involved). 76 | func (p *PaymentPath) TrueRouteLength() int { 77 | var routeLength int 78 | for _, hop := range p { 79 | // When we hit the first empty hop, we know we're now in the 80 | // zero'd out portion of the array. 81 | if hop.IsEmpty() { 82 | return routeLength 83 | } 84 | 85 | routeLength++ 86 | } 87 | 88 | return routeLength 89 | } 90 | 91 | // TotalPayloadSize returns the sum of the size of each payload in the "true" 92 | // route. 93 | func (p *PaymentPath) TotalPayloadSize() int { 94 | var totalSize int 95 | for _, hop := range p { 96 | if hop.IsEmpty() { 97 | continue 98 | } 99 | 100 | totalSize += hop.HopPayload.NumBytes() 101 | } 102 | 103 | return totalSize 104 | } 105 | 106 | // BlindedPath represents all the data that the creator of a blinded path must 107 | // transmit to the builder of route that will send to this path. 108 | type BlindedPath struct { 109 | // IntroductionPoint is the real node ID of the first hop in the blinded 110 | // path. The sender should be able to find this node in the network 111 | // graph and route to it. 112 | IntroductionPoint *btcec.PublicKey 113 | 114 | // BlindingPoint is the first ephemeral blinding point. This is the 115 | // point that the introduction node will use in order to create a shared 116 | // secret with the builder of the blinded route. This point will need 117 | // to be communicated to the introduction node by the sender in some 118 | // way. 119 | BlindingPoint *btcec.PublicKey 120 | 121 | // BlindedHops is a list of ordered BlindedHopInfo. Each entry 122 | // represents a hop in the blinded path along with the encrypted data to 123 | // be sent to that node. Note that the first entry in the list 124 | // represents the introduction point of the path and so the node ID of 125 | // this point does not strictly need to be transmitted to the sender 126 | // since they will be able to derive the point using the BlindingPoint. 127 | BlindedHops []*BlindedHopInfo 128 | } 129 | 130 | // BlindedHopInfo represents a blinded node pub key along with the encrypted 131 | // data for a node in a blinded route. 132 | type BlindedHopInfo struct { 133 | // BlindedNodePub is the blinded public key of the node in the blinded 134 | // route. 135 | BlindedNodePub *btcec.PublicKey 136 | 137 | // CipherText is the encrypted payload to be transported to the hop in 138 | // the blinded route. 139 | CipherText []byte 140 | } 141 | 142 | // HopInfo represents a real node pub key along with the plaintext data for a 143 | // node in a blinded route. 144 | type HopInfo struct { 145 | // NodePub is the real public key of the node in the blinded route. 146 | NodePub *btcec.PublicKey 147 | 148 | // PlainText is the un-encrypted payload to be transported to the hop 149 | // the blinded route. 150 | PlainText []byte 151 | } 152 | 153 | // Encrypt uses the given sharedSecret to blind the public key of the node and 154 | // encrypt the payload and returns the resulting BlindedHopInfo. 155 | func (i *HopInfo) Encrypt(sharedSecret Hash256) (*BlindedHopInfo, error) { 156 | blindedData, err := encryptBlindedHopData(sharedSecret, i.PlainText) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return &BlindedHopInfo{ 162 | BlindedNodePub: blindNodeID(sharedSecret, i.NodePub), 163 | CipherText: blindedData, 164 | }, nil 165 | } 166 | 167 | // BlindedPathInfo holds a BlindedPath and any other items that a path 168 | // constructor may find useful. 169 | type BlindedPathInfo struct { 170 | // Path holds the constructed BlindedPath which holds all info about a 171 | // blinded path that must be communicated to a potential path user. 172 | Path *BlindedPath 173 | 174 | // SessionKey holds the private key that was used as the session key 175 | // of the path. This is the private key for the first ephemeral blinding 176 | // key of the path. 177 | SessionKey *btcec.PrivateKey 178 | 179 | // LastEphemeralKey is the very last ephemeral blinding key used on the 180 | // path. This may be useful to the path creator as they can use this 181 | // key to uniquely identify the path that was used for an incoming 182 | // payment. 183 | LastEphemeralKey *btcec.PublicKey 184 | } 185 | 186 | // BuildBlindedPath creates a new BlindedPath from a session key along with a 187 | // list of HopInfo representing the nodes in the blinded path. The first hop in 188 | // paymentPath is expected to be the introduction node. 189 | func BuildBlindedPath(sessionKey *btcec.PrivateKey, 190 | paymentPath []*HopInfo) (*BlindedPathInfo, error) { 191 | 192 | if len(paymentPath) < 1 { 193 | return nil, errors.New("at least 1 hop is required to create " + 194 | "a blinded path") 195 | } 196 | 197 | bp := &BlindedPath{ 198 | IntroductionPoint: paymentPath[0].NodePub, 199 | BlindingPoint: sessionKey.PubKey(), 200 | BlindedHops: make([]*BlindedHopInfo, len(paymentPath)), 201 | } 202 | 203 | keys := make([]*btcec.PublicKey, len(paymentPath)) 204 | for i, p := range paymentPath { 205 | keys[i] = p.NodePub 206 | } 207 | 208 | hopSharedSecrets, lastEphem, err := generateSharedSecrets( 209 | keys, sessionKey, 210 | ) 211 | if err != nil { 212 | return nil, fmt.Errorf("error generating shared secret: %v", 213 | err) 214 | } 215 | 216 | for i, hop := range paymentPath { 217 | blindedInfo, err := hop.Encrypt(hopSharedSecrets[i]) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | bp.BlindedHops[i] = blindedInfo 223 | } 224 | 225 | return &BlindedPathInfo{ 226 | Path: bp, 227 | SessionKey: sessionKey, 228 | LastEphemeralKey: lastEphem, 229 | }, nil 230 | } 231 | 232 | // blindNodeID blinds the given public key using the provided shared secret. 233 | func blindNodeID(sharedSecret Hash256, 234 | pubKey *btcec.PublicKey) *btcec.PublicKey { 235 | 236 | blindingFactorBytes := generateKey(routeBlindingHMACKey, &sharedSecret) 237 | 238 | var blindingFactor btcec.ModNScalar 239 | blindingFactor.SetBytes(&blindingFactorBytes) 240 | 241 | return blindGroupElement(pubKey, blindingFactor) 242 | } 243 | 244 | // encryptBlindedHopData blinds/encrypts the given plain text data using the 245 | // provided shared secret. 246 | func encryptBlindedHopData(sharedSecret Hash256, plainTxt []byte) ([]byte, 247 | error) { 248 | 249 | rhoKey := generateKey("rho", &sharedSecret) 250 | 251 | return chacha20polyEncrypt(rhoKey[:], plainTxt) 252 | } 253 | 254 | // decryptBlindedHopData decrypts the data encrypted by the creator of the 255 | // blinded route. 256 | func decryptBlindedHopData(privKey SingleKeyECDH, ephemPub *btcec.PublicKey, 257 | encryptedData []byte) ([]byte, error) { 258 | 259 | ss, err := privKey.ECDH(ephemPub) 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | ssHash := Hash256(ss) 265 | rho := generateKey("rho", &ssHash) 266 | 267 | return chacha20polyDecrypt(rho[:], encryptedData) 268 | } 269 | 270 | // NextEphemeral computes the next ephemeral key given the current ephemeral 271 | // key and this node's private key. 272 | func NextEphemeral(privKey SingleKeyECDH, 273 | ephemPub *btcec.PublicKey) (*btcec.PublicKey, error) { 274 | 275 | ss, err := privKey.ECDH(ephemPub) 276 | if err != nil { 277 | return nil, err 278 | } 279 | 280 | blindingFactor := computeBlindingFactor(ephemPub, ss[:]) 281 | nextEphem := blindGroupElement(ephemPub, blindingFactor) 282 | 283 | return nextEphem, nil 284 | } 285 | 286 | // NextEphemeralPriv computes the next ephemeral priv key given the current 287 | // ephemeral private key and a node's public key. 288 | func NextEphemeralPriv(ephemPriv *PrivKeyECDH, 289 | pubKey *btcec.PublicKey) (*btcec.PrivateKey, error) { 290 | 291 | // ss = e1 * P 292 | ss, err := ephemPriv.ECDH(pubKey) 293 | if err != nil { 294 | return nil, err 295 | } 296 | 297 | // bf = H( E1 || ss ) 298 | blindingFactor := computeBlindingFactor(ephemPriv.PubKey(), ss[:]) 299 | 300 | // e2 = e1 * bf 301 | var nextPrivEphem btcec.ModNScalar 302 | nextPrivEphem.Set(&ephemPriv.PrivKey.Key) 303 | nextPrivEphem.Mul(&blindingFactor) 304 | 305 | return secp.NewPrivateKey(&nextPrivEphem), nil 306 | } 307 | -------------------------------------------------------------------------------- /testdata/route-blinding-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "test vector for using blinded routes", 3 | "generate": { 4 | "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.", 5 | "hops": [ 6 | { 7 | "comment": "Bob creates a Bob -> Carol route with the following session_key and concatenates it with the Dave -> Eve route.", 8 | "session_key": "0202020202020202020202020202020202020202020202020202020202020202", 9 | "alias": "Bob", 10 | "node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 11 | "tlvs": { 12 | "padding": "0000000000000000000000000000000000000000000000000000", 13 | "short_channel_id": "0x0x1729", 14 | "payment_relay": { 15 | "cltv_expiry_delta": 36, 16 | "fee_proportional_millionths": 150, 17 | "fee_base_msat": 10000 18 | }, 19 | "payment_constraints": { 20 | "max_cltv_expiry": 748005, 21 | "htlc_minimum_msat": 1500 22 | }, 23 | "allowed_features": { 24 | "features": [] 25 | }, 26 | "unknown_tag_561": "123456" 27 | }, 28 | "encoded_tlvs": "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", 29 | "ephemeral_privkey": "0202020202020202020202020202020202020202020202020202020202020202", 30 | "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", 31 | "shared_secret": "76771bab0cc3d0de6e6f60147fd7c9c7249a5ced3d0612bdfaeec3b15452229d", 32 | "rho": "ba217b23c0978d84c4a19be8a9ff64bc1b40ed0d7ecf59521567a5b3a9a1dd48", 33 | "encrypted_data": "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb", 34 | "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25" 35 | }, 36 | { 37 | "comment": "Notice the next_blinding_override tlv in Carol's payload, indicating that Bob concatenated his route with another blinded route starting at Dave.", 38 | "alias": "Carol", 39 | "node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", 40 | "tlvs": { 41 | "short_channel_id": "0x0x1105", 42 | "next_blinding_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 43 | "payment_relay": { 44 | "cltv_expiry_delta": 48, 45 | "fee_proportional_millionths": 100, 46 | "fee_base_msat": 500 47 | }, 48 | "payment_constraints": { 49 | "max_cltv_expiry": 747969, 50 | "htlc_minimum_msat": 1500 51 | }, 52 | "allowed_features": { 53 | "features": [] 54 | } 55 | }, 56 | "encoded_tlvs": "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", 57 | "ephemeral_privkey": "0a2aa791ac81265c139237b2b84564f6000b1d4d0e68d4b9cc97c5536c9b61c1", 58 | "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", 59 | "shared_secret": "dc91516ec6b530a3d641c01f29b36ed4dc29a74e063258278c0eeed50313d9b8", 60 | "rho": "d1e62bae1a8e169da08e6204997b60b1a7971e0f246814c648125c35660f5416", 61 | "encrypted_data": "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e", 62 | "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7" 63 | }, 64 | { 65 | "comment": "Eve creates a Dave -> Eve blinded route using the following session_key.", 66 | "session_key": "0101010101010101010101010101010101010101010101010101010101010101", 67 | "alias": "Dave", 68 | "node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", 69 | "tlvs": { 70 | "padding": "0000000000000000000000000000000000000000000000000000000000000000000000", 71 | "short_channel_id": "0x0x561", 72 | "payment_relay": { 73 | "cltv_expiry_delta": 144, 74 | "fee_proportional_millionths": 250 75 | }, 76 | "payment_constraints": { 77 | "max_cltv_expiry": 747921, 78 | "htlc_minimum_msat": 1500 79 | }, 80 | "allowed_features": { 81 | "features": [] 82 | } 83 | }, 84 | "encoded_tlvs": "01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00", 85 | "ephemeral_privkey": "0101010101010101010101010101010101010101010101010101010101010101", 86 | "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 87 | "shared_secret": "dc46f3d1d99a536300f17bc0512376cc24b9502c5d30144674bfaa4b923d9057", 88 | "rho": "393aa55d35c9e207a8f28180b81628a31dff558c84959cdc73130f8c321d6a06", 89 | "encrypted_data": "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105", 90 | "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf" 91 | }, 92 | { 93 | "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.", 94 | "alias": "Eve", 95 | "node_id": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", 96 | "tlvs": { 97 | "padding": "0000000000000000000000000000000000000000000000000000", 98 | "path_id": "deadbeef", 99 | "payment_constraints": { 100 | "max_cltv_expiry": 747777, 101 | "htlc_minimum_msat": 1500 102 | }, 103 | "allowed_features": { 104 | "features": [113] 105 | }, 106 | "unknown_tag_65535": "06c1" 107 | }, 108 | "encoded_tlvs": "011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1", 109 | "ephemeral_privkey": "62e8bcd6b5f7affe29bec4f0515aab2eebd1ce848f4746a9638aa14e3024fb1b", 110 | "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", 111 | "shared_secret": "352a706b194c2b6d0a04ba1f617383fb816dc5f8f9ac0b60dd19c9ae3b517289", 112 | "rho": "719d0307340b1c68b79865111f0de6e97b093a30bc603cebd1beb9eef116f2d8", 113 | "encrypted_data": "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8", 114 | "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae" 115 | } 116 | ] 117 | }, 118 | "route": { 119 | "comment": "This section contains the resulting blinded route, which can then be used inside onion messages or payments.", 120 | "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 121 | "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", 122 | "hops": [ 123 | { 124 | "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", 125 | "encrypted_data": "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb" 126 | }, 127 | { 128 | "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", 129 | "encrypted_data": "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e" 130 | }, 131 | { 132 | "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", 133 | "encrypted_data": "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105" 134 | }, 135 | { 136 | "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", 137 | "encrypted_data": "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8" 138 | } 139 | ] 140 | }, 141 | "unblind": { 142 | "comment": "This section contains test data for unblinding the route at each intermediate hop.", 143 | "hops": [ 144 | { 145 | "alias": "Bob", 146 | "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", 147 | "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", 148 | "blinded_privkey": "d12fec0332c3e9d224789a17ebd93595f37d37bd8ef8bd3d2e6ce50acb9e554f", 149 | "decrypted_data": "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", 150 | "next_ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" 151 | }, 152 | { 153 | "alias": "Carol", 154 | "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", 155 | "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", 156 | "blinded_privkey": "bfa697fbbc8bbc43ca076e6dd60d306038a32af216b9dc6fc4e59e5ae28823c1", 157 | "decrypted_data": "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", 158 | "next_ephemeral_pubkey": "03af5ccc91851cb294e3a364ce63347709a08cdffa58c672e9a5c587ddd1bbca60", 159 | "next_ephemeral_pubkey_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" 160 | }, 161 | { 162 | "alias": "Dave", 163 | "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", 164 | "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 165 | "blinded_privkey": "cebc115c7fce4c295dc396dea6c79115b289b8ceeceea2ed61cf31428d88fc4e", 166 | "decrypted_data": "01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00", 167 | "next_ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" 168 | }, 169 | { 170 | "alias": "Eve", 171 | "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", 172 | "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", 173 | "blinded_privkey": "ff4e07da8d92838bedd019ce532eb990ed73b574e54a67862a1df81b40c0d2af", 174 | "decrypted_data": "011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1", 175 | "next_ephemeral_pubkey": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" 176 | } 177 | ] 178 | } 179 | } -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module tools 2 | 3 | go 1.24.6 4 | 5 | tool ( 6 | github.com/golangci/golangci-lint/v2/cmd/golangci-lint 7 | github.com/rinchsan/gosimports/cmd/gosimports 8 | ) 9 | 10 | require ( 11 | 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 12 | 4d63.com/gochecknoglobals v0.2.2 // indirect 13 | codeberg.org/chavacava/garif v0.2.0 // indirect 14 | dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect 15 | dev.gaijin.team/go/golib v0.6.0 // indirect 16 | github.com/4meepo/tagalign v1.4.3 // indirect 17 | github.com/Abirdcfly/dupword v0.1.6 // indirect 18 | github.com/AlwxSin/noinlineerr v1.0.5 // indirect 19 | github.com/Antonboom/errname v1.1.0 // indirect 20 | github.com/Antonboom/nilnil v1.1.0 // indirect 21 | github.com/Antonboom/testifylint v1.6.1 // indirect 22 | github.com/BurntSushi/toml v1.5.0 // indirect 23 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 24 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 25 | github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect 26 | github.com/alecthomas/chroma/v2 v2.20.0 // indirect 27 | github.com/alecthomas/go-check-sumtype v0.3.1 // indirect 28 | github.com/alexkohler/nakedret/v2 v2.0.6 // indirect 29 | github.com/alexkohler/prealloc v1.0.0 // indirect 30 | github.com/alfatraining/structtag v1.0.0 // indirect 31 | github.com/alingse/asasalint v0.0.11 // indirect 32 | github.com/alingse/nilnesserr v0.2.0 // indirect 33 | github.com/ashanbrown/forbidigo/v2 v2.1.0 // indirect 34 | github.com/ashanbrown/makezero/v2 v2.0.1 // indirect 35 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 36 | github.com/beorn7/perks v1.0.1 // indirect 37 | github.com/bkielbasa/cyclop v1.2.3 // indirect 38 | github.com/blizzy78/varnamelen v0.8.0 // indirect 39 | github.com/bombsimon/wsl/v4 v4.7.0 // indirect 40 | github.com/bombsimon/wsl/v5 v5.1.1 // indirect 41 | github.com/breml/bidichk v0.3.3 // indirect 42 | github.com/breml/errchkjson v0.4.1 // indirect 43 | github.com/butuzov/ireturn v0.4.0 // indirect 44 | github.com/butuzov/mirror v1.3.0 // indirect 45 | github.com/catenacyber/perfsprint v0.9.1 // indirect 46 | github.com/ccojocar/zxcvbn-go v1.0.4 // indirect 47 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 48 | github.com/charithe/durationcheck v0.0.10 // indirect 49 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 50 | github.com/charmbracelet/lipgloss v1.1.0 // indirect 51 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 52 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 53 | github.com/charmbracelet/x/term v0.2.1 // indirect 54 | github.com/ckaznocha/intrange v0.3.1 // indirect 55 | github.com/curioswitch/go-reassign v0.3.0 // indirect 56 | github.com/daixiang0/gci v0.13.7 // indirect 57 | github.com/dave/dst v0.27.3 // indirect 58 | github.com/davecgh/go-spew v1.1.1 // indirect 59 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 60 | github.com/dlclark/regexp2 v1.11.5 // indirect 61 | github.com/ettle/strcase v0.2.0 // indirect 62 | github.com/fatih/color v1.18.0 // indirect 63 | github.com/fatih/structtag v1.2.0 // indirect 64 | github.com/firefart/nonamedreturns v1.0.6 // indirect 65 | github.com/fsnotify/fsnotify v1.5.4 // indirect 66 | github.com/fzipp/gocyclo v0.6.0 // indirect 67 | github.com/ghostiam/protogetter v0.3.15 // indirect 68 | github.com/go-critic/go-critic v0.13.0 // indirect 69 | github.com/go-toolsmith/astcast v1.1.0 // indirect 70 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 71 | github.com/go-toolsmith/astequal v1.2.0 // indirect 72 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 73 | github.com/go-toolsmith/astp v1.1.0 // indirect 74 | github.com/go-toolsmith/strparse v1.1.0 // indirect 75 | github.com/go-toolsmith/typep v1.1.0 // indirect 76 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 77 | github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect 78 | github.com/gobwas/glob v0.2.3 // indirect 79 | github.com/gofrs/flock v0.12.1 // indirect 80 | github.com/golang/protobuf v1.5.3 // indirect 81 | github.com/golangci/asciicheck v0.5.0 // indirect 82 | github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect 83 | github.com/golangci/go-printf-func-name v0.1.0 // indirect 84 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect 85 | github.com/golangci/golangci-lint/v2 v2.4.1-0.20250818164121-838684c5bc0c // indirect 86 | github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect 87 | github.com/golangci/misspell v0.7.0 // indirect 88 | github.com/golangci/plugin-module-register v0.1.2 // indirect 89 | github.com/golangci/revgrep v0.8.0 // indirect 90 | github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect 91 | github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect 92 | github.com/google/go-cmp v0.7.0 // indirect 93 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 94 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 95 | github.com/gostaticanalysis/comment v1.5.0 // indirect 96 | github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect 97 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 98 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect 99 | github.com/hashicorp/go-version v1.7.0 // indirect 100 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 101 | github.com/hashicorp/hcl v1.0.0 // indirect 102 | github.com/hexops/gotextdiff v1.0.3 // indirect 103 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 104 | github.com/jgautheron/goconst v1.8.2 // indirect 105 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 106 | github.com/jjti/go-spancheck v0.6.5 // indirect 107 | github.com/julz/importas v0.2.0 // indirect 108 | github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect 109 | github.com/kisielk/errcheck v1.9.0 // indirect 110 | github.com/kkHAIKE/contextcheck v1.1.6 // indirect 111 | github.com/kulti/thelper v0.6.3 // indirect 112 | github.com/kunwardeep/paralleltest v1.0.14 // indirect 113 | github.com/lasiar/canonicalheader v1.1.2 // indirect 114 | github.com/ldez/exptostd v0.4.4 // indirect 115 | github.com/ldez/gomoddirectives v0.7.0 // indirect 116 | github.com/ldez/grignotin v0.10.0 // indirect 117 | github.com/ldez/tagliatelle v0.7.1 // indirect 118 | github.com/ldez/usetesting v0.5.0 // indirect 119 | github.com/leonklingele/grouper v1.1.2 // indirect 120 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 121 | github.com/macabu/inamedparam v0.2.0 // indirect 122 | github.com/magiconair/properties v1.8.6 // indirect 123 | github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect 124 | github.com/manuelarte/funcorder v0.5.0 // indirect 125 | github.com/maratori/testableexamples v1.0.0 // indirect 126 | github.com/maratori/testpackage v1.1.1 // indirect 127 | github.com/matoous/godox v1.1.0 // indirect 128 | github.com/mattn/go-colorable v0.1.14 // indirect 129 | github.com/mattn/go-isatty v0.0.20 // indirect 130 | github.com/mattn/go-runewidth v0.0.16 // indirect 131 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 132 | github.com/mgechev/revive v1.11.0 // indirect 133 | github.com/mitchellh/go-homedir v1.1.0 // indirect 134 | github.com/mitchellh/mapstructure v1.5.0 // indirect 135 | github.com/moricho/tparallel v0.3.2 // indirect 136 | github.com/muesli/termenv v0.16.0 // indirect 137 | github.com/nakabonne/nestif v0.3.1 // indirect 138 | github.com/nishanths/exhaustive v0.12.0 // indirect 139 | github.com/nishanths/predeclared v0.2.2 // indirect 140 | github.com/nunnatsa/ginkgolinter v0.20.0 // indirect 141 | github.com/pelletier/go-toml v1.9.5 // indirect 142 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 143 | github.com/pmezard/go-difflib v1.0.0 // indirect 144 | github.com/polyfloyd/go-errorlint v1.8.0 // indirect 145 | github.com/prometheus/client_golang v1.12.1 // indirect 146 | github.com/prometheus/client_model v0.2.0 // indirect 147 | github.com/prometheus/common v0.32.1 // indirect 148 | github.com/prometheus/procfs v0.7.3 // indirect 149 | github.com/quasilyte/go-ruleguard v0.4.4 // indirect 150 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 151 | github.com/quasilyte/gogrep v0.5.0 // indirect 152 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 153 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 154 | github.com/raeperd/recvcheck v0.2.0 // indirect 155 | github.com/rinchsan/gosimports v0.3.8 // indirect 156 | github.com/rivo/uniseg v0.4.7 // indirect 157 | github.com/rogpeppe/go-internal v1.14.1 // indirect 158 | github.com/ryancurrah/gomodguard v1.4.1 // indirect 159 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 160 | github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect 161 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect 162 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 163 | github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect 164 | github.com/securego/gosec/v2 v2.22.8 // indirect 165 | github.com/sirupsen/logrus v1.9.3 // indirect 166 | github.com/sivchari/containedctx v1.0.3 // indirect 167 | github.com/sonatard/noctx v0.4.0 // indirect 168 | github.com/sourcegraph/go-diff v0.7.0 // indirect 169 | github.com/spf13/afero v1.14.0 // indirect 170 | github.com/spf13/cast v1.5.0 // indirect 171 | github.com/spf13/cobra v1.9.1 // indirect 172 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 173 | github.com/spf13/pflag v1.0.7 // indirect 174 | github.com/spf13/viper v1.12.0 // indirect 175 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 176 | github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect 177 | github.com/stretchr/objx v0.5.2 // indirect 178 | github.com/stretchr/testify v1.10.0 // indirect 179 | github.com/subosito/gotenv v1.4.1 // indirect 180 | github.com/tetafro/godot v1.5.1 // indirect 181 | github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect 182 | github.com/timonwong/loggercheck v0.11.0 // indirect 183 | github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect 184 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 185 | github.com/ultraware/funlen v0.2.0 // indirect 186 | github.com/ultraware/whitespace v0.2.0 // indirect 187 | github.com/uudashr/gocognit v1.2.0 // indirect 188 | github.com/uudashr/iface v1.4.1 // indirect 189 | github.com/xen0n/gosmopolitan v1.3.0 // indirect 190 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 191 | github.com/yagipy/maintidx v1.0.0 // indirect 192 | github.com/yeya24/promlinter v0.3.0 // indirect 193 | github.com/ykadowak/zerologlint v0.1.5 // indirect 194 | gitlab.com/bosi/decorder v0.4.2 // indirect 195 | go-simpler.org/musttag v0.14.0 // indirect 196 | go-simpler.org/sloglint v0.11.1 // indirect 197 | go.augendre.info/arangolint v0.2.0 // indirect 198 | go.augendre.info/fatcontext v0.8.1 // indirect 199 | go.uber.org/automaxprocs v1.6.0 // indirect 200 | go.uber.org/multierr v1.10.0 // indirect 201 | go.uber.org/zap v1.27.0 // indirect 202 | golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect 203 | golang.org/x/mod v0.27.0 // indirect 204 | golang.org/x/sync v0.16.0 // indirect 205 | golang.org/x/sys v0.35.0 // indirect 206 | golang.org/x/text v0.28.0 // indirect 207 | golang.org/x/tools v0.36.0 // indirect 208 | google.golang.org/protobuf v1.36.6 // indirect 209 | gopkg.in/ini.v1 v1.67.0 // indirect 210 | gopkg.in/yaml.v2 v2.4.0 // indirect 211 | gopkg.in/yaml.v3 v3.0.1 // indirect 212 | honnef.co/go/tools v0.6.1 // indirect 213 | mvdan.cc/gofumpt v0.8.0 // indirect 214 | mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect 215 | ) 216 | -------------------------------------------------------------------------------- /payload.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "math" 10 | 11 | "github.com/btcsuite/btcd/wire" 12 | ) 13 | 14 | // PayloadType denotes the type of the payload included in the onion packet. 15 | // Serialization of a raw HopPayload will depend on the payload type, as some 16 | // include a varint length prefix, while others just encode the raw payload. 17 | type PayloadType uint8 18 | 19 | const ( 20 | // PayloadLegacy is the legacy payload type. It includes a fixed 32 21 | // bytes, 12 of which are padding, and uses a "zero length" (the old 22 | // realm) prefix. 23 | PayloadLegacy PayloadType = iota 24 | 25 | // PayloadTLV is the new modern TLV based format. This payload includes 26 | // a set of opaque bytes with a varint length prefix. The varint used 27 | // is the same CompactInt as used in the Bitcoin protocol. 28 | PayloadTLV 29 | ) 30 | 31 | // HopPayload is a slice of bytes and associated payload-type that are destined 32 | // for a specific hop in the PaymentPath. The payload itself is treated as an 33 | // opaque data field by the onion router. The included Type field informs the 34 | // serialization/deserialziation of the raw payload. 35 | type HopPayload struct { 36 | // Type is the type of the payload. 37 | Type PayloadType 38 | 39 | // Payload is the raw bytes of the per-hop payload for this hop. 40 | // Depending on the realm, this pay be the regular legacy hop data, or 41 | // a set of opaque blobs to be parsed by higher layers. 42 | Payload []byte 43 | 44 | // HMAC is an HMAC computed over the entire per-hop payload that also 45 | // includes the higher-level (optional) associated data bytes. 46 | HMAC [HMACSize]byte 47 | } 48 | 49 | // NewTLVHopPayload creates a new TLV encoded HopPayload. The payload will be 50 | // a TLV encoded stream that will contain forwarding instructions for a hop. 51 | func NewTLVHopPayload(payload []byte) (HopPayload, error) { 52 | var ( 53 | h HopPayload 54 | b bytes.Buffer 55 | ) 56 | 57 | // Write out the raw payload which contains a set of opaque bytes that 58 | // the recipient can decode to make a forwarding decision. 59 | if _, err := b.Write(payload); err != nil { 60 | return h, nil 61 | } 62 | 63 | h.Type = PayloadTLV 64 | h.Payload = b.Bytes() 65 | 66 | return h, nil 67 | } 68 | 69 | // NumBytes returns the number of bytes it will take to serialize the full 70 | // payload. Depending on the payload type, this may include some additional 71 | // signalling bytes. 72 | func (hp *HopPayload) NumBytes() int { 73 | if hp.Type == PayloadLegacy { 74 | return legacyNumBytes() 75 | } 76 | 77 | return tlvNumBytes(len(hp.Payload)) 78 | } 79 | 80 | // Encode encodes the hop payload into the passed writer. 81 | func (hp *HopPayload) Encode(w io.Writer) error { 82 | if hp.Type == PayloadLegacy { 83 | return encodeLegacyHopPayload(hp, w) 84 | } 85 | 86 | return encodeTLVHopPayload(hp, w) 87 | } 88 | 89 | // DecodeHopPayload unpacks an encoded HopPayload from the passed reader into 90 | // the target HopPayload. tlvGuaranteed should be set to true if the caller only 91 | // wishes to accept TLV encoded payloads. By doing so, zero-lengt tlv payloads 92 | // are supported. If set to false, then the function will inspect the first byte 93 | // to determine the type of payload. 94 | func DecodeHopPayload(r io.Reader, tlvGuaranteed bool) (*HopPayload, error) { 95 | var ( 96 | payloadSize uint16 97 | payloadType = PayloadTLV 98 | hmac [HMACSize]byte 99 | bufReader = bufio.NewReader(r) 100 | ) 101 | 102 | // If we are not sure if this is a TLV or legacy payload, then we need 103 | // to inspect the first byte to determine the type of payload. The first 104 | // byte is either a realm (legacy) or the beginning of a var-int 105 | // encoding the length of the payload (TLV). We'll use a bufio reader to 106 | // peek at it without consuming it from the buffer. 107 | peekByte, err := bufReader.Peek(1) 108 | if err != nil { 109 | return nil, fmt.Errorf("peek first payload byte: %w", err) 110 | } 111 | 112 | if !tlvGuaranteed && isLegacyPayloadByte(peekByte[0]) { 113 | // If we're not guaranteed TLV, and the first byte indicates a 114 | // legacy payload, then we treat this as a legacy payload. 115 | payloadType = PayloadLegacy 116 | payloadSize = legacyPayloadSize() 117 | } else { 118 | // Otherwise, we treat this as a TLV payload. 119 | payloadSize, err = tlvPayloadSize(bufReader) 120 | if err != nil { 121 | return nil, err 122 | } 123 | } 124 | 125 | // Now that we know the payload size, we'll create a new buffer to read 126 | // it out in full. 127 | payload := make([]byte, payloadSize) 128 | 129 | _, err = io.ReadFull(bufReader, payload) 130 | if err != nil { 131 | return nil, fmt.Errorf("%w: %w", ErrIOReadFull, err) 132 | } 133 | 134 | _, err = io.ReadFull(bufReader, hmac[:]) 135 | if err != nil { 136 | return nil, fmt.Errorf("%w: %w", ErrIOReadFull, err) 137 | } 138 | 139 | return &HopPayload{ 140 | Type: payloadType, 141 | Payload: payload, 142 | HMAC: hmac, 143 | }, nil 144 | } 145 | 146 | // HopData attempts to extract a set of forwarding instructions from the target 147 | // HopPayload. If the realm isn't what we expect, then an error is returned. 148 | // This method also returns the left over EOB that remain after the hop data 149 | // has been parsed. Callers may want to map this blob into something more 150 | // concrete. 151 | func (hp *HopPayload) HopData() (*HopData, error) { 152 | // The HopData can only be extracted at this layer for payloads using 153 | // the legacy encoding. 154 | if hp.Type == PayloadLegacy { 155 | return decodeLegacyHopData(hp.Payload) 156 | } 157 | 158 | return nil, nil 159 | } 160 | 161 | // tlvPayloadSize uses the passed reader to extract the payload length encoded 162 | // as a var-int. 163 | func tlvPayloadSize(r io.Reader) (uint16, error) { 164 | var b [8]byte 165 | varInt, err := ReadVarInt(r, &b) 166 | if err != nil { 167 | return 0, err 168 | } 169 | 170 | if varInt > math.MaxUint16 { 171 | return 0, fmt.Errorf("payload size of %d is larger than the "+ 172 | "maximum allowed size of %d", varInt, math.MaxUint16) 173 | } 174 | 175 | return uint16(varInt), nil 176 | } 177 | 178 | // tlvNumBytes takes the length of the payload and returns the number of bytes 179 | // that it would take to serialise such a payload. For the TLV type encoding, 180 | // the payload length itself would be encoded as a var-int, this is then 181 | // followed by the payload itself and finally an HMAC would be appended. 182 | func tlvNumBytes(payloadLen int) int { 183 | return wire.VarIntSerializeSize(uint64(payloadLen)) + payloadLen + 184 | HMACSize 185 | } 186 | 187 | // encodeTLVHopPayload takes a HopPayload and writes it to the given writer 188 | // using the TLV encoding which requires the payload and HMAC to be pre-fixed 189 | // with a var-int encoded length. 190 | func encodeTLVHopPayload(hp *HopPayload, w io.Writer) error { 191 | // First, the length of the payload is encoded as a var-int. 192 | var b [8]byte 193 | err := WriteVarInt(w, uint64(len(hp.Payload)), &b) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | // Then, the raw payload and he HMAC are written in series. 199 | if _, err := w.Write(hp.Payload); err != nil { 200 | return err 201 | } 202 | 203 | _, err = w.Write(hp.HMAC[:]) 204 | 205 | return err 206 | } 207 | 208 | // HopData is the information destined for individual hops. It is a fixed size 209 | // 64 bytes, prefixed with a 1 byte realm that indicates how to interpret it. 210 | // For now we simply assume it's the bitcoin realm (0x00) and hence the format 211 | // is fixed. The last 32 bytes are always the HMAC to be passed to the next 212 | // hop, or zero if this is the packet is not to be forwarded, since this is the 213 | // last hop. 214 | type HopData struct { 215 | // Realm denotes the "real" of target chain of the next hop. For 216 | // bitcoin, this value will be 0x00. 217 | Realm [RealmByteSize]byte 218 | 219 | // NextAddress is the address of the next hop that this packet should 220 | // be forward to. 221 | NextAddress [AddressSize]byte 222 | 223 | // ForwardAmount is the HTLC amount that the next hop should forward. 224 | // This value should take into account the fee require by this 225 | // particular hop, and the cumulative fee for the entire route. 226 | ForwardAmount uint64 227 | 228 | // OutgoingCltv is the value of the outgoing absolute time-lock that 229 | // should be included in the HTLC forwarded. 230 | OutgoingCltv uint32 231 | 232 | // ExtraBytes is the set of unused bytes within the onion payload. This 233 | // extra set of bytes can be utilized by higher level applications to 234 | // package additional data within the per-hop payload, or signal that a 235 | // portion of the remaining set of hops are to be consumed as Extra 236 | // Onion Blobs. 237 | // 238 | // TODO(roasbeef): rename to padding bytes? 239 | ExtraBytes [NumPaddingBytes]byte 240 | } 241 | 242 | // Encode writes the serialized version of the target HopData into the passed 243 | // io.Writer. 244 | func (hd *HopData) Encode(w io.Writer) error { 245 | if _, err := w.Write(hd.Realm[:]); err != nil { 246 | return err 247 | } 248 | 249 | if _, err := w.Write(hd.NextAddress[:]); err != nil { 250 | return err 251 | } 252 | 253 | err := binary.Write(w, binary.BigEndian, hd.ForwardAmount) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | err = binary.Write(w, binary.BigEndian, hd.OutgoingCltv) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | if _, err := w.Write(hd.ExtraBytes[:]); err != nil { 264 | return err 265 | } 266 | 267 | return nil 268 | } 269 | 270 | // Decode Decodes populates the target HopData with the contents of a serialized 271 | // HopData packed into the passed io.Reader. 272 | func (hd *HopData) Decode(r io.Reader) error { 273 | if _, err := io.ReadFull(r, hd.Realm[:]); err != nil { 274 | return err 275 | } 276 | 277 | if _, err := io.ReadFull(r, hd.NextAddress[:]); err != nil { 278 | return err 279 | } 280 | 281 | err := binary.Read(r, binary.BigEndian, &hd.ForwardAmount) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | err = binary.Read(r, binary.BigEndian, &hd.OutgoingCltv) 287 | if err != nil { 288 | return err 289 | } 290 | 291 | _, err = io.ReadFull(r, hd.ExtraBytes[:]) 292 | return err 293 | } 294 | 295 | // NewLegacyHopPayload creates a new hop payload given a set of forwarding 296 | // instructions specified as HopData for a hop. This is the legacy encoding 297 | // for a HopPayload. 298 | func NewLegacyHopPayload(hopData *HopData) (HopPayload, error) { 299 | var ( 300 | h HopPayload 301 | b bytes.Buffer 302 | ) 303 | 304 | if err := hopData.Encode(&b); err != nil { 305 | return h, nil 306 | } 307 | 308 | // We'll also mark that this particular hop will be using the legacy 309 | // format as the modern format packs the existing hop data information 310 | // into the EOB space as a TLV stream. 311 | h.Type = PayloadLegacy 312 | h.Payload = b.Bytes() 313 | 314 | return h, nil 315 | } 316 | 317 | // legacyPayloadSize returns the size of payloads encoded using the legacy 318 | // fixed-size encoding. 319 | func legacyPayloadSize() uint16 { 320 | return LegacyHopDataSize - HMACSize 321 | } 322 | 323 | // legacyNumBytes returns the number of bytes it will take to serialize the full 324 | // payload. For the legacy encoding type, this is always a fixed number. 325 | func legacyNumBytes() int { 326 | return LegacyHopDataSize 327 | } 328 | 329 | // isLegacyPayloadByte determines if the first byte of a hop payload indicates 330 | // that it is a legacy payload. The first byte of a legacy payload will always 331 | // be 0x00, as this is the realm. For TLV payloads, the first byte is a 332 | // var-int encoding the length of the payload. A TLV stream can be empty, in 333 | // which case its length is 0, which is also encoded as a 0x00 byte. This 334 | // creates an ambiguity between a legacy payload and an empty TLV payload. 335 | func isLegacyPayloadByte(b byte) bool { 336 | return b == 0x00 337 | } 338 | 339 | // encodeLegacyHopPayload takes a HopPayload and writes it to the given writer 340 | // using the legacy encoding. 341 | func encodeLegacyHopPayload(hp *HopPayload, w io.Writer) error { 342 | // The raw payload and he HMAC are written in series. 343 | if _, err := w.Write(hp.Payload); err != nil { 344 | return err 345 | } 346 | 347 | _, err := w.Write(hp.HMAC[:]) 348 | 349 | return err 350 | } 351 | 352 | // decodeLegacyHopData takes a payload and decodes it into a HopData struct. 353 | func decodeLegacyHopData(payload []byte) (*HopData, error) { 354 | var ( 355 | payloadReader = bytes.NewBuffer(payload) 356 | hd HopData 357 | ) 358 | if err := hd.Decode(payloadReader); err != nil { 359 | return nil, err 360 | } 361 | 362 | return &hd, nil 363 | } 364 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/aead/chacha20" 11 | "github.com/btcsuite/btcd/btcec/v2" 12 | secp "github.com/decred/dcrd/dcrec/secp256k1/v4" 13 | "golang.org/x/crypto/chacha20poly1305" 14 | ) 15 | 16 | const ( 17 | // HMACSize is the length of the HMACs used to verify the integrity of 18 | // the onion. Any value lower than 32 will truncate the HMAC both 19 | // during onion creation as well as during the verification. 20 | HMACSize = 32 21 | ) 22 | 23 | // chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305 24 | // encryption and decryption. 25 | var chaChaPolyZeroNonce [chacha20poly1305.NonceSize]byte 26 | 27 | // Hash256 is a statically sized, 32-byte array, typically containing 28 | // the output of a SHA256 hash. 29 | type Hash256 [sha256.Size]byte 30 | 31 | // SingleKeyECDH is an abstraction interface that hides the implementation of an 32 | // ECDH operation against a specific private key. We use this abstraction for 33 | // the long term keys which we eventually want to be able to keep in a hardware 34 | // wallet or HSM. 35 | type SingleKeyECDH interface { 36 | // PubKey returns the public key of the private key that is abstracted 37 | // away by the interface. 38 | PubKey() *btcec.PublicKey 39 | 40 | // ECDH performs a scalar multiplication (ECDH-like operation) between 41 | // the abstracted private key and a remote public key. The output 42 | // returned will be the sha256 of the resulting shared point serialized 43 | // in compressed format. 44 | ECDH(pubKey *btcec.PublicKey) ([32]byte, error) 45 | } 46 | 47 | // PrivKeyECDH is an implementation of the SingleKeyECDH in which we do have the 48 | // full private key. This can be used to wrap a temporary key to conform to the 49 | // SingleKeyECDH interface. 50 | type PrivKeyECDH struct { 51 | // PrivKey is the private key that is used for the ECDH operation. 52 | PrivKey *btcec.PrivateKey 53 | } 54 | 55 | // PubKey returns the public key of the private key that is abstracted away by 56 | // the interface. 57 | // 58 | // NOTE: This is part of the SingleKeyECDH interface. 59 | func (p *PrivKeyECDH) PubKey() *btcec.PublicKey { 60 | return p.PrivKey.PubKey() 61 | } 62 | 63 | // ECDH performs a scalar multiplication (ECDH-like operation) between the 64 | // abstracted private key and a remote public key. The output returned will be 65 | // the sha256 of the resulting shared point serialized in compressed format. If 66 | // k is our private key, and P is the public key, we perform the following 67 | // operation: 68 | // 69 | // sx := k*P 70 | // s := sha256(sx.SerializeCompressed()) 71 | // 72 | // NOTE: This is part of the SingleKeyECDH interface. 73 | func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) { 74 | var pubJ btcec.JacobianPoint 75 | pub.AsJacobian(&pubJ) 76 | 77 | var ecdhPoint btcec.JacobianPoint 78 | btcec.ScalarMultNonConst(&p.PrivKey.Key, &pubJ, &ecdhPoint) 79 | 80 | ecdhPoint.ToAffine() 81 | ecdhPubKey := btcec.NewPublicKey(&ecdhPoint.X, &ecdhPoint.Y) 82 | 83 | return sha256.Sum256(ecdhPubKey.SerializeCompressed()), nil 84 | } 85 | 86 | // DecryptedError contains the decrypted error message and its sender. 87 | type DecryptedError struct { 88 | // Sender is the node that sent the error. Note that a node may occur in 89 | // the path multiple times. If that is the case, the sender pubkey does 90 | // not tell the caller on which visit the error occurred. 91 | Sender *btcec.PublicKey 92 | 93 | // SenderIdx is the position of the error sending node in the path. 94 | // Index zero is the self node. SenderIdx allows to distinguish between 95 | // errors from nodes that occur in the path multiple times. 96 | SenderIdx int 97 | 98 | // Message is the decrypted error message. 99 | Message []byte 100 | } 101 | 102 | // zeroHMAC is the special HMAC value that allows the final node to determine 103 | // if it is the payment destination or not. 104 | var zeroHMAC [HMACSize]byte 105 | 106 | // calcMac calculates HMAC-SHA-256 over the message using the passed secret key 107 | // as input to the HMAC. 108 | func calcMac(key [keyLen]byte, msg []byte) [HMACSize]byte { 109 | hmac := hmac.New(sha256.New, key[:]) 110 | hmac.Write(msg) 111 | h := hmac.Sum(nil) 112 | 113 | var mac [HMACSize]byte 114 | copy(mac[:], h[:HMACSize]) 115 | 116 | return mac 117 | } 118 | 119 | // xor computes the byte wise XOR of a and b, storing the result in dst. Only 120 | // the frist `min(len(a), len(b))` bytes will be xor'd. 121 | func xor(dst, a, b []byte) int { 122 | n := len(a) 123 | if len(b) < n { 124 | n = len(b) 125 | } 126 | for i := 0; i < n; i++ { 127 | dst[i] = a[i] ^ b[i] 128 | } 129 | return n 130 | } 131 | 132 | // generateKey generates a new key for usage in Sphinx packet 133 | // construction/processing based off of the denoted keyType. Within Sphinx 134 | // various keys are used within the same onion packet for padding generation, 135 | // MAC generation, and encryption/decryption. 136 | func generateKey(keyType string, sharedKey *Hash256) [keyLen]byte { 137 | mac := hmac.New(sha256.New, []byte(keyType)) 138 | mac.Write(sharedKey[:]) 139 | h := mac.Sum(nil) 140 | 141 | var key [keyLen]byte 142 | copy(key[:], h[:keyLen]) 143 | 144 | return key 145 | } 146 | 147 | // generateCipherStream generates a stream of cryptographic psuedo-random bytes 148 | // intended to be used to encrypt a message using a one-time-pad like 149 | // construction. 150 | func generateCipherStream(key [keyLen]byte, numBytes uint) []byte { 151 | var ( 152 | nonce [8]byte 153 | ) 154 | cipher, err := chacha20.NewCipher(nonce[:], key[:]) 155 | if err != nil { 156 | panic(err) 157 | } 158 | output := make([]byte, numBytes) 159 | cipher.XORKeyStream(output, output) 160 | 161 | return output 162 | } 163 | 164 | // computeBlindingFactor for the next hop given the ephemeral pubKey and 165 | // sharedSecret for this hop. The blinding factor is computed as the 166 | // sha-256(pubkey || sharedSecret). 167 | func computeBlindingFactor(hopPubKey *btcec.PublicKey, 168 | hopSharedSecret []byte) btcec.ModNScalar { 169 | 170 | sha := sha256.New() 171 | sha.Write(hopPubKey.SerializeCompressed()) 172 | sha.Write(hopSharedSecret) 173 | 174 | var hash Hash256 175 | copy(hash[:], sha.Sum(nil)) 176 | 177 | var blindingBytes btcec.ModNScalar 178 | blindingBytes.SetByteSlice(hash[:]) 179 | 180 | return blindingBytes 181 | } 182 | 183 | // blindGroupElement blinds the group element P by performing scalar 184 | // multiplication of the group element by blindingFactor: blindingFactor * P. 185 | func blindGroupElement(hopPubKey *btcec.PublicKey, blindingFactor btcec.ModNScalar) *btcec.PublicKey { 186 | var hopPubKeyJ btcec.JacobianPoint 187 | hopPubKey.AsJacobian(&hopPubKeyJ) 188 | 189 | var blindedPoint btcec.JacobianPoint 190 | btcec.ScalarMultNonConst( 191 | &blindingFactor, &hopPubKeyJ, &blindedPoint, 192 | ) 193 | blindedPoint.ToAffine() 194 | 195 | return btcec.NewPublicKey(&blindedPoint.X, &blindedPoint.Y) 196 | } 197 | 198 | // blindBaseElement blinds the groups's generator G by performing scalar base 199 | // multiplication using the blindingFactor: blindingFactor * G. 200 | func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey { 201 | // TODO(roasbeef): remove after btcec version bump to add alias for 202 | // this method 203 | priv := secp.NewPrivateKey(&blindingFactor) 204 | return priv.PubKey() 205 | } 206 | 207 | // chacha20polyEncrypt initialises the ChaCha20Poly1305 algorithm with the given 208 | // key and uses it to encrypt the passed message. This uses an all-zero nonce as 209 | // required by the route-blinding spec. 210 | func chacha20polyEncrypt(key, plainTxt []byte) ([]byte, error) { 211 | aead, err := chacha20poly1305.New(key) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | return aead.Seal(plainTxt[:0], chaChaPolyZeroNonce[:], plainTxt, nil), 217 | nil 218 | } 219 | 220 | // chacha20polyDecrypt initialises the ChaCha20Poly1305 algorithm with the given 221 | // key and uses it to decrypt the passed cipher text. This uses an all-zero 222 | // nonce as required by the route-blinding spec. 223 | func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) { 224 | aead, err := chacha20poly1305.New(key) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | return aead.Open(cipherTxt[:0], chaChaPolyZeroNonce[:], cipherTxt, nil) 230 | } 231 | 232 | // sharedSecretGenerator is an interface that abstracts away exactly *how* the 233 | // shared secret for each hop is generated. 234 | // 235 | // TODO(roasbef): rename? 236 | type sharedSecretGenerator interface { 237 | // generateSharedSecret given a public key, generates a shared secret 238 | // using private data of the underlying sharedSecretGenerator. 239 | generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) 240 | } 241 | 242 | // generateSharedSecret generates the shared secret using the given ephemeral 243 | // pub key and the Router's private key. If a blindingPoint is provided then it 244 | // is used to tweak the Router's private key before creating the shared secret 245 | // with the ephemeral pub key. The blinding point is used to determine our 246 | // shared secret with the receiver. From that we can determine our shared 247 | // secret with the sender using the dhKey. 248 | func (r *Router) generateSharedSecret(dhKey, 249 | blindingPoint *btcec.PublicKey) (Hash256, error) { 250 | 251 | // If no blinding point is provided, then the un-tweaked dhKey can 252 | // be used to derive the shared secret 253 | if blindingPoint == nil { 254 | return sharedSecret(r.onionKey, dhKey) 255 | } 256 | 257 | // We use the blinding point to calculate the blinding factor that the 258 | // receiver used with us so that we can use it to tweak our priv key. 259 | // The sender would have created their shared secret with our blinded 260 | // pub key. 261 | // * ss_receiver = H(k * E_receiver) 262 | ssReceiver, err := sharedSecret(r.onionKey, blindingPoint) 263 | if err != nil { 264 | return Hash256{}, err 265 | } 266 | 267 | // Compute the blinding factor that the receiver would have used to 268 | // blind our public key. 269 | // 270 | // * bf = HMAC256("blinded_node_id", ss_receiver) 271 | blindingFactorBytes := generateKey(routeBlindingHMACKey, &ssReceiver) 272 | var blindingFactor btcec.ModNScalar 273 | blindingFactor.SetBytes(&blindingFactorBytes) 274 | 275 | // Now, we want to calculate the shared secret between the sender and 276 | // our blinded key. In other words we want to calculate: 277 | // * ss_sender = H(E_sender * bf * k) 278 | // 279 | // Since the order in which the above multiplication happens does not 280 | // matter, we will first multiply E_sender with the blinding factor: 281 | blindedEphemeral := blindGroupElement(dhKey, blindingFactor) 282 | 283 | // Finally, we compute the ECDH to get the shared secret, ss_sender: 284 | return sharedSecret(r.onionKey, blindedEphemeral) 285 | } 286 | 287 | // sharedSecret does a ECDH operation on the passed private and public keys and 288 | // returns the result. 289 | func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) { 290 | var sharedSecret Hash256 291 | 292 | // Ensure that the public key is on our curve. 293 | if !pub.IsOnCurve() { 294 | return sharedSecret, ErrInvalidOnionKey 295 | } 296 | 297 | // Compute the shared secret. 298 | return priv.ECDH(pub) 299 | } 300 | 301 | // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a 302 | // stream cipher, calling onionEncrypt on an already encrypted piece of data 303 | // will decrypt it. 304 | func onionEncrypt(sharedSecret *Hash256, data []byte) []byte { 305 | p := make([]byte, len(data)) 306 | 307 | ammagKey := generateKey("ammag", sharedSecret) 308 | streamBytes := generateCipherStream(ammagKey, uint(len(data))) 309 | xor(p, data, streamBytes) 310 | 311 | return p 312 | } 313 | 314 | // minOnionErrorLength is the minimally expected length of the onion error 315 | // message. Including padding, all messages on the wire should be at least 256 316 | // bytes. We then add the size of the sha256 HMAC as well. 317 | const minOnionErrorLength = 2 + 2 + 256 + sha256.Size 318 | 319 | // DecryptError attempts to decrypt the passed encrypted error response. The 320 | // onion failure is encrypted in backward manner, starting from the node where 321 | // error have occurred. As a result, in order to decrypt the error we need get 322 | // all shared secret and apply decryption in the reverse order. A structure is 323 | // returned that contains the decrypted error message and information on the 324 | // sender. 325 | func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( 326 | *DecryptedError, error) { 327 | 328 | // Ensure the error message length is as expected. 329 | if len(encryptedData) < minOnionErrorLength { 330 | return nil, fmt.Errorf("invalid error length: "+ 331 | "expected at least %v got %v", minOnionErrorLength, 332 | len(encryptedData)) 333 | } 334 | 335 | sharedSecrets, _, err := generateSharedSecrets( 336 | o.circuit.PaymentPath, 337 | o.circuit.SessionKey, 338 | ) 339 | if err != nil { 340 | return nil, fmt.Errorf("error generating shared secret: %v", 341 | err) 342 | } 343 | 344 | var ( 345 | sender int 346 | msg []byte 347 | dummySecret Hash256 348 | ) 349 | copy(dummySecret[:], bytes.Repeat([]byte{1}, 32)) 350 | 351 | // We'll iterate a constant amount of hops to ensure that we don't give 352 | // away an timing information pertaining to the position in the route 353 | // that the error emanated from. 354 | for i := 0; i < NumMaxHops; i++ { 355 | var sharedSecret Hash256 356 | 357 | // If we've already found the sender, then we'll use our dummy 358 | // secret to continue decryption attempts to fill out the rest 359 | // of the loop. Otherwise, we'll use the next shared secret in 360 | // line. 361 | if sender != 0 || i > len(sharedSecrets)-1 { 362 | sharedSecret = dummySecret 363 | } else { 364 | sharedSecret = sharedSecrets[i] 365 | } 366 | 367 | // With the shared secret, we'll now strip off a layer of 368 | // encryption from the encrypted error payload. 369 | encryptedData = onionEncrypt(&sharedSecret, encryptedData) 370 | 371 | // Next, we'll need to separate the data, from the MAC itself 372 | // so we can reconstruct and verify it. 373 | expectedMac := encryptedData[:sha256.Size] 374 | data := encryptedData[sha256.Size:] 375 | 376 | // With the data split, we'll now re-generate the MAC using its 377 | // specified key. 378 | umKey := generateKey("um", &sharedSecret) 379 | h := hmac.New(sha256.New, umKey[:]) 380 | h.Write(data) 381 | 382 | // If the MAC matches up, then we've found the sender of the 383 | // error and have also obtained the fully decrypted message. 384 | realMac := h.Sum(nil) 385 | if hmac.Equal(realMac, expectedMac) && sender == 0 { 386 | sender = i + 1 387 | msg = data 388 | } 389 | } 390 | 391 | // If the sender index is still zero, then we haven't found the sender, 392 | // meaning we've failed to decrypt. 393 | if sender == 0 { 394 | return nil, errors.New("unable to retrieve onion failure") 395 | } 396 | 397 | return &DecryptedError{ 398 | SenderIdx: sender, 399 | Sender: o.circuit.PaymentPath[sender-1], 400 | Message: msg, 401 | }, nil 402 | } 403 | 404 | // EncryptError is used to make data obfuscation using the generated shared 405 | // secret. 406 | // 407 | // In context of Lightning Network is either used by the nodes in order to make 408 | // initial obfuscation with the creation of the hmac or by the forwarding nodes 409 | // for backward failure obfuscation of the onion failure blob. By obfuscating 410 | // the onion failure on every node in the path we are adding additional step of 411 | // the security and barrier for malware nodes to retrieve valuable information. 412 | // The reason for using onion obfuscation is to not give 413 | // away to the nodes in the payment path the information about the exact 414 | // failure and its origin. 415 | func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte { 416 | if initial { 417 | umKey := generateKey("um", &o.sharedSecret) 418 | hash := hmac.New(sha256.New, umKey[:]) 419 | hash.Write(data) 420 | h := hash.Sum(nil) 421 | data = append(h, data...) 422 | } 423 | 424 | return onionEncrypt(&o.sharedSecret, data) 425 | } 426 | -------------------------------------------------------------------------------- /obfuscation_test.go: -------------------------------------------------------------------------------- 1 | package sphinx 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/btcsuite/btcd/btcec/v2" 11 | ) 12 | 13 | // TestOnionFailure checks the ability of sender of payment to decode the 14 | // obfuscated onion error. 15 | func TestOnionFailure(t *testing.T) { 16 | // Create numHops random sphinx paymentPath. 17 | paymentPath := make([]*btcec.PublicKey, 5) 18 | for i := 0; i < len(paymentPath); i++ { 19 | privKey, err := btcec.NewPrivateKey() 20 | if err != nil { 21 | t.Fatalf("unable to generate random key for sphinx node: %v", err) 22 | } 23 | paymentPath[i] = privKey.PubKey() 24 | } 25 | sessionKey, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{'A'}, 32)) 26 | 27 | // Reduce the error path on one node, in order to check that we are 28 | // able to receive the error not only from last hop. 29 | errorPath := paymentPath[:len(paymentPath)-1] 30 | 31 | failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength-sha256.Size) 32 | sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) 33 | if err != nil { 34 | t.Fatalf("Unexpected error while generating secrets: %v", err) 35 | } 36 | 37 | // Emulate creation of the obfuscator on node where error have occurred. 38 | obfuscator := &OnionErrorEncrypter{ 39 | sharedSecret: sharedSecrets[len(errorPath)-1], 40 | } 41 | 42 | // Emulate the situation when last hop creates the onion failure 43 | // message and send it back. 44 | obfuscatedData := obfuscator.EncryptError(true, failureData) 45 | 46 | // Emulate that failure message is backward obfuscated on every hop. 47 | for i := len(errorPath) - 2; i >= 0; i-- { 48 | // Emulate creation of the obfuscator on forwarding node which 49 | // propagates the onion failure. 50 | obfuscator = &OnionErrorEncrypter{ 51 | sharedSecret: sharedSecrets[i], 52 | } 53 | obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) 54 | } 55 | 56 | // Emulate creation of the deobfuscator on the receiving onion error side. 57 | deobfuscator := NewOnionErrorDecrypter(&Circuit{ 58 | SessionKey: sessionKey, 59 | PaymentPath: paymentPath, 60 | }) 61 | 62 | // Emulate that sender node receive the failure message and trying to 63 | // unwrap it, by applying obfuscation and checking the hmac. 64 | decryptedError, err := deobfuscator.DecryptError(obfuscatedData) 65 | if err != nil { 66 | t.Fatalf("unable to de-obfuscate the onion failure: %v", err) 67 | } 68 | 69 | // We should understand the node from which error have been received. 70 | if !bytes.Equal(decryptedError.Sender.SerializeCompressed(), 71 | errorPath[len(errorPath)-1].SerializeCompressed()) { 72 | t.Fatalf("unable to properly conclude from which node in " + 73 | "the path we received an error") 74 | } 75 | 76 | if decryptedError.SenderIdx != len(errorPath) { 77 | t.Fatalf("unable to properly conclude from which node in " + 78 | "the path we received an error") 79 | } 80 | 81 | // Check that message have been properly de-obfuscated. 82 | if !bytes.Equal(decryptedError.Message, failureData) { 83 | t.Fatalf("data not equals, expected: \"%v\", real: \"%v\"", 84 | string(failureData), string(decryptedError.Message)) 85 | } 86 | } 87 | 88 | // onionErrorData is a specification onion error obfuscation data which is 89 | // produces by another lightning network node. 90 | var onionErrorData = []struct { 91 | sharedSecret string 92 | stream string 93 | ammagKey string 94 | obfuscatedData string 95 | }{ 96 | { 97 | sharedSecret: "b5756b9b542727dbafc6765a49488b023a725d631af688" + 98 | "fc031217e90770c328", 99 | ammagKey: "2f36bb8822e1f0d04c27b7d8bb7d7dd586e032a3218b8d414a" + 100 | "fbba6f169a4d68", 101 | stream: "e9c975b07c9a374ba64fd9be3aae955e917d34d1fa33f2e90f53bbf4394713c6a8c9b16ab5f12fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4", 102 | obfuscatedData: "a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4", 103 | }, 104 | { 105 | sharedSecret: "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e" + 106 | "7ecc8c725503ad02d", 107 | ammagKey: "cd9ac0e09064f039fa43a31dea05f5fe5f6443d40a98be4071" + 108 | "af4a9d704be5ad", 109 | stream: "617ca1e4624bc3f04fece3aa5a2b615110f421ec62408d16c48ea6c1b7c33fe7084a2bd9d4652fc5068e5052bf6d0acae2176018a3d8c75f37842712913900263cff92f39f3c18aa1f4b20a93e70fc429af7b2b1967ca81a761d40582daf0eb49cef66e3d6fbca0218d3022d32e994b41c884a27c28685ef1eb14603ea80a204b2f2f474b6ad5e71c6389843e3611ebeafc62390b717ca53b3670a33c517ef28a659c251d648bf4c966a4ef187113ec9848bf110816061ca4f2f68e76ceb88bd6208376460b916fb2ddeb77a65e8f88b2e71a2cbf4ea4958041d71c17d05680c051c3676fb0dc8108e5d78fb1e2c44d79a202e9d14071d536371ad47c39a05159e8d6c41d17a1e858faaaf572623aa23a38ffc73a4114cb1ab1cd7f906c6bd4e21b29694", 110 | obfuscatedData: "c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270", 111 | }, 112 | { 113 | sharedSecret: "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c3" + 114 | "4120b30bc9c04891cc", 115 | ammagKey: "1bf08df8628d452141d56adfd1b25c1530d7921c23cecfc749" + 116 | "ac03a9b694b0d3", 117 | stream: "6149f48b5a7e8f3d6f5d870b7a698e204cf64452aab4484ff1dee671fe63fd4b5f1b78ee2047dfa61e3d576b149bedaf83058f85f06a3172a3223ad6c4732d96b32955da7d2feb4140e58d86fc0f2eb5d9d1878e6f8a7f65ab9212030e8e915573ebbd7f35e1a430890be7e67c3fb4bbf2def662fa625421e7b411c29ebe81ec67b77355596b05cc155755664e59c16e21410aabe53e80404a615f44ebb31b365ca77a6e91241667b26c6cad24fb2324cf64e8b9dd6e2ce65f1f098cfd1ef41ba2d4c7def0ff165a0e7c84e7597c40e3dffe97d417c144545a0e38ee33ebaae12cc0c14650e453d46bfc48c0514f354773435ee89b7b2810606eb73262c77a1d67f3633705178d79a1078c3a01b5fadc9651feb63603d19decd3a00c1f69af2dab259593", 118 | obfuscatedData: "a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3", 119 | }, 120 | { 121 | sharedSecret: "a6519e98832a0b179f62123b3567c106db99ee37bef036" + 122 | "e783263602f3488fae", 123 | ammagKey: "59ee5867c5c151daa31e36ee42530f429c433836286e63744f" + 124 | "2020b980302564", 125 | stream: "0f10c86f05968dd91188b998ee45dcddfbf89fe9a99aa6375c42ed5520a257e048456fe417c15219ce39d921555956ae2ff795177c63c819233f3bcb9b8b28e5ac6e33a3f9b87ca62dff43f4cc4a2755830a3b7e98c326b278e2bd31f4a9973ee99121c62873f5bfb2d159d3d48c5851e3b341f9f6634f51939188c3b9ff45feeb11160bb39ce3332168b8e744a92107db575ace7866e4b8f390f1edc4acd726ed106555900a0832575c3a7ad11bb1fe388ff32b99bcf2a0d0767a83cf293a220a983ad014d404bfa20022d8b369fe06f7ecc9c74751dcda0ff39d8bca74bf9956745ba4e5d299e0da8f68a9f660040beac03e795a046640cf8271307a8b64780b0588422f5a60ed7e36d60417562938b400802dac5f87f267204b6d5bcfd8a05b221ec2", 126 | obfuscatedData: "aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921", 127 | }, 128 | { 129 | sharedSecret: "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b" + 130 | "348c4e1cd36a03ea66", 131 | ammagKey: "3761ba4d3e726d8abb16cba5950ee976b84937b61b7ad09e74" + 132 | "1724d7dee12eb5", 133 | stream: "3699fd352a948a05f604763c0bca2968d5eaca2b0118602e52e59121f050936c8dd90c24df7dc8cf8f1665e39a6c75e9e2c0900ea245c9ed3b0008148e0ae18bbfaea0c711d67eade980c6f5452e91a06b070bbde68b5494a92575c114660fb53cf04bf686e67ffa4a0f5ae41a59a39a8515cb686db553d25e71e7a97cc2febcac55df2711b6209c502b2f8827b13d3ad2f491c45a0cafe7b4d8d8810e805dee25d676ce92e0619b9c206f922132d806138713a8f69589c18c3fdc5acee41c1234b17ecab96b8c56a46787bba2c062468a13919afc18513835b472a79b2c35f9a91f38eb3b9e998b1000cc4a0dbd62ac1a5cc8102e373526d7e8f3c3a1b4bfb2f8a3947fe350cb89f73aa1bb054edfa9895c0fc971c2b5056dc8665902b51fced6dff80c", 134 | obfuscatedData: "9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d", 135 | }, 136 | } 137 | 138 | func getSpecPubKeys() ([]*btcec.PublicKey, error) { 139 | specPubKeys := []string{ 140 | "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", 141 | "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 142 | "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", 143 | "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", 144 | "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", 145 | } 146 | 147 | keys := make([]*btcec.PublicKey, len(specPubKeys)) 148 | for i, sKey := range specPubKeys { 149 | bKey, err := hex.DecodeString(sKey) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | key, err := btcec.ParsePubKey(bKey) 155 | if err != nil { 156 | return nil, err 157 | } 158 | keys[i] = key 159 | } 160 | return keys, nil 161 | } 162 | 163 | func getSpecSessionKey() (*btcec.PrivateKey, error) { 164 | bKey, err := hex.DecodeString("4141414141414141414141414141414141414" + 165 | "141414141414141414141414141") 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | privKey, _ := btcec.PrivKeyFromBytes(bKey) 171 | return privKey, nil 172 | } 173 | 174 | func getSpecOnionErrorData() ([]byte, error) { 175 | sData := "0002200200fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 176 | return hex.DecodeString(sData) 177 | } 178 | 179 | // TestOnionFailureSpecVector checks that onion error corresponds to the 180 | // specification. 181 | func TestOnionFailureSpecVector(t *testing.T) { 182 | failureData, err := getSpecOnionErrorData() 183 | if err != nil { 184 | t.Fatalf("unable to get specification onion failure "+ 185 | "data: %v", err) 186 | } 187 | 188 | paymentPath, err := getSpecPubKeys() 189 | if err != nil { 190 | t.Fatalf("unable to get specification public keys: %v", err) 191 | } 192 | 193 | sessionKey, err := getSpecSessionKey() 194 | if err != nil { 195 | t.Fatalf("unable to get specification session key: %v", err) 196 | } 197 | 198 | var obfuscatedData []byte 199 | sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) 200 | if err != nil { 201 | t.Fatalf("Unexpected error while generating secrets: %v", err) 202 | } 203 | for i, test := range onionErrorData { 204 | // Decode the shared secret and check that it matchs with 205 | // specification. 206 | expectedSharedSecret, err := hex.DecodeString(test.sharedSecret) 207 | if err != nil { 208 | t.Fatalf("unable to decode spec shared secret: %v", 209 | err) 210 | } 211 | obfuscator := &OnionErrorEncrypter{ 212 | sharedSecret: sharedSecrets[len(sharedSecrets)-1-i], 213 | } 214 | 215 | var b bytes.Buffer 216 | if err := obfuscator.Encode(&b); err != nil { 217 | t.Fatalf("unable to encode obfuscator: %v", err) 218 | } 219 | 220 | obfuscator2 := &OnionErrorEncrypter{} 221 | obfuscatorReader := bytes.NewReader(b.Bytes()) 222 | if err := obfuscator2.Decode(obfuscatorReader); err != nil { 223 | t.Fatalf("unable to decode obfuscator: %v", err) 224 | } 225 | 226 | if !reflect.DeepEqual(obfuscator, obfuscator2) { 227 | t.Fatalf("unable to reconstruct obfuscator: %v", err) 228 | } 229 | 230 | if !bytes.Equal(expectedSharedSecret, obfuscator.sharedSecret[:]) { 231 | t.Fatalf("shared secret not match with spec: expected "+ 232 | "%x, got %x", expectedSharedSecret, 233 | obfuscator.sharedSecret[:]) 234 | } 235 | 236 | if i == 0 { 237 | // Emulate the situation when last hop creates the onion failure 238 | // message and send it back. 239 | obfuscatedData = obfuscator.EncryptError(true, failureData) 240 | } else { 241 | // Emulate the situation when forward node obfuscates 242 | // the onion failure. 243 | obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) 244 | } 245 | 246 | // Decode the obfuscated data and check that it matches the 247 | // specification. 248 | expectedEncryptErrordData, err := hex.DecodeString(test.obfuscatedData) 249 | if err != nil { 250 | t.Fatalf("unable to decode spec obfusacted "+ 251 | "data: %v", err) 252 | } 253 | if !bytes.Equal(expectedEncryptErrordData, obfuscatedData) { 254 | t.Fatalf("obfuscated data not match spec: expected %x, "+ 255 | "got %x", expectedEncryptErrordData[:], 256 | obfuscatedData[:]) 257 | } 258 | } 259 | 260 | deobfuscator := NewOnionErrorDecrypter(&Circuit{ 261 | SessionKey: sessionKey, 262 | PaymentPath: paymentPath, 263 | }) 264 | 265 | // Emulate that sender node receives the failure message and trying to 266 | // unwrap it, by applying obfuscation and checking the hmac. 267 | decryptedError, err := deobfuscator.DecryptError(obfuscatedData) 268 | if err != nil { 269 | t.Fatalf("unable to de-obfuscate the onion failure: %v", err) 270 | } 271 | 272 | // Check that message have been properly de-obfuscated. 273 | if !bytes.Equal(decryptedError.Message, failureData) { 274 | t.Fatalf("data not equals, expected: \"%v\", real: \"%v\"", 275 | string(failureData), string(decryptedError.Message)) 276 | } 277 | 278 | // We should understand the node from which error have been received. 279 | if !bytes.Equal(decryptedError.Sender.SerializeCompressed(), 280 | paymentPath[len(paymentPath)-1].SerializeCompressed()) { 281 | t.Fatalf("unable to properly conclude from which node in " + 282 | "the path we received an error") 283 | } 284 | 285 | if decryptedError.SenderIdx != len(paymentPath) { 286 | t.Fatalf("unable to properly conclude from which node in " + 287 | "the path we received an error") 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /testdata/blinded-onion-message-onion-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "Test vector creating an onionmessage, including joining an existing one", 3 | "generate": { 4 | "comment": "This sections contains test data for Dave's blinded path Bob->Dave; sender has to prepend a hop to Alice to reach Bob", 5 | "session_key": "0303030303030303030303030303030303030303030303030303030303030303", 6 | "hops": [ 7 | { 8 | "alias": "Alice", 9 | "comment": "Alice->Bob: note next_path_key_override to match that give by Dave for Bob", 10 | "path_key_secret": "6363636363636363636363636363636363636363636363636363636363636363", 11 | "tlvs": { 12 | "next_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 13 | "next_path_key_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 14 | "path_key_override_secret": "0101010101010101010101010101010101010101010101010101010101010101" 15 | }, 16 | "encrypted_data_tlv": "04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 17 | "ss": "c04d2a4c518241cb49f2800eea92554cb543f268b4c73f85693541e86d649205", 18 | "HMAC256('blinded_node_id', ss)": "bc5388417c8db33af18ab7ba43f6a5641861f7b0ecb380e501a739af446a7bf4", 19 | "blinded_node_id": "02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1", 20 | "E": "031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99", 21 | "H(E || ss)": "83377bd6096f82df3a46afec20d68f3f506168f2007f6e86c2dc267417de9e34", 22 | "next_e": "bf3e8999518c0bb6e876abb0ae01d44b9ba211720048099a2ba5a83afd730cad01", 23 | "rho": "6926df9d4522b26ad4330a51e3481208e4816edd9ae4feaf311ea0342eb90c44", 24 | "encrypted_recipient_data": "49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b" 25 | }, 26 | { 27 | "alias": "Bob", 28 | "comment": "Bob->Carol", 29 | "path_key_secret": "0101010101010101010101010101010101010101010101010101010101010101", 30 | "tlvs": { 31 | "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", 32 | "unknown_tag_561": "123456" 33 | }, 34 | "encrypted_data_tlv": "0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007fd023103123456", 35 | "ss": "196f1f3e0be9d65f88463c1ab63e07f41b4e7c0368c28c3e6aa290cc0d22eaed", 36 | "HMAC256('blinded_node_id', ss)": "c331d35827bdd509a02f1e64d48c7f0d7b2603355abbb1a3733c86e50135608e", 37 | "blinded_node_id": "03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a", 38 | "E": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", 39 | "H(E || ss)": "1889a6cf337d9b34f80bb23a91a2ca194e80d7614f0728bdbda153da85e46b69", 40 | "next_e": "f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce01", 41 | "rho": "db991242ce366ab44272f38383476669b713513818397a00d4808d41ea979827", 42 | "encrypted_recipient_data": "adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5" 43 | }, 44 | { 45 | "alias": "Carol", 46 | "comment": "Carol->Dave", 47 | "path_key_secret": "f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce", 48 | "tlvs": { 49 | "padding": "0000000000", 50 | "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" 51 | }, 52 | "encrypted_data_tlv": "010500000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", 53 | "ss": "c7b33d74a723e26331a91c15ae5bc77db28a18b801b6bc5cd5bba98418303a9d", 54 | "HMAC256('blinded_node_id', ss)": "a684c7495444a8cc2a6dfdecdf0819f3cdf4e86b81cc14e39825a40872ecefff", 55 | "blinded_node_id": "035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b", 56 | "E": "02b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582", 57 | "H(E || ss)": "2d80c5619a5a68d22dd3d784cab584c2718874922735d36cb36a179c10a796ca", 58 | "next_e": "5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea01", 59 | "rho": "739851e89b61cab34ee9ba7d5f3c342e4adc8b91a72991664026f68a685f0bdc", 60 | "encrypted_recipient_data": "d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac" 61 | }, 62 | { 63 | "alias": "Dave", 64 | "comment": "Dave is final node, hence path_id", 65 | "path_key_secret": "5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea", 66 | "tlvs": { 67 | "padding": "", 68 | "path_id": "deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0", 69 | "unknown_tag_65535": "06c1" 70 | }, 71 | "encrypted_data_tlv": "01000620deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0fdffff0206c1", 72 | "ss": "024955ed0d4ebbfab13498f5d7aacd00bf096c8d9ed0473cdfc96d90053c86b7", 73 | "HMAC256('blinded_node_id', ss)": "3f5612df60f050ac571aeaaf76655e138529bea6d23293ebe15659f2588cd039", 74 | "blinded_node_id": "0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6", 75 | "E": "025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c", 76 | "H(E || ss)": "db5719e79919d706eab17eebaad64bd691e56476a42f0e26ae60caa9082f56fa", 77 | "next_e": "ae31d2fbbf2f59038542c13287b9b624ea1a212c82be87c137c3d92aa30a185d01", 78 | "rho": "c47cde57edc790df7b9b6bf921aff5e5eee43f738ab8fa9103ef675495f3f50e", 79 | "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" 80 | } 81 | ] 82 | }, 83 | "route": { 84 | "comment": "The resulting blinded route Alice to Dave.", 85 | "first_node_id": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", 86 | "first_path_key": "031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99", 87 | "hops": [ 88 | { 89 | "blinded_node_id": "02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1", 90 | "encrypted_recipient_data": "49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b" 91 | }, 92 | { 93 | "blinded_node_id": "03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a", 94 | "encrypted_recipient_data": "adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5" 95 | }, 96 | { 97 | "blinded_node_id": "035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b", 98 | "encrypted_recipient_data": "d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac" 99 | }, 100 | { 101 | "blinded_node_id": "0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6", 102 | "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" 103 | } 104 | ] 105 | }, 106 | "onionmessage": { 107 | "comment": "An onion message which sends a 'hello' to Dave", 108 | "unknown_tag_1": "68656c6c6f", 109 | "onion_message_packet": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb" 110 | }, 111 | "decrypt": { 112 | "comment": "This section contains the internal values generated by intermediate nodes when decrypting the onion.", 113 | "hops": [ 114 | { 115 | "alias": "Alice", 116 | "privkey": "4141414141414141414141414141414141414141414141414141414141414141", 117 | "onion_message": "0201031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd9905560002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb", 118 | "next_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" 119 | }, 120 | { 121 | "alias": "Bob", 122 | "privkey": "4242424242424242424242424242424242424242424242424242424242424242", 123 | "onion_message": "0201031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f05560002536d53f93796cad550b6c68662dca41f7e8c221c31022c64dd1a627b2df3982b25eac261e88369cfc66e1e3b6d9829cb3dcd707046e68a7796065202a7904811bf2608c5611cf74c9eb5371c7eb1a4428bb39a041493e2a568ddb0b2482a6cc6711bc6116cef144ebf988073cb18d9dd4ce2d3aa9de91a7dc6d7c6f11a852024626e66b41ba1158055505dff9cb15aa51099f315564d9ee3ed6349665dc3e209eedf9b5805ee4f69d315df44c80e63d0e2efbdab60ec96f44a3447c6a6ddb1efb6aa4e072bde1dab974081646bfddf3b02daa2b83847d74dd336465e76e9b8fecc2b0414045eeedfc39939088a76820177dd1103c99939e659beb07197bab9f714b30ba8dc83738e9a6553a57888aaeda156c68933a2f4ff35e3f81135076b944ed9856acbfee9c61299a5d1763eadd14bf5eaf71304c8e165e590d7ecbcd25f1650bf5b6c2ad1823b2dc9145e168974ecf6a2273c94decff76d94bc6708007a17f22262d63033c184d0166c14f41b225a956271947aae6ce65890ed8f0d09c6ffe05ec02ee8b9de69d7077a0c5adeb813aabcc1ba8975b73ab06ddea5f4db3c23a1de831602de2b83f990d4133871a1a81e53f86393e6a7c3a7b73f0c099fa72afe26c3027bb9412338a19303bd6e6591c04fb4cde9b832b5f41ae199301ea8c303b5cef3aca599454273565de40e1148156d1f97c1aa9e58459ab318304075e034f5b7899c12587b86776a18a1da96b7bcdc22864fccc4c41538ebce92a6f054d53bf46770273a70e75fe0155cd6d2f2e937465b0825ce3123b8c206fac4c30478fa0f08a97ade7216dce11626401374993213636e93545a31f500562130f2feb04089661ad8c34d5a4cbd2e4e426f37cb094c786198a220a2646ecadc38c04c29ee67b19d662c209a7b30bfecc7fe8bf7d274de0605ee5df4db490f6d32234f6af639d3fce38a2801bcf8d51e9c090a6c6932355a83848129a378095b34e71cb8f51152dc035a4fe8e802fec8de221a02ba5afd6765ce570bef912f87357936ea0b90cb2990f56035e89539ec66e8dbd6ed50835158614096990e019c3eba3d7dd6a77147641c6145e8b17552cd5cf7cd163dd40b9eaeba8c78e03a2cd8c0b7997d6f56d35f38983a202b4eb8a54e14945c4de1a6dde46167e11708b7a5ff5cb9c0f7fc12fae49a012aa90bb1995c038130b749c48e6f1ffb732e92086def42af10fbc460d94abeb7b2fa744a5e9a491d62a08452be8cf2fdef573deedc1fe97098bce889f98200b26f9bb99da9aceddda6d793d8e0e44a2601ef4590cfbb5c3d0197aac691e3d31c20fd8e38764962ca34dabeb85df28feabaf6255d4d0df3d814455186a84423182caa87f9673df770432ad8fdfe78d4888632d460d36d2719e8fa8e4b4ca10d817c5d6bc44a8b2affab8c2ba53b8bf4994d63286c2fad6be04c28661162fa1a67065ecda8ba8c13aee4a8039f4f0110e0c0da2366f178d8903e19136dad6df9d8693ce71f3a270f9941de2a93d9b67bc516207ac1687bf6e00b29723c42c7d9c90df9d5e599dbeb7b73add0a6a2b7aba82f98ac93cb6e60494040445229f983a81c34f7f686d166dfc98ec23a6318d4a02a311ac28d655ea4e0f9c3014984f31e621ef003e98c373561d9040893feece2e0fa6cd2dd565e6fbb2773a2407cb2c3273c306cf71f427f2e551c4092e067cf9869f31ac7c6c80dd52d4f85be57a891a41e34be0d564e39b4af6f46b85339254a58b205fb7e10e7d0470ee73622493f28c08962118c23a1198467e72c4ae1cd482144b419247a5895975ea90d135e2a46ef7e5794a1551a447ff0a0d299b66a7f565cd86531f5e7af5408d85d877ce95b1df12b88b7d5954903a5296325ba478ba1e1a9d1f30a2d5052b2e2889bbd64f72c72bc71d8817288a2", 124 | "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007" 125 | }, 126 | { 127 | "alias": "Carol", 128 | "privkey": "4343434343434343434343434343434343434343434343434343434343434343", 129 | "onion_message": "020102b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582055600029a77e8523162efa1f4208f4f2050cd5c386ddb6ce6d36235ea569d217ec52209fb85fdf7dbc4786c373eebdba0ddc184cfbe6da624f610e93f62c70f2c56be1090b926359969f040f932c03f53974db5656233bd60af375517d4323002937d784c2c88a564bcefe5c33d3fc21c26d94dfacab85e2e19685fd2ff4c543650958524439b6da68779459aee5ffc9dc543339acec73ff43be4c44ddcbe1c11d50e2411a67056ba9db7939d780f5a86123fdd3abd6f075f7a1d78ab7daf3a82798b7ec1e9f1345bc0d1e935098497067e2ae5a51ece396fcb3bb30871ad73aee51b2418b39f00c8e8e22be4a24f4b624e09cb0414dd46239de31c7be035f71e8da4f5a94d15b44061f46414d3f355069b5c5b874ba56704eb126148a22ec873407fe118972127e63ff80e682e410f297f23841777cec0517e933eaf49d7e34bd203266b42081b3a5193b51ccd34b41342bc67cf73523b741f5c012ba2572e9dda15fbe131a6ac2ff24dc2a7622d58b9f3553092cfae7fae3c8864d95f97aa49ec8edeff5d9f5782471160ee412d82ff6767030fc63eec6a93219a108cd41433834b26676a39846a944998796c79cd1cc460531b8ded659cedfd8aecefd91944f00476f1496daafb4ea6af3feacac1390ea510709783c2aa81a29de27f8959f6284f4684102b17815667cbb0645396ac7d542b878d90c42a1f7f00c4c4eedb2a22a219f38afadb4f1f562b6e000a94e75cc38f535b43a3c0384ccef127fde254a9033a317701c710b2b881065723486e3f4d3eea5e12f374a41565fe43fa137c1a252c2153dde055bb343344c65ad0529010ece29bbd405effbebfe3ba21382b94a60ac1a5ffa03f521792a67b30773cb42e862a8a02a8bbd41b842e115969c87d1ff1f8c7b5726b9f20772dd57fe6e4ea41f959a2a673ffad8e2f2a472c4c8564f3a5a47568dd75294b1c7180c500f7392a7da231b1fe9e525ea2d7251afe9ca52a17fe54a116cb57baca4f55b9b6de915924d644cba9dade4ccc01939d7935749c008bafc6d3ad01cd72341ce5ddf7a5d7d21cf0465ab7a3233433aef21f9acf2bfcdc5a8cc003adc4d82ac9d72b36eb74e05c9aa6ccf439ac92e6b84a3191f0764dd2a2e0b4cc3baa08782b232ad6ecd3ca6029bc08cc094aef3aebddcaddc30070cb6023a689641de86cfc6341c8817215a4650f844cd2ca60f2f10c6e44cfc5f23912684d4457bf4f599879d30b79bf12ef1ab8d34dddc15672b82e56169d4c770f0a2a7a960b1e8790773f5ff7fce92219808f16d061cc85e053971213676d28fb48925e9232b66533dbd938458eb2cc8358159df7a2a2e4cf87500ede2afb8ce963a845b98978edf26a6948d4932a6b95d022004556d25515fe158092ce9a913b4b4a493281393ca731e8d8e5a3449b9d888fc4e73ffcbb9c6d6d66e88e03cf6e81a0496ede6e4e4172b08c000601993af38f80c7f68c9d5fff9e0e215cff088285bf039ca731744efcb7825a272ca724517736b4890f47e306b200aa2543c363e2c9090bcf3cf56b5b86868a62471c7123a41740392fc1d5ab28da18dca66618e9af7b42b62b23aba907779e73ca03ec60e6ab9e0484b9cae6578e0fddb6386cb3468506bf6420298bf4a690947ab582255551d82487f271101c72e19e54872ab47eae144db66bc2f8194a666a5daec08d12822cb83a61946234f2dfdbd6ca7d8763e6818adee7b401fcdb1ac42f9df1ac5cc5ac131f2869013c8d6cd29d4c4e3d05bccd34ca83366d616296acf854fa05149bfd763a25b9938e96826a037fdcb85545439c76df6beed3bdbd01458f9cf984997cc4f0a7ac3cc3f5e1eeb59c09cadcf5a537f16e444149c8f17d4bdaef16c9fbabc5ef06eb0f0bf3a07a1beddfeacdaf1df5582d6dbd6bb808d6ab31bc22e5d7", 130 | "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" 131 | }, 132 | { 133 | "alias": "Dave", 134 | "privkey": "4444444444444444444444444444444444444444444444444444444444444444", 135 | "onion_message": "0201025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c055600025550b2910294fa73bda99b9de9c851be9cbb481e23194a1743033630efba546b86e7d838d0f6e9cc0ed088dbf6889f0dceca3bfc745bd77d013a31311fa932a8bf1d28387d9ff521eabc651dee8f861fed609a68551145a451f017ec44978addeee97a423c08445531da488fd1ddc998e9cdbfcea59517b53fbf1833f0bbe6188dba6ca773a247220ec934010daca9cc185e1ceb136803469baac799e27a0d82abe53dc48a06a55d1f643885cc7894677dd20a4e4152577d1ba74b870b9279f065f9b340cedb3ca13b7df218e853e10ccd1b59c42a2acf93f489e170ee4373d30ab158b60fc20d3ba73a1f8c750951d69fb5b9321b968ddc8114936412346aff802df65516e1c09c51ef19849ff36c0199fd88c8bec301a30fef0c7cb497901c038611303f64e4174b5daf42832aa5586b84d2c9b95f382f4269a5d1bd4be898618dc78dfd451170f72ca16decac5b03e60702112e439cadd104fb3bbb3d5023c9b80823fdcd0a212a7e1aaa6eeb027adc7f8b3723031d135a09a979a4802788bb7861c6cc85501fb91137768b70aeab309b27b885686604ffc387004ac4f8c44b101c39bc0597ef7fd957f53fc5051f534b10eb3852100962b5e58254e5558689913c26ad6072ea41f5c5db10077cfc91101d4ae393be274c74297da5cc381cd88d54753aaa7df74b2f9da8d88a72bc9218fcd1f19e4ff4aace182312b9509c5175b6988f044c5756d232af02a451a02ca752f3c52747773acff6fd07d2032e6ce562a2c42105d106eba02d0b1904182cdc8c74875b082d4989d3a7e9f0e73de7c75d357f4af976c28c0b206c5e8123fc2391d078592d0d5ff686fd245c0a2de2e535b7cca99c0a37d432a8657393a9e3ca53eec1692159046ba52cb9bc97107349d8673f74cbc97e231f1108005c8d03e24ca813cea2294b39a7a493bcc062708f1f6cf0074e387e7d50e0666ce784ef4d31cb860f6cad767438d9ea5156ff0ae86e029e0247bf94df75ee0cda4f2006061455cb2eaff513d558863ae334cef7a3d45f55e7cc13153c6719e9901c1d4db6c03f643b69ea4860690305651794284d9e61eb848ccdf5a77794d376f0af62e46d4835acce6fd9eef5df73ebb8ea3bb48629766967f446e744ecc57ff3642c4aa1ccee9a2f72d5caa75fa05787d08b79408fce792485fdecdc25df34820fb061275d70b84ece540b0fc47b2453612be34f2b78133a64e812598fbe225fd85415f8ffe5340ce955b5fd9d67dd88c1c531dde298ed25f96df271558c812c26fa386966c76f03a6ebccbca49ac955916929bd42e134f982dde03f924c464be5fd1ba44f8dc4c3cbc8162755fd1d8f7dc044b15b1a796c53df7d8769bb167b2045b49cc71e08908796c92c16a235717cabc4bb9f60f8f66ff4fff1f9836388a99583acebdff4a7fb20f48eedcd1f4bdcc06ec8b48e35307df51d9bc81d38a94992dd135b30079e1f592da6e98dff496cb1a7776460a26b06395b176f585636ebdf7eab692b227a31d6979f5a6141292698e91346b6c806b90c7c6971e481559cae92ee8f4136f2226861f5c39ddd29bbdb118a35dece03f49a96804caea79a3dacfbf09d65f2611b5622de51d98e18151acb3bb84c09caaa0cc80edfa743a4679f37d6167618ce99e73362fa6f213409931762618a61f1738c071bba5afc1db24fe94afb70c40d731908ab9a505f76f57a7d40e708fd3df0efc5b7cbb2a7b75cd23449e09684a2f0e2bfa0d6176c35f96fe94d92fc9fa4103972781f81cb6e8df7dbeb0fc529c600d768bed3f08828b773d284f69e9a203459d88c12d6df7a75be2455fec128f07a497a2b2bf626cc6272d0419ca663e9dc66b8224227eb796f0246dcae9c5b0b6cfdbbd40c3245a610481c92047c968c9fc92c04b89cc41a0c15355a8f", 136 | "tlvs": { 137 | "unknown_tag_1": "68656c6c6f", 138 | "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" 139 | } 140 | } 141 | ] 142 | } 143 | } 144 | --------------------------------------------------------------------------------