Update project structure and build process

This commit is contained in:
Juan Pablo Civile 2025-05-13 11:10:08 -03:00
parent 124e9fa1bc
commit d9f3e925a4
277 changed files with 15321 additions and 930 deletions

View File

@ -11,7 +11,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-12, macos-11] os: [macos-13, macos-14, macos-15]
arch: [amd64, arm64] arch: [amd64, arm64]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -19,32 +19,32 @@ jobs:
out: recovery-tool-${{ matrix.os }}-${{ matrix.arch }} out: recovery-tool-${{ matrix.os }}-${{ matrix.arch }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Create output dir - name: Create output dir
run: | run: |
mkdir -p bin mkdir -p bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
with: with:
go-version: 1.18.1 go-version: '1.22.6'
- name: Build - name: Build
run: | run: |
CGO_ENABLED=1 \ CGO_ENABLED=1 \
GOOS=darwin \ GOOS=darwin \
GOARCH=${{ matrix.arch }} \ GOARCH=${{ matrix.arch }} \
go build -mod=vendor -a -trimpath -o bin/${{ env.out }} go build -mod=vendor -a -trimpath -o bin/${{ env.out }} ./recovery_tool
- name: Upload binary - name: Upload binary
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with: with:
name: ${{ env.out }} name: ${{ env.out }}
path: bin/${{ env.out }} path: bin/${{ env.out }}
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
@ -53,9 +53,15 @@ jobs:
- os: "linux" - os: "linux"
arch: "386" arch: "386"
out: "recovery-tool-linux32" out: "recovery-tool-linux32"
cc: "i686-linux-gnu-gcc-12"
- os: "linux" - os: "linux"
arch: "amd64" arch: "amd64"
out: "recovery-tool-linux64" out: "recovery-tool-linux64"
cc: "x86_64-linux-gnu-gcc-12"
- os: "linux"
arch: "arm64"
out: "recovery-tool-linuxaarch64"
cc: "aarch64-linux-gnu-gcc-12"
- os: "windows" - os: "windows"
arch: "386" arch: "386"
cc: "i686-w64-mingw32-gcc" cc: "i686-w64-mingw32-gcc"
@ -67,21 +73,21 @@ jobs:
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with: with:
buildkitd-flags: --debug buildkitd-flags: --debug
- name: Checkout - name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Create output dir - name: Create output dir
run: | run: |
mkdir -p bin mkdir -p bin
- name: Build - name: Build
uses: docker/build-push-action@c84f38281176d4c9cdb1626ffafcd6b3911b5d94 uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1
with: with:
file: Dockerfile file: recovery_tool/Dockerfile
context: . context: .
outputs: bin outputs: bin
cache-from: type=gha cache-from: type=gha
@ -93,21 +99,25 @@ jobs:
out=${{ matrix.target.out }} out=${{ matrix.target.out }}
- name: Upload binary - name: Upload binary
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with: with:
name: ${{ matrix.target.out }} name: ${{ matrix.target.out }}
path: bin/${{ matrix.target.out }} path: bin/${{ matrix.target.out }}
release: release:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: [build-mac, build] needs: [build-mac, build]
permissions:
contents: write
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
path: artifacts path: artifacts
pattern: recovery-tool-*
- name: Compute SHA256 checksums - name: Compute SHA256 checksums
run: | run: |
@ -117,7 +127,7 @@ jobs:
- name: Release - name: Release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
body_path: sha_sum_table body_path: sha_sum_table

View File

@ -7,7 +7,7 @@ To build the tool locally and run it, you must:
``` ```
git clone https://github.com/muun/recovery git clone https://github.com/muun/recovery
cd recovery cd recovery/recovery_tool
``` ```
3. Run the tool with: 3. Run the tool with:
@ -40,4 +40,4 @@ We use Docker for these builds to ensure they are reproducible.
For the 2.2 release, we had to disable reproducible builds for MacOS. The inclusion of C code for For the 2.2 release, we had to disable reproducible builds for MacOS. The inclusion of C code for
the musig implementation made building the tool inside a Linux container extremely difficult. We'll the musig implementation made building the tool inside a Linux container extremely difficult. We'll
be moving the process to GitHub actions soon, which can be easily audited and can build natively on be moving the process to GitHub actions soon, which can be easily audited and can build natively on
MacOS. MacOS.

4
go.work Normal file
View File

@ -0,0 +1,4 @@
go 1.22.1
use ./libwallet
use ./recovery_tool

View File

View File

View File

View File

View File

View File

321
libwallet/address_test.go Executable file
View File

@ -0,0 +1,321 @@
package libwallet
import (
"encoding/hex"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"google.golang.org/protobuf/proto"
)
const (
address = "2NDhvuRPCYXq4fB8SprminieZ2a1i3JFXyS"
amountURI = address + "?amount=1.2"
completeURI = amountURI + "&label=hola&message=mensaje%20con%20espacios"
uriWithSlashes = "bitcoin://" + amountURI
invalidAddress = "2NDhvuRPCYXq4fB8SprminieZ2a1i3JFXya"
randomText = "fooo"
bip70URL = "https://bitpay.com/i/KXCEAtJQssR9vG2BxdjFwx"
bip70NonRetroCompatAddress = bitcoinScheme + "?r=" + bip70URL
bip70RetroCompatAddress = bitcoinScheme + address + "?r=" + bip70URL
)
func TestGetPaymentURI(t *testing.T) {
const (
invoice = "lnbcrt1pwtpd4xpp55meuklpslk5jtxytyh7u2q490c2xhm68dm3a94486zntsg7ad4vsdqqcqzys763w70h39ze44ngzhdt2mag84wlkefqkphuy7ssg4la5gt9vcpmqts00fnapf8frs928mc5ujfutzyu8apkezhrfvydx82l40w0fckqqmerzjc"
invoiceHashHex = "a6f3cb7c30fda925988b25fdc502a57e146bef476ee3d2d6a7d0a6b823dd6d59"
invoiceDestinationHex = "028cfad4e092191a41f081bedfbe5a6e8f441603c78bf9001b8fb62ac0858f20edasd"
)
invoiceDestination, _ := hex.DecodeString(invoiceDestinationHex)
invoicePaymentHash := make([]byte, 32)
hex.Decode(invoicePaymentHash[:], []byte(invoiceHashHex))
type args struct {
address string
network Network
}
tests := []struct {
name string
args args
want *MuunPaymentURI
wantErr bool
}{
{
name: "validAddress",
args: args{
address: address,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Uri: bitcoinScheme + address,
},
},
{
name: "amountValidAddress",
args: args{
address: amountURI,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Amount: "1.2",
Uri: bitcoinScheme + amountURI,
},
},
{
name: "completeValidAddress",
args: args{
address: completeURI,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Amount: "1.2",
Label: "hola",
Message: "mensaje con espacios",
Uri: bitcoinScheme + completeURI,
},
},
{
name: "invalidAddress",
args: args{
address: invalidAddress,
network: *Regtest(),
},
wantErr: true,
},
{
name: "randomText",
args: args{
address: randomText,
network: *Regtest(),
},
wantErr: true,
},
{
name: "BIP70NonRetroCompatAddress",
args: args{
address: bip70NonRetroCompatAddress,
network: *Regtest(),
},
want: &MuunPaymentURI{
Uri: bip70NonRetroCompatAddress,
Bip70Url: bip70URL,
},
},
{
name: "BIP70RetroCompatAddress",
args: args{
address: bip70RetroCompatAddress,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Uri: bip70RetroCompatAddress,
Bip70Url: bip70URL,
},
},
{
name: "URL like address",
args: args{
address: uriWithSlashes,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Uri: uriWithSlashes,
Amount: "1.2",
},
},
{
name: "bad url",
args: args{
address: ":foo#%--",
network: *Regtest(),
},
wantErr: true,
},
{
name: "bad query",
args: args{
address: "bitcoin:123123?%&-=asd",
network: *Regtest(),
},
wantErr: true,
},
{
name: "network mismatch",
args: args{
address: amountURI,
network: *Mainnet(),
},
wantErr: true,
},
{
name: "BIP with lightning",
args: args{
address: "bitcoin:123123?lightning=" + invoice,
network: *network,
},
want: &MuunPaymentURI{Invoice: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
}},
},
{
name: "ALL CAPS",
args: args{
address: "BITCOIN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
network: *Mainnet(),
},
want: &MuunPaymentURI{
Address: strings.ToLower("BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2"),
Uri: "BITCOIN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
},
},
{
name: "MiXeD Case",
args: args{
address: "BiTcOiN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
network: *Mainnet(),
},
want: &MuunPaymentURI{
Address: strings.ToLower("BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2"),
Uri: "BiTcOiN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPaymentURI(tt.args.address, &tt.args.network)
if (err != nil) != tt.wantErr {
t.Errorf("GetPaymentURI() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil && got.Invoice != nil {
// expiry is relative to now, so ignore it
got.Invoice.Expiry = 0
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPaymentURI() = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_normalizeAddress(t *testing.T) {
type args struct {
rawAddress string
targetScheme string
}
tests := []struct {
name string
args args
want string
}{
{
name: "normalAddress",
args: args{
rawAddress: address,
targetScheme: bitcoinScheme,
},
want: bitcoinScheme + address,
},
{
name: "bitcoinAddress",
args: args{
rawAddress: bitcoinScheme + address,
targetScheme: bitcoinScheme,
},
want: bitcoinScheme + address,
},
{
name: "muunAddress",
args: args{
rawAddress: muunScheme + address,
targetScheme: bitcoinScheme,
},
want: bitcoinScheme + address,
},
{
name: "muun to lightning",
args: args{
rawAddress: muunScheme + address,
targetScheme: lightningScheme,
},
want: lightningScheme + address,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, _ := buildUriFromString(tt.args.rawAddress, tt.args.targetScheme); got != tt.want {
t.Errorf("buildUriFromString() = %v, want %v", got, tt.want)
}
})
}
}
func TestDoPaymentRequestCall(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/payment-request/", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") != "application/bitcoin-paymentrequest" {
t.Fatal("expected Accept header to be application/bitcoin-paymentrequest")
}
script, _ := hex.DecodeString("76a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac")
serializedPaymentDetails, _ := proto.Marshal(&PaymentDetails{
Network: "test",
Outputs: []*Output{
{
Script: script,
Amount: 2500,
},
},
Time: 100000,
Expires: 102000,
Memo: "Hello World",
PaymentUrl: "http://localhost:8000/pay",
MerchantData: []byte(""),
})
payReq, _ := proto.Marshal(&PaymentRequest{SerializedPaymentDetails: serializedPaymentDetails})
w.Write(payReq)
})
server := httptest.NewServer(mux)
defer server.Close()
url := server.URL + "/payment-request/"
paymentURI, err := DoPaymentRequestCall(url, Testnet())
if err != nil {
t.Fatal(err)
}
expected := &MuunPaymentURI{
Address: "mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f",
Message: "Hello World",
Amount: "0.000025",
Bip70Url: url,
CreationTime: "100000",
ExpiresTime: "102000",
}
if !reflect.DeepEqual(paymentURI, expected) {
t.Fatalf("decoded URI struct does not match expected, %+v != %+v", paymentURI, expected)
}
}

View File

@ -0,0 +1,30 @@
package addresses
import (
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/muun/libwallet/hdpath"
)
func parseKey(s string) *hdkeychain.ExtendedKey {
key, err := hdkeychain.NewKeyFromString(s)
if err != nil {
panic(err)
}
return key
}
func derive(key *hdkeychain.ExtendedKey, fromPath, toPath string) *hdkeychain.ExtendedKey {
indexes := hdpath.MustParse(toPath).IndexesFrom(hdpath.MustParse(fromPath))
for _, index := range indexes {
var err error
var modifier uint32
if index.Hardened {
modifier = hdkeychain.HardenedKeyStart
}
key, err = key.Child(index.Index | modifier)
if err != nil {
panic(err)
}
}
return key
}

57
libwallet/addresses/v2_test.go Executable file
View File

@ -0,0 +1,57 @@
package addresses
import (
"reflect"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
)
var network = &chaincfg.RegressionNetParams
func TestCreateAddressV2(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/0"
originAddress = "2NDeWrsJEwvxwVnvtWzPjhDC5B2LYkFuX2s"
encodedMuunKey = "tpubDBYMnFoxYLdMBZThTk4uARTe4kGPeEYWdKcaEzaUxt1cesetnxtTqmAxVkzDRou51emWytommyLWcF91SdF5KecA6Ja8oHK1FF7d5U2hMxX"
encodedUserKey = "tprv8dfM4H5fYJirMai5Er3LguicgUAyxmcSQbFub5ens16amX1e1HAFiW4SXnFVw9nu9FedFQqTPGTTjPEmgfvvXMKww3UcRpFbbC4DFjbCcTb"
basePath = "m/schema:1'/recovery:1'"
v2EncodedScript = "5221029fa5af7a34c142c1ce348b360abeb7de01df25b1d50129e58a67a6b846c9303b21025714f6b3670d4a38f5e2d6e8f239c9fc072543ce33dca54fcb4f4886a5cb87a652ae"
)
baseMuunKey := parseKey(encodedMuunKey)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(encodedUserKey)
userKey := derive(baseUserKey, basePath, addressPath)
type args struct {
userKey *hdkeychain.ExtendedKey
muunKey *hdkeychain.ExtendedKey
}
tests := []struct {
name string
args args
want *WalletAddress
wantErr bool
}{
{name: "gen address",
args: args{userKey: userKey, muunKey: muunKey},
want: &WalletAddress{address: originAddress, derivationPath: addressPath, version: V2}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateAddressV2(tt.args.userKey, tt.args.muunKey, addressPath, network)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAddressV2() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateAddressV2() = %v, want %v", got, tt.want)
}
})
}
}

54
libwallet/addresses/v3_test.go Executable file
View File

@ -0,0 +1,54 @@
package addresses
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
)
func TestCreateAddressV3(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/0"
v3Address = "2MswEXmCLaHQq6pUTtnUVF8wVArfYSqUec5"
basePK = "tpubDAN21T1DFREQQS4FvpUktKRBzXXsj5ddenAa5u198hLXvErFFR4Lj8bt8xMG3xnZr6u8mx1vrFW9RwCDXQwQuYRCLq1j9Nr2VJUrENzteQH"
baseCosigningPK = "tpubDAsVhzq6otpasovieofhiaY38bSFGyJaBGvrJjBv9whhSnftUXfMTMVrq4BbTXT5A9b78CqqbPuM2j1ZGWdiggd7JHUTZAHh8GXDTt4Pkj9"
basePath = "m/schema:1'/recovery:1'"
v3EncodedScript = "0020e1fbfbd395aff8b4087fee3e4488815ef659b559b3cd0d6800b5a591efd99f38"
)
baseMuunKey := parseKey(baseCosigningPK)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(basePK)
userKey := derive(baseUserKey, basePath, addressPath)
type args struct {
userKey *hdkeychain.ExtendedKey
muunKey *hdkeychain.ExtendedKey
}
tests := []struct {
name string
args args
want *WalletAddress
wantErr bool
}{
{name: "gen address",
args: args{userKey: userKey, muunKey: muunKey},
want: &WalletAddress{address: v3Address, derivationPath: addressPath, version: V3}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateAddressV3(tt.args.userKey, tt.args.muunKey, addressPath, network)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAddressV3() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateAddressV3() = %v, want %v", got, tt.want)
}
})
}
}

52
libwallet/addresses/v4_test.go Executable file
View File

@ -0,0 +1,52 @@
package addresses
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
)
func TestCreateAddressV4(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/2"
v4Address = "bcrt1qrs3vk4dzv70syck2qdz3g06tgckq4pftenuk5p77st9glnskpvtqe2tvvk"
basePK = "tpubDBf5wCeqg3KrLJiXaveDzD5JtFJ1ss9NVvFMx4RYS73SjwPEEawcAQ7V1B5DGM4gunWDeYNrnkc49sUaf7mS1wUKiJJQD6WEctExUQoLvrg"
baseCosigningPK = "tpubDB22PFkUaHoB7sgxh7exCivV5rAevVSzbB8WkFCCdbHq39r8xnYexiot4NGbi8PM6E1ySVeaHsoDeMYb6EMndpFrzVmuX8iQNExzwNpU61B"
basePath = "m/schema:1'/recovery:1'"
)
baseMuunKey := parseKey(baseCosigningPK)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(basePK)
userKey := derive(baseUserKey, basePath, addressPath)
type args struct {
userKey *hdkeychain.ExtendedKey
muunKey *hdkeychain.ExtendedKey
}
tests := []struct {
name string
args args
want *WalletAddress
wantErr bool
}{
{name: "gen bech32 address",
args: args{userKey: userKey, muunKey: muunKey},
want: &WalletAddress{address: v4Address, derivationPath: addressPath, version: V4}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateAddressV4(tt.args.userKey, tt.args.muunKey, addressPath, network)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAddressV4() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateAddressV4() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,34 @@
package addresses
import (
"reflect"
"testing"
)
func TestCreateAddressV5(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/17"
v5Address = "bcrt1pvqngr85tm8hmsv2hjyrejlpsy7u65f7vke8mmrxnyuj3aj3xsapqvh8yrf"
basePK = "tpubDBf5wCeqg3KrLJiXaveDzD5JtFJ1ss9NVvFMx4RYS73SjwPEEawcAQ7V1B5DGM4gunWDeYNrnkc49sUaf7mS1wUKiJJQD6WEctExUQoLvrg"
baseCosigningPK = "tpubDB22PFkUaHoB7sgxh7exCivV5rAevVSzbB8WkFCCdbHq39r8xnYexiot4NGbi8PM6E1ySVeaHsoDeMYb6EMndpFrzVmuX8iQNExzwNpU61B"
basePath = "m/schema:1'/recovery:1'"
)
baseMuunKey := parseKey(baseCosigningPK)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(basePK)
userKey := derive(baseUserKey, basePath, addressPath)
expectedAddr := &WalletAddress{address: v5Address, derivationPath: addressPath, version: V5}
actualAddr, err := CreateAddressV5(userKey, muunKey, addressPath, network)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actualAddr, expectedAddr) {
t.Errorf("Created v5 address %v, expected %v", actualAddr, expectedAddr)
}
}

View File

@ -0,0 +1,36 @@
package aescbc
import (
"bytes"
"crypto/rand"
"testing"
)
func TestEncryptionWithPkcs7Padding(t *testing.T) {
key := randomBytes(32)
iv := randomBytes(16)
plaintext := []byte("foobar")
ciphertext, err := EncryptPkcs7(key, iv, plaintext)
if err != nil {
t.Fatal(err)
}
decrypted, err := DecryptPkcs7(key, iv, ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Fatalf("expected decrypted text to match plaintext")
}
}
func randomBytes(count int) []byte {
buf := make([]byte, count)
_, err := rand.Read(buf)
if err != nil {
panic("couldn't read random bytes")
}
return buf
}

View File

View File

@ -0,0 +1,66 @@
package bech32m
import (
"testing"
)
// The following test vectors were taken from BIP-350.
// We only test for valid/invalid (and not decoded data), since only the checksum changed.
var validBech32m = []string{
"A1LQFN3A",
"a1lqfn3a",
"an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
"abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
"11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8",
"split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
"?1v759aa",
}
var invalidBech32m = []string{
"\x201xj0phk", // HRP character out of range
"\x7f1g6xzxy", // HRP character out of range
"\x801vctc34", // HRP character out of range
"an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4", // Overall max length exceeded
"qyrz8wqd2c9m", // No separator
"1qyrz8wqd2c9m", // Empty HRP
"16plkw9", // Empty HRP
"1p2gdwpf", // Empty HRP
"y1b0jsk6g", // Invalid data character
"lt1igcx5c0", // Invalid data character
"in1muywd", // Too short checksum
"mm1crxm3i", // Invalid character in checksum
"au1s5cgom", // Invalid character in checksum
"M1VUXWEZ", // checksum calculated with uppercase form of HRP
}
func TestDecodeValid(t *testing.T) {
for _, validBech := range validBech32m {
_, _, err := Decode(validBech)
if err != nil {
t.Fatalf("failed to decode valid bech32m %s: %v", validBech, err)
}
}
}
func TestDecodeInvalid(t *testing.T) {
for _, invalidBech := range invalidBech32m {
_, _, err := Decode(invalidBech)
if err == nil {
t.Fatalf("success decoding invalid string %s", invalidBech)
}
}
}
func TestNotCompat(t *testing.T) {
someBech32 := "bcrt1q77ayq0ldrwr3vg0rl0ss8u0ne0hajllz4h7yrqm8ldyy2v0860vs9xzmr4"
_, _, err := Decode(someBech32)
if err == nil {
t.Fatalf("success decoding bech32 with bech32m %s (expected checksum failure)", someBech32)
}
}

View File

@ -0,0 +1,139 @@
package txscriptw
import (
"bytes"
"encoding/binary"
"encoding/hex"
"testing"
_ "unsafe"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
)
// These test cases were taken from rust-bitcoin, which in turn took them from Bitcoin Core:
var sigHashTestCases = []sigHashTestCase{
{
tx: "020000000164eb050a5e3da0c2a65e4786f26d753b7bc69691fabccafb11f7acef36641f1846010000003101b2b404392a22000000000017a9147f2bde86fe78bf68a0544a4f290e12f0b7e0a08c87580200000000000017a91425d11723074ecfb96a0a83c3956bfaf362ae0c908758020000000000001600147e20f938993641de67bb0cdd71682aa34c4d29ad5802000000000000160014c64984dc8761acfa99418bd6bedc79b9287d652d72000000",
prevOuts: "01365724000000000023542156b39dab4f8f3508e0432cfb41fab110170acaa2d4c42539cb90a4dc7c093bc500",
index: 0,
hashType: txscript.SigHashOld,
// expectSigHash: "33ca0ebfb4a945eeee9569fc0f5040221275f88690b7f8592ada88ce3bdf6703",
expectError: true,
},
{
tx: "0200000002fff49be59befe7566050737910f6ccdc5e749c7f8860ddc140386463d88c5ad0f3000000002cf68eb4a3d67f9d4c079249f7e4f27b8854815cb1ed13842d4fbf395f9e217fd605ee24090100000065235d9203f458520000000000160014b6d48333bb13b4c644e57c43a9a26df3a44b785e58020000000000001976a914eea9461a9e1e3f765d3af3e726162e0229fe3eb688ac58020000000000001976a9143a8869c9f2b5ea1d4ff3aeeb6a8fb2fffb1ad5fe88ac0ad7125c",
prevOuts: "02591f220000000000225120f25ad35583ea31998d968871d7de1abd2a52f6fe4178b54ea158274806ff4ece48fb310000000000225120f25ad35583ea31998d968871d7de1abd2a52f6fe4178b54ea158274806ff4ece",
index: 1,
hashType: txscript.SigHashAll,
expectSigHash: "626ab955d58c9a8a600a0c580549d06dc7da4e802eb2a531f62a588e430967a8",
expectError: false,
},
{
tx: "0200000001350005f65aa830ced2079df348e2d8c2bdb4f10e2dde6a161d8a07b40d1ad87dae000000001611d0d603d9dc0e000000000017a914459b6d7d6bbb4d8837b4bf7e9a4556f952da2f5c8758020000000000001976a9141dd70e1299ffc2d5b51f6f87de9dfe9398c33cbb88ac58020000000000001976a9141dd70e1299ffc2d5b51f6f87de9dfe9398c33cbb88aca71c1f4f",
prevOuts: "01c4811000000000002251201bf9297d0a2968ae6693aadd0fa514717afefd218087a239afb7418e2d22e65c",
index: 0,
hashType: txscript.SigHashAll | txscript.SigHashAnyOneCanPay,
// expectSigHash: "dfa9437f9c9a1d1f9af271f79f2f5482f287cdb0d2e03fa92c8a9b216cc6061c",
expectError: true,
},
{
tx: "020000000185bed1a6da2bffbd60ec681a1bfb71c5111d6395b99b3f8b2bf90167111bcb18f5010000007c83ace802ded24a00000000001600142c4698f9f7a773866879755aa78c516fb332af8e5802000000000000160014d38639dfbac4259323b98a472405db0c461b31fa61073747",
prevOuts: "0144c84d0000000000225120e3f2107989c88e67296ab2faca930efa2e3a5bd3ff0904835a11c9e807458621",
index: 0,
hashType: txscript.SigHashNone,
// expectSigHash: "3129de36a5d05fff97ffca31eb75fcccbbbc27b3147a7a36a9e4b45d8b625067",
expectError: true,
},
{
tx: "eb93dbb901028c8515589dac980b6e7f8e4088b77ed866ca0d6d210a7218b6fd0f6b22dd6d7300000000eb4740a9047efc0e0000000000160014913da2128d8fcf292b3691db0e187414aa1783825802000000000000160014913da2128d8fcf292b3691db0e187414aa178382580200000000000017a9143dd27f01c6f7ef9bb9159937b17f17065ed01a0c875802000000000000160014d7630e19df70ada9905ede1722b800c0005f246641000000",
prevOuts: "013fed110000000000225120eb536ae8c33580290630fc495046e998086a64f8f33b93b07967d9029b265c55",
index: 0,
hashType: txscript.SigHashNone | txscript.SigHashAnyOneCanPay,
// expectSigHash: "2441e8b0e063a2083ee790f14f2045022f07258ddde5ee01de543c9e789d80ae",
expectError: true,
},
{
tx: "02000000017836b409a5fed32211407e44b971591f2032053f14701fb5b3a30c0ff382f2cc9c0100000061ac55f60288fb5600000000001976a9144ea02f6f182b082fb6ce47e36bbde390b6a41b5088ac58020000000000001976a9144ea02f6f182b082fb6ce47e36bbde390b6a41b5088ace4000000",
prevOuts: "01efa558000000000022512007071ea3dc7e331b0687d0193d1e6d6ed10e645ef36f10ef8831d5e522ac9e80",
index: 0,
hashType: txscript.SigHashSingle,
// expectSigHash: "30239345177cadd0e3ea413d49803580abb6cb27971b481b7788a78d35117a88",
expectError: true,
},
{
tx: "0100000001aa6deae89d5e0aaca58714fc76ef6f3c8284224888089232d4e663843ed3ab3eae010000008b6657a60450cb4c0000000000160014a3d42b5413ef0c0701c4702f3cd7d4df222c147058020000000000001976a91430b4ed8723a4ee8992aa2c8814cfe5c3ad0ab9d988ac5802000000000000160014365b1166a6ed0a5e8e9dff17a6d00bbb43454bc758020000000000001976a914bc98c51a84fe7fad5dc380eb8b39586eff47241688ac4f313247",
prevOuts: "0107af4e00000000002251202c36d243dfc06cb56a248e62df27ecba7417307511a81ae61aa41c597a929c69",
index: 0,
hashType: txscript.SigHashSingle | txscript.SigHashAnyOneCanPay,
// expectSigHash: "bf9c83f26c6dd16449e4921f813f551c4218e86f2ec906ca8611175b41b566df",
expectError: true,
},
}
func TestTaprootSigHash(t *testing.T) {
for i, testCase := range sigHashTestCases {
tx := testCase.ParseTx()
prevOuts := testCase.ParsePrevOuts()
sigHashes := NewTaprootSigHashes(tx, prevOuts)
sigHash, err := CalcTaprootSigHash(tx, sigHashes, testCase.index, testCase.hashType)
if (err != nil) != testCase.expectError {
t.Fatalf("case %d: expect error %v, actual error: %v", i, testCase.expectError, err)
}
if !bytes.Equal(sigHash, testCase.ParseExpectedSigHash()) {
t.Fatalf("case %d: sigHash does not match expected value", i)
}
}
}
type sigHashTestCase struct {
tx string
prevOuts string
index int
hashType txscript.SigHashType
expectSigHash string
expectError bool
}
func (c *sigHashTestCase) ParseTx() *wire.MsgTx {
b, _ := hex.DecodeString(c.tx)
r := bytes.NewReader(b)
tx := wire.NewMsgTx(0)
tx.BtcDecode(r, 0, wire.WitnessEncoding)
return tx
}
func (c *sigHashTestCase) ParsePrevOuts() []*wire.TxOut {
b, _ := hex.DecodeString(c.prevOuts)
r := bytes.NewReader(b)
prevOutCount, _ := wire.ReadVarInt(r, 0)
prevOuts := make([]*wire.TxOut, prevOutCount)
for i := 0; i < int(prevOutCount); i++ {
valueLe := make([]byte, 8)
r.Read(valueLe[:])
value := binary.LittleEndian.Uint64(valueLe)
pkScriptSize, _ := wire.ReadVarInt(r, 0)
pkScript := make([]byte, pkScriptSize)
r.Read(pkScript)
prevOuts[i] = &wire.TxOut{
Value: int64(value),
PkScript: pkScript,
}
}
return prevOuts
}
func (c *sigHashTestCase) ParseExpectedSigHash() []byte {
b, _ := hex.DecodeString(c.expectSigHash)
return b
}

160
libwallet/challenge_keys_test.go Executable file
View File

@ -0,0 +1,160 @@
package libwallet
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil/base58"
)
func TestNewChallengePrivateKey(t *testing.T) {
type args struct {
input []byte
salt []byte
}
tests := []struct {
name string
args args
want *ChallengePrivateKey
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewChallengePrivateKey(tt.args.input, tt.args.salt); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewChallengePrivateKey() = %v, want %v", got, tt.want)
}
})
}
}
func TestChallengeKeySignSha(t *testing.T) {
input := randomBytes(32)
salt := randomBytes(32)
challengePrivKey := NewChallengePrivateKey(input, salt)
payload := []byte("foobar")
_, err := challengePrivKey.SignSha(payload)
if err != nil {
t.Fatal(err)
}
// TODO(federicobond): assert that signature verifies
}
func TestChallengeKeyCrypto(t *testing.T) {
const birthday = 376
network := Regtest()
salt := randomBytes(8)
privKey, _ := NewHDPrivateKey(randomBytes(32), network)
challengePrivKey := NewChallengePrivateKey([]byte("a very good password"), salt)
encryptedKey, err := challengePrivKey.PubKey().EncryptKey(privKey, salt, birthday)
if err != nil {
t.Fatal(err)
}
decryptedKey, err := challengePrivKey.DecryptRawKey(encryptedKey, network)
if err != nil {
t.Fatal(err)
}
if privKey.String() != decryptedKey.Key.String() {
t.Fatalf("keys dont match: orig %v vs decrypted %v", privKey.String(), decryptedKey.Key.String())
}
if birthday != decryptedKey.Birthday {
t.Fatalf("birthdays dont match: expected %v got %v", birthday, decryptedKey.Birthday)
}
}
func TestChallengeKeyCryptoV2(t *testing.T) {
const (
encodedKey = "tprv8ZgxMBicQKsPcxg1GFGZgL5zALjPwijrYNUqTi2s9JsVqDLzbpX55U9JH2PKAQKExtpdTyboZmV2ytaqr9pAHuxE1hX8k9bQgZAjq25E6P7"
encryptedKey = "4LbSKwcepbbx4dPetoxvTWszb6mLyJHFhumzmdPRVprbn8XZBvFa6Ffarm6R3WGKutFzdxxJgQDdSHuYdjhDp1EZfSNbj12gXMND1AgmNijSxEua3LwVURU3nzWsvV5b1AsWEjJca24CaFY6T3C"
password = "a very good password"
saltLength = 8
birthday = 376
)
extractSalt := func(rawKey string) []byte {
bytes := base58.Decode(rawKey)
return bytes[len(bytes)-saltLength:]
}
challengeKey := NewChallengePrivateKey([]byte(password), extractSalt(encryptedKey))
decryptedKey, err := challengeKey.DecryptRawKey(encryptedKey, Regtest())
if err != nil {
t.Fatal(err)
}
if decryptedKey.Birthday != birthday {
t.Fatalf("decrypted birthday %v differs from expected %v", decryptedKey.Birthday, birthday)
}
if decryptedKey.Key.String() != encodedKey {
t.Fatalf("key doesnt match\ngot %v\nexpected %v\n", decryptedKey.Key.String(), encodedKey)
}
_, err = challengeKey.PubKey().EncryptKey(decryptedKey.Key, extractSalt(encryptedKey), birthday)
if err != nil {
t.Fatal(err)
}
}
func TestDecodeKeyWithOrWithoutSalt(t *testing.T) {
const (
// The same encoded key, with one version missing the salt field:
saltedKey = "4LbSKwcepbbx4dPetoxvTWszb6mLyJHFhumzmdPRVprbn8XZBvFa6Ffarm6R3WGKutFzdxxJgQDdSHuYdjhDp1EZfSNbj12gXMND1AgmNijSxEua3LwVURU3nzWsvV5b1AsWEjJca24CaFY6T3C"
unsaltedKey = "5XEEts6mc9WV34krDWsqmpLcPCw2JkK8qJu3gFdZpP8ngkERuQEsaDvYrGkhXUpM6jQRtimTYm4XnBPujpo3MsdYBedsNVxvT3WC6uCCFuzNUZCoydVY39yJXbxva7naDxH5iTra"
)
expected := &EncryptedPrivateKeyInfo{
Version: 2,
Birthday: 376,
CipherText: "f6af1ecd17052a81b75902c1712567cf1c650329875feb7e24af3e27235f384054ea549025e99dc2659f95bb6447cf861aa2ec0407ea74baf5a9d6a885ae184b",
EphPublicKey: "020a8d322dda8ff685d80b16681d4e87c109664cdc246a9d3625adfe0de203e71e",
Salt: "e3305526d0cd675f",
}
// Verify the salted version:
actual, err := DecodeEncryptedPrivateKey(saltedKey)
if err != nil {
t.Fatal(err)
}
assertDecodedKeysEqual(t, actual, expected)
// Verify the unsalted version:
actual, err = DecodeEncryptedPrivateKey(unsaltedKey)
if err != nil {
t.Fatal(err)
}
expected.Salt = "0000000000000000" // unsalted key should decode with zeroed field
assertDecodedKeysEqual(t, actual, expected)
}
func assertDecodedKeysEqual(t *testing.T, actual, expected *EncryptedPrivateKeyInfo) {
if actual.Version != expected.Version {
t.Fatalf("version %v expected %v", actual.Version, expected.Version)
}
if actual.Birthday != expected.Birthday {
t.Fatalf("birthday %v, expected %v", actual.Birthday, expected.Birthday)
}
if actual.CipherText != expected.CipherText {
t.Fatalf("cipherText %x expected %x", actual.CipherText, expected.CipherText)
}
if actual.EphPublicKey != expected.EphPublicKey {
t.Fatalf("ephPublicKey %x expected %x", actual.EphPublicKey, expected.EphPublicKey)
}
if actual.Salt != expected.Salt {
t.Fatalf("salt %x expected %x", actual.Salt, expected.Salt)
}
}

15
libwallet/emergency_kit_test.go Executable file
View File

@ -0,0 +1,15 @@
package libwallet
import (
"testing"
)
func TestGenerateEmergencyKitHTML(t *testing.T) {
_, err := GenerateEmergencyKitHTML(&EKInput{
FirstEncryptedKey: "5zZPk5V7oJcXtQyFgdxrP6D5A4Xck2XMC2FG7rrxeDu89K4YuuMoAdZ2MeAGqMU28aR4Lsa5HRxB5mDXmajmYgLaZi6CivXeBRSzazJb8T4VizArrDA8NDH8TipEsHnwCyCd6eiNQYbedyRPw4B",
SecondEncryptedKey: "4RLVcRNPSdCcV5pdd6FsNuUzhGwp3h7piXhpDkHbF31PrHmNqsyMd9vRveXsBVsWPLXHvMkvhzk68yGw4Wwcxfz55yPeN5Jogqpmn7BQc7P1SNymwtgbatLiJfwqFLm1iqoLPobCmK6wH7MY9N7",
}, "es")
if err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,34 @@
package emergencykit
import "testing"
func TestChecksum(t *testing.T) {
// These descriptors are in https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md and
// their expected checksums obtained via the `getdescriptorinfo` RPC endpoint. Note that, to
// reproduce these results, you need a mainnet Bitcoin node (HD key parsing fails otherwise).
testChecksum(t, "gn28ywm7", "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")
testChecksum(t, "8fhd9pwu", "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)")
testChecksum(t, "8zl0zxma", "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)")
testChecksum(t, "qkrrc7je", "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))")
testChecksum(t, "lq9sf04s", "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")
testChecksum(t, "2wtr0ej5", "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))")
testChecksum(t, "hzhjw406", "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)")
testChecksum(t, "y9zthqta", "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))")
testChecksum(t, "en3tu306", "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))")
testChecksum(t, "ks05yr6p", "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))")
testChecksum(t, "qwx6n9lh", "sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))")
testChecksum(t, "axav5m0j", "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)")
testChecksum(t, "kczqajcv", "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/2)")
testChecksum(t, "ml40v0wf", "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")
testChecksum(t, "t2zpj2eu", "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))")
testChecksum(t, "v66cvalc", "wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))")
}
func testChecksum(t *testing.T, expectedChecksum string, descriptor string) {
actualChecksum := calculateChecksum(descriptor)
if actualChecksum != expectedChecksum {
t.Errorf("Descriptor %s checksum was %s expecting %s", descriptor, actualChecksum, expectedChecksum)
}
}

View File

@ -0,0 +1,105 @@
package emergencykit
import (
"strings"
"testing"
)
func TestGenerateHTML(t *testing.T) {
out, err := GenerateHTML(&Input{
FirstEncryptedKey: "MyFirstEncryptedKey",
SecondEncryptedKey: "MySecondEncryptedKey",
}, "en")
if err != nil {
t.Fatal(err)
}
if len(out.VerificationCode) != 6 {
t.Fatal("expected verification code to have length 6")
}
if !strings.Contains(out.HTML, out.VerificationCode) {
t.Fatal("expected output html to contain verification code")
}
if !strings.Contains(out.HTML, "MyFirstEncryptedKey") {
t.Fatal("expected output html to contain first encrypted key")
}
if !strings.Contains(out.HTML, "MySecondEncryptedKey") {
t.Fatal("expected output html to contain second encrypted key")
}
if !strings.Contains(out.HTML, `<ul class="descriptors">`) {
t.Fatal("expected output html to contain output descriptors")
}
if !strings.Contains(out.HTML, `<span class="f">wsh</span>`) {
t.Fatal("expected output html to contain output descriptor scripts")
}
}
func TestGenerateHTMLWithFingerprints(t *testing.T) {
data := &Input{
FirstEncryptedKey: "MyFirstEncryptedKey",
FirstFingerprint: "abababab",
SecondEncryptedKey: "MySecondEncryptedKey",
SecondFingerprint: "cdcdcdcd",
}
out, err := GenerateHTML(data, "en")
if err != nil {
t.Fatal(err)
}
if len(out.VerificationCode) != 6 {
t.Fatal("expected verification code to have length 6")
}
if !strings.Contains(out.HTML, out.VerificationCode) {
t.Fatal("expected output html to contain verification code")
}
if !strings.Contains(out.HTML, "MyFirstEncryptedKey") {
t.Fatal("expected output html to contain first encrypted key")
}
if !strings.Contains(out.HTML, "MySecondEncryptedKey") {
t.Fatal("expected output html to contain second encrypted key")
}
if !strings.Contains(out.HTML, `<ul class="descriptors">`) {
t.Fatal("expected output html to contain output descriptors")
}
if !strings.Contains(out.HTML, `<span class="f">wsh</span>`) {
t.Fatal("expected output html to contain output descriptor scripts")
}
if !strings.Contains(out.HTML, `<span class="f">musig</span>`) {
t.Fatal("expected output html to contain musig output descriptor scripts")
}
if !strings.Contains(out.HTML, data.FirstFingerprint) {
t.Fatal("expected output html to contain FirstFingerprint")
}
if !strings.Contains(out.HTML, data.SecondFingerprint) {
t.Fatal("expected output html to contain SecondFingerprint")
}
}
func TestGenerateDeterministicCode(t *testing.T) {
// Create a base Input, without version, which we'll set for each case below:
input := &Input{
FirstEncryptedKey: "foo",
SecondEncryptedKey: "bar",
}
// List our cases for each version:
versionExpectedCodes := []struct {
version int
expectedCode string
}{
{1, "190981"},
{2, "257250"},
{3, "494327"},
}
// Do the thing:
for _, testCase := range versionExpectedCodes {
input.Version = testCase.version
code := generateDeterministicCode(input)
if code != testCase.expectedCode {
t.Fatalf("expected code from %+v to be %s, not %s", input, testCase.expectedCode, code)
}
}
}

View File

@ -0,0 +1,121 @@
package emergencykit
import (
"encoding/hex"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
var someMetadata = Metadata{
Version: 1,
BirthdayBlock: 12345,
EncryptedKeys: []*MetadataKey{
&MetadataKey{
DhPubKey: "0338c52ecbb886ab45de31120c76888da73437e3d6e81510f56d3746399f0fef52",
EncryptedPrivKey: "d0a801c1923663295892e9a9a0bfc770abcb00c20e7cef28e2d743c96b441e677c875e8d6495afb8362aba886ae9ee346c62e82758f5b5ba9a70f61957529255",
Salt: "d579c14c61365bc0",
},
},
OutputDescriptors: []string{
"sh(wsh(multi(2, 89a1749c/1'/1'/0/*, 77e21d45/1'/1'/0/*)))#0wp4hp36",
},
}
func TestReadWriteMetadata(t *testing.T) {
// Create a temporary directory and pick some suitable paths for our input/output files:
tmpDir := createTmpDir(t)
srcFile := filepath.Join(tmpDir, "src.pdf")
dstFile := filepath.Join(tmpDir, "src.pdf")
defer os.RemoveAll(tmpDir)
// Save the sample PDF (included at the end of this file, for readability):
createPdfFile(t, srcFile)
// Write metadata:
mw := MetadataWriter{
SrcFile: srcFile,
DstFile: dstFile,
}
mw.WriteMetadata(&someMetadata)
// Read metadata:
mr := MetadataReader{
SrcFile: dstFile,
}
metadata, err := mr.ReadMetadata()
if err != nil {
t.Fatalf("Failed to read metadata from %s: %v", dstFile, err)
}
// Verify that we got the original metadata back:
if !reflect.DeepEqual(&someMetadata, metadata) {
t.Fatalf("Metadata objects don't match: %v (%v vs %v)", err, someMetadata, metadata)
}
}
func createTmpDir(t *testing.T) string {
tmpDir, err := ioutil.TempDir("", "pdf")
if err != nil {
t.Fatalf("Failed to create temporary directory %s: %v", tmpDir, err)
}
return tmpDir
}
func createPdfFile(t *testing.T, path string) {
content, err := hex.DecodeString(strings.Join(strings.Fields(verySmallPdf), ""))
if err != nil {
t.Fatalf("Failed to hex-decode the sample PDF data: %v", err)
}
err = ioutil.WriteFile(path, content, os.FileMode(0600))
if err != nil {
t.Fatalf("Failed to write PDF to %s: %v", path, err)
}
}
// A very small valid PDF obtained by printing `<html></html>` with Chromium:
const verySmallPdf = `
255044462d312e340a25d3ebe9e10a312030206f626a0a3c3c2f43726561
746f7220284d6f7a696c6c612f352e30205c284d6163696e746f73683b20
496e74656c204d6163204f5320582031305f31345f365c29204170706c65
5765624b69742f3533372e3336205c284b48544d4c2c206c696b65204765
636b6f5c29204368726f6d652f38372e302e343238302e38382053616661
72692f3533372e3336290a2f50726f64756365722028536b69612f504446
206d3837290a2f4372656174696f6e446174652028443a32303230313231
313136333033332b303027303027290a2f4d6f64446174652028443a3230
3230313231313136333033332b303027303027293e3e0a656e646f626a0a
332030206f626a0a3c3c2f636120310a2f424d202f4e6f726d616c3e3e0a
656e646f626a0a342030206f626a0a3c3c2f46696c746572202f466c6174
654465636f64650a2f4c656e6774682039353e3e2073747265616d0a789c
d33332b60403050320d4d543e29a5b1a2924e772157281648c4c4d0d148c
8d0d0c148a52b9c2b514f280e2c67a8646a6607d08165083a1020806b92b
401845e95cfaeec60ae9c560732c0ccd140c0d4ccd40c6a471050221009d
2a19fb0a656e6473747265616d0a656e646f626a0a322030206f626a0a3c
3c2f54797065202f506167650a2f5265736f7572636573203c3c2f50726f
63536574205b2f504446202f54657874202f496d61676542202f496d6167
6543202f496d616765495d0a2f457874475374617465203c3c2f47332033
203020523e3e3e3e0a2f4d65646961426f78205b30203020363132203739
325d0a2f436f6e74656e74732034203020520a2f53747275637450617265
6e747320300a2f506172656e742035203020523e3e0a656e646f626a0a35
2030206f626a0a3c3c2f54797065202f50616765730a2f436f756e742031
0a2f4b696473205b32203020525d3e3e0a656e646f626a0a362030206f62
6a0a3c3c2f54797065202f436174616c6f670a2f50616765732035203020
523e3e0a656e646f626a0a787265660a3020370a30303030303030303030
2036353533352066200a30303030303030303135203030303030206e200a
30303030303030343731203030303030206e200a30303030303030323730
203030303030206e200a30303030303030333037203030303030206e200a
30303030303030363539203030303030206e200a30303030303030373134
203030303030206e200a747261696c65720a3c3c2f53697a6520370a2f52
6f6f742036203020520a2f496e666f2031203020523e3e0a737461727478
7265660a3736310a2525454f46
`

46
libwallet/encodings_test.go Executable file
View File

@ -0,0 +1,46 @@
package libwallet
import (
"encoding/hex"
"math/big"
"reflect"
"testing"
)
func hexToBigInt(value string) *big.Int {
result := &big.Int{}
bytes, _ := hex.DecodeString(value)
result.SetBytes(bytes)
return result
}
func hexToBytes(value string) []byte {
bytes, _ := hex.DecodeString(value)
return bytes
}
func Test_paddedSerializeBigInt(t *testing.T) {
type args struct {
size uint
x *big.Int
}
tests := []struct {
name string
args args
want []byte
}{
{
name: "31 bytes key",
args: args{size: 32, x: hexToBigInt("0e815b7892396a2e28e09c0d50082931eedd7fec16ef2e06724fe48f877ea6")},
want: hexToBytes("000e815b7892396a2e28e09c0d50082931eedd7fec16ef2e06724fe48f877ea6"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := paddedSerializeBigInt(tt.args.size, tt.args.x); !reflect.DeepEqual(got, tt.want) {
t.Errorf("paddedSerializeBigInt() = %v, want %v", got, tt.want)
}
})
}
}

View File

252
libwallet/encrypt_test.go Executable file
View File

@ -0,0 +1,252 @@
package libwallet
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil/base58"
)
func TestPublicKeyEncryption(t *testing.T) {
network := Mainnet()
senderKey, _ := NewHDPrivateKey(randomBytes(32), network)
receiverKey, _ := NewHDPrivateKey(randomBytes(32), network)
payload := randomBytes(178)
ciphertext, err := senderKey.EncrypterTo(receiverKey.PublicKey()).Encrypt(payload)
if err != nil {
t.Fatal(err)
}
ecKey, _ := senderKey.PublicKey().key.ECPubKey()
publicKey := &PublicKey{ecKey}
plaintext, err := receiverKey.DecrypterFrom(publicKey).Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, payload) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(plaintext),
hex.EncodeToString(payload))
}
// If we don't know who the sender was
plaintext, err = receiverKey.DecrypterFrom(nil).Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, payload) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(plaintext),
hex.EncodeToString(payload))
}
badKey, _ := NewHDPrivateKey(randomBytes(32), network)
badEcKey, _ := badKey.PublicKey().key.ECPubKey()
badPubKey := &PublicKey{badEcKey}
_, err = receiverKey.DecrypterFrom(badPubKey).Decrypt(ciphertext)
if err == nil {
t.Fatal("Expected decryption from bad sender key to fail")
}
derivedSenderKey, _ := senderKey.DerivedAt(10, false)
ciphertext, err = derivedSenderKey.Encrypter().Encrypt(payload)
if err != nil {
t.Fatal(err)
}
plaintext, err = senderKey.Decrypter().Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, payload) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(plaintext),
hex.EncodeToString(payload))
}
}
func TestPublicKeyEncryptionV1(t *testing.T) {
const (
priv = "xprv9s21ZrQH143K2DAjx7FiAo2GQAQ5g7GrPYkTB2RaCd2Ei5ZH7f9cbREHiZTCc1FPn9HKuviUHk8sf5cW3dhYjz6W6XPjXNHu5mLpT5oRH1j"
ciphertext = "AMWm2L3YjA7myBTQQgiZi9F5g1NzaaupkPq1y7csUkf7WLXwnPYjkmy5KjVkyTKjaSXPwjx2zmX9Augzwwh89AsWYTv7KfJTXTj3Lx2mNZgmxJ7eezaJyRHv4koQaEmRykSoVE4esjWK779Sac28kCstkqDMPDYeNud5H4ApetF4BvhvPJyMaVn4RHYSAGzBzMcBV7WxYoRveKHqU9LbAfhCndPtRSVZyTVXY8iE3EvQJFeZVyYdovPK67aHsXWRdi8QCinMQSG21TMmhs7GQAh6iB26X2ABcVFJRGeEKE2coAsfuAHzcAMZ3CdzGgVAm7rrQw13W3XpxwwjWVatH9Jm9H4TrnnnLxRCsBoSKDvA1hmH8a2UG9iMxkhsBVMPzNRMy4Bg4MHk8WyRo3bwCLSVJUFFEciQ3mUneHprezzbVZio"
plaintextHex = "ca4dabb05a47d3ab306c1fad895d97b06dc30564191e610f9b254b1a1d0a536b6eca2b83d0d17d67aaad2a958fe6a6557ad5b26f44e12e7662f47a4e4fd6f482b68a83cd140ad4ded43b90a2c2cf349af84d828b1f961901616b4c4cb01f761bd277ad0d3d90506065aef76b930a962fcb90f2f009898c0d55cd07b5e01c355a9067937185fa9237d03e5ed4243e1bf0f8a959c72a83cbb1729b679cbd660052dd2dd3096b0f19e9275ac459b94d02a95642"
)
privKey, _ := NewHDPrivateKeyFromString(priv, "m", Mainnet())
plaintext, _ := hex.DecodeString(plaintextHex)
decrypted, err := privKey.Decrypter().Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(decrypted),
hex.EncodeToString(plaintext))
}
_, err = privKey.Encrypter().Encrypt(decrypted)
if err != nil {
t.Fatal(err)
}
// Since we use a random key every time, comparing cipher texts makes no sense
}
func TestPublicKeyDecryptV1(t *testing.T) {
const (
privHex = "xprv9s21ZrQH143K36uECEJcmTnxSXfHjT9jdb7FpMoUJpENDxeRgpscDF3g2w4ySH6G9uVsGKK7e6WgGp7Vc9VVnwC2oWdrr7a3taWiKW8jKnD"
path = "m"
pathLen = 1
)
payload := []byte("Asado Viernes")
privKey, _ := NewHDPrivateKeyFromString(privHex, path, Mainnet())
encrypted, _ := privKey.Encrypter().Encrypt(payload)
decrypted, err := privKey.Decrypter().Decrypt(encrypted)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(payload, decrypted) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
string(decrypted),
string(payload))
}
alterAndCheck := func(msg string, alter func(data []byte)) {
t.Run(msg, func(t *testing.T) {
encryptedData := base58.Decode(encrypted)
alter(encryptedData)
_, err = privKey.Decrypter().Decrypt(base58.Encode(encryptedData))
if err == nil {
t.Fatalf("Got nil error for altered payload: %v", msg)
}
t.Logf("Got error for altered payload %v: %v", msg, err)
})
}
alterAndCheck("big nonce size", func(data []byte) {
// Override the nonce size
data[1+serializedPublicKeyLength+2+pathLen] = 255
})
alterAndCheck("bigger nonce size", func(data []byte) {
// Override the nonce size
data[1+serializedPublicKeyLength+2+pathLen+1] = 14
})
alterAndCheck("smaller nonce size", func(data []byte) {
// Override the nonce size
data[1+serializedPublicKeyLength+2+pathLen+1] = 1
})
alterAndCheck("big derivation path len", func(data []byte) {
// Override derivation path length
data[1+serializedPublicKeyLength] = 255
})
alterAndCheck("bigger derivation path len", func(data []byte) {
// Override derivation path length
data[1+serializedPublicKeyLength+1] = 4
})
alterAndCheck("smaller derivation path len", func(data []byte) {
// Override derivation path length
data[1+serializedPublicKeyLength+1] = 0
})
alterAndCheck("nonce", func(data []byte) {
// Invert last byte of the nonce
data[1+serializedPublicKeyLength+2+pathLen+2+11] =
^data[1+serializedPublicKeyLength+2+pathLen+2+11]
})
alterAndCheck("tamper ciphertext", func(data []byte) {
// Invert last byte of the ciphertext
data[len(data)-1] = ^data[len(data)-1]
})
t.Run("tamperCiphertextWithAEAD", func(t *testing.T) {
data := base58.Decode(encrypted)
additionalData := data[0 : 1+serializedPublicKeyLength+2+pathLen+2]
nonce := data[len(data)-12:]
encryptionKey, _ := privKey.key.ECPrivKey()
secret, _ := recoverSharedEncryptionSecretForAES(encryptionKey, data[1:serializedPublicKeyLength+1])
block, _ := aes.NewCipher(secret)
gcm, _ := cipher.NewGCM(block)
fakeHdPrivKey, _ := NewHDPrivateKey(randomBytes(32), Mainnet())
fakePayload := []byte(strings.ToLower(string(payload)))
fakePrivKey, _ := fakeHdPrivKey.key.ECPrivKey()
hash := sha256.Sum256(fakePayload)
fakeSig, _ := btcec.SignCompact(btcec.S256(), fakePrivKey, hash[:], false)
plaintext := bytes.NewBuffer(nil)
addVariableBytes(plaintext, fakeSig)
plaintext.Write(fakePayload)
ciphertext := gcm.Seal(nil, nonce, plaintext.Bytes(), additionalData)
offset := len(additionalData)
for _, b := range ciphertext {
data[offset] = b
offset++
}
for _, b := range nonce {
data[offset] = b
offset++
}
_, err = privKey.Decrypter().Decrypt(base58.Encode(data))
if err == nil {
t.Errorf("Got nil error for altered payload: tamper chiphertex recalculating AEAD")
}
t.Logf("Got error for altered payload tamper chiphertex recalculating AEAD: %v", err)
})
}
func TestEncDecOps(t *testing.T) {
const (
privHex = "xprv9s21ZrQH143K36uECEJcmTnxSXfHjT9jdb7FpMoUJpENDxeRgpscDF3g2w4ySH6G9uVsGKK7e6WgGp7Vc9VVnwC2oWdrr7a3taWiKW8jKnD"
path = "m"
pathLen = 1
)
payload := []byte("Asado Viernes")
privKey, _ := NewHDPrivateKeyFromString(privHex, path, Mainnet())
encrypted, _ := NewEncryptOperation(privKey, payload).Encrypt()
decrypted, err := NewDecryptOperation(privKey, encrypted).Decrypt()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(payload, decrypted) {
t.Fatal("decrypt is bad")
}
}

211
libwallet/features_test.go Normal file
View File

@ -0,0 +1,211 @@
package libwallet
import (
"testing"
)
func Test_DetermineUserActivatedFeatureStatus(t *testing.T) {
backendFeaturesWithTaproot := NewStringListWithElements([]string{
BackendFeatureTaproot, BackendFeatureTaprootPreactivation,
})
backendFeaturesWithTaprootPreactivation := NewStringListWithElements([]string{
BackendFeatureTaprootPreactivation,
})
backendFeaturesWithoutTaproot := NewStringListWithElements([]string{})
neverExportedKit := newIntList([]int{})
exportedPreviousKit := newIntList([]int{EKVersionDescriptors})
exportedOnlyLatest := newIntList([]int{EKVersionMusig})
exportedBoth := newIntList([]int{EKVersionDescriptors, EKVersionMusig})
type args struct {
feature UserActivatedFeature
height int
kitVersion *IntList
backendFeatures *StringList
network *Network
}
const (
postTaprootActivationHeight = 709_650
preTaprootActivationHeight = 709_620
)
tests := []struct {
name string
args args
want string
}{
{
"taproot scheduled in regtest",
args{
feature: UserActivatedFeatureTaproot,
height: 99,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaproot,
network: Regtest(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"taproot off in regtest",
args{
feature: UserActivatedFeatureTaproot,
height: 100,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithoutTaproot,
network: Regtest(),
},
UserActivatedFeatureStatusOff,
},
{
"taproot live in mainnet with no kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusActive,
},
{
"taproot live in mainnet with new kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusActive,
},
{
"taproot preactivated in mainnet with new kit",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusPreactivated,
},
{
"taproot needs activation in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedPreviousKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusCanActivate,
},
{
"taproot can preactivate in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: exportedPreviousKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusCanPreactivate,
},
{
"taproot scheduled in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"scheduled activation in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: exportedOnlyLatest,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"backend only preactivation for pre-activated user",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithTaprootPreactivation,
network: Mainnet(),
},
UserActivatedFeatureStatusPreactivated,
},
{
"backend only preactivation for scheduled activated user with no kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaprootPreactivation,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
{
"backend only preactivation for scheduled activated user with latest kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedOnlyLatest,
backendFeatures: backendFeaturesWithTaprootPreactivation,
network: Mainnet(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"backend turned off for pre-activated user",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithoutTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
{
"backend turned off for scheduled activated user with no kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithoutTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
{
"backend turned off for scheduled activated user with latest kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedOnlyLatest,
backendFeatures: backendFeaturesWithoutTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DetermineUserActivatedFeatureStatus(tt.args.feature, tt.args.height, tt.args.kitVersion, tt.args.backendFeatures, tt.args.network); got != tt.want {
t.Errorf("DetermineUserActivatedFeatureStatus() = %v, want %v", got, tt.want)
}
})
}
}

363
libwallet/fees/fees_test.go Normal file
View File

@ -0,0 +1,363 @@
package fees
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil"
)
func TestComputeSwapFees(t *testing.T) {
testCases := []struct {
desc string
amount btcutil.Amount
bestRouteFees []BestRouteFees
policies *FundingOutputPolicies
takeFeeFromAmount bool
expected *SwapFees
}{
{
desc: "smoke test",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
takeFeeFromAmount: false,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 1,
},
},
{
desc: "qualifies for 0-conf",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
takeFeeFromAmount: false,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
ConfirmationsNeeded: 0,
OutputAmount: 1010,
},
},
{
desc: "qualifies for debt lend",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeLend,
DebtAmount: 1010,
OutputAmount: 0,
ConfirmationsNeeded: 0,
},
},
{
desc: "debt collect",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 100,
PotentialCollect: 1010,
MaxAmountFor0Conf: 1000000,
},
takeFeeFromAmount: false,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeCollect,
DebtAmount: 1010,
OutputAmount: 2020,
ConfirmationsNeeded: 0,
},
},
{
desc: "dust threshold",
amount: 50,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 486,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 546,
ConfirmationsNeeded: 1,
},
},
{
desc: "sub-dust lend",
amount: 50,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 486,
DebtType: DebtTypeLend,
DebtAmount: 60,
OutputAmount: 0,
ConfirmationsNeeded: 0,
},
},
{
desc: "uses last route if route with enough capacity",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 20,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 20,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1020,
ConfirmationsNeeded: 1,
},
},
{
desc: "smoke test TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
takeFeeFromAmount: true,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 1,
},
},
{
desc: "qualifies for 0-conf TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
takeFeeFromAmount: true,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 1,
},
},
{
desc: "qualifies for debt lend TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: true,
policies: &FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 0,
},
},
{
desc: "debt collect TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 100,
PotentialCollect: 1010,
MaxAmountFor0Conf: 1000000,
},
takeFeeFromAmount: true,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeCollect,
DebtAmount: 1010,
OutputAmount: 2020,
ConfirmationsNeeded: 0,
},
},
{
desc: "dust threshold TFFA",
amount: 50,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: true,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 486,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 546,
ConfirmationsNeeded: 1,
},
},
{
desc: "uses last route if route with enough capacity",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 20,
},
},
takeFeeFromAmount: true,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 20,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1020,
ConfirmationsNeeded: 1,
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
fees := ComputeSwapFees(tC.amount, tC.bestRouteFees, tC.policies, tC.takeFeeFromAmount)
if !reflect.DeepEqual(fees, tC.expected) {
t.Errorf("fees do not equal expected fees (%+v != %+v)", fees, tC.expected)
}
})
}
}

View File

@ -0,0 +1,82 @@
package hdpath
import (
"reflect"
"testing"
)
var (
rootPath = make([]PathIndex, 0)
shortPath = []PathIndex{
PathIndex{Index: 0, Hardened: true},
}
longPath = []PathIndex{
PathIndex{Index: 44, Hardened: true},
PathIndex{Index: 1, Hardened: true},
PathIndex{Index: 2, Hardened: false},
}
shortMuunPath = []PathIndex{
PathIndex{Index: 1, Hardened: true, Name: "schema"},
}
longMuunPath = []PathIndex{
PathIndex{Index: 1, Hardened: true, Name: "schema"},
PathIndex{Index: 1, Hardened: true, Name: "recovery"},
}
)
func TestBuild(t *testing.T) {
p, err := Parse("m/1/1")
if err != nil {
t.Fatal(err)
}
p = p.Child(0)
p = p.NamedChild("foo", 1)
if p.String() != "m/1/1/0/foo:1" {
t.Fatalf("expected path to be m/1/1/0/foo:1, got %s instead", p.String())
}
}
func TestParsingAndValidation(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want []PathIndex
wantErr bool
}{
{name: "root1", args: args{path: ""}, want: rootPath},
{name: "root2", args: args{path: "m"}, want: rootPath},
{name: "root3", args: args{path: "/"}, want: rootPath},
{name: "short1", args: args{path: "m/0'"}, want: shortPath},
{name: "short2", args: args{path: "0'"}, want: shortPath},
{name: "short3", args: args{path: "/0'"}, want: shortPath},
{name: "long1", args: args{path: "m/44'/1'/2"}, want: longPath},
{name: "long2", args: args{path: "/44'/1'/2"}, want: longPath},
{name: "long3", args: args{path: "44'/1'/2"}, want: longPath},
{name: "shortMuun", args: args{path: "m/schema:1'"}, want: shortMuunPath},
{name: "longMuun", args: args{path: "m/schema:1'/recovery:1'"}, want: longMuunPath},
{name: "has spaces", args: args{path: "m / 0 / 1"}, wantErr: true},
{name: "has no indexes", args: args{path: "m/b/c"}, wantErr: true},
{name: "has weird chars", args: args{path: "m/1.2^3"}, wantErr: true},
{name: "has several :", args: args{path: "m/recovery:1:1"}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := Parse(tt.args.path)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
indexes := path.Indexes()
if !reflect.DeepEqual(indexes, tt.want) {
t.Errorf("Indexes() = %v, want %v", indexes, tt.want)
}
})
}
}

294
libwallet/hdprivatekey_test.go Executable file
View File

@ -0,0 +1,294 @@
package libwallet
import (
"crypto/sha256"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
)
const (
// m
vector1PrivKey = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
vector1PubKey = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
vector1FirstPath = "m/0'/1"
vector1FirstPriv = "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"
vector1FirstPub = "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
vector1SecondPath = "m/0'/1/2'/2"
vector1SecondPriv = "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"
vector1SecondPub = "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
vector2PrivKey = "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
vector2PubKey = "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
vector2FirstPath = "m/0"
vector2FirstPriv = "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
vector2FirstPub = "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
vector2SecondPath = "m/0/2147483647'/1"
vector2SecondPriv = "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
vector2SecondPub = "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
vector2ThirdPath = "m/0/2147483647'/1/2147483646'"
vector2ThirdPriv = "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"
vector2ThirdPub = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
vector3PrivKey = "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6"
vector3PubKey = "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13"
vector3FirstPath = "m/0'"
vector3FirstPriv = "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"
vector3FirstPub = "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y"
symmetricPrivKey = "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
symmetricPubKey = "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
symmetricFirstPath = "m/0"
symmetricFirstPriv = "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
symmetricFirstPub = "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
symmetricSecondPath = "m/0/2147483647'/1"
symmetricSecondPriv = "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
symmetricSecondPub = "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
symmetricThirdPath = "m/0/2147483647'/1/2147483646'"
symmetricThirdPriv = "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"
symmetricThirdPub = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
)
var (
network = Regtest()
)
func TestNewHDPrivateKeySerialization(t *testing.T) {
t.Run("bad key generation", func(t *testing.T) {
badKey, err := NewHDPrivateKey(randomBytes(1), network)
if badKey != nil || err == nil {
t.Errorf("keys with only 1 byte should return error, got %v, %v", badKey, err)
}
})
t.Run("invalid key deserialization", func(t *testing.T) {
badKey, err := NewHDPrivateKeyFromString("fooo", "m", Regtest())
if badKey != nil || err == nil {
t.Errorf("bad key should only return error returned %v, %v", badKey, err)
}
badKey, err = NewHDPrivateKeyFromString(vector1FirstPub, "m", Regtest())
if badKey != nil || err == nil {
t.Errorf("expected failure when parsing pub key as priv key, got %v, %v", badKey, err)
}
badPubKey, err := NewHDPublicKeyFromString(vector1FirstPriv, "m", Regtest())
if badPubKey != nil || err == nil {
t.Errorf("expected failure when parsing priv key as pub key, got %v, %v", badPubKey, err)
}
})
t.Run("test regtest address serialization", func(t *testing.T) {
// Create a new key and set regtest as chain
randomKey, _ := NewHDPrivateKey(randomBytes(16), network)
randomKey.key.SetNet(&chaincfg.RegressionNetParams)
// Parsing it should fail since we check we know the chain
key, err := NewHDPrivateKeyFromString(randomKey.String(), "m", Regtest())
if key == nil || err != nil {
t.Errorf("failed to parse regtest key, got err %v", err)
}
})
t.Run("Random key serialization", func(t *testing.T) {
seed := randomBytes(16)
randomKey, err := NewHDPrivateKey(seed, network)
if err != nil {
t.Fatalf("couldn't generate priv key")
}
serialized := randomKey.String()
deserialized, err := NewHDPrivateKeyFromString(serialized, "m", Regtest())
if err != nil {
t.Fatalf("failed to deserialize key")
}
if serialized != deserialized.String() {
t.Errorf("keys are different")
}
})
t.Run("Child key serialization", func(t *testing.T) {
root, err := NewHDPrivateKeyFromString(
"tprv8ZgxMBicQKsPdGCzsJ31BsQnFL1TSQ82dfsZYTtsWJ1T8g7xTfnV19gf8nYPqzkzk6yLL9kzDYshmUrYyXt7uXsGbk9eN7juRxg9sjaxSjn", "m", Regtest())
if err != nil {
t.Fatalf("failed to parse root key")
}
key1, _ := root.DerivedAt(1, true)
key2, _ := key1.DerivedAt(1, true)
const encodedKey = "tprv8e8vMhwEcLr1ZfZETKTQSpxJ6KfZuczALe8KrRCDLpSbXPwp7PY1ZVHtqUkFsYZETPRcfjVSCv8DiYP9RyAZrFhnLE8aYdaSaZEWyT5c8Ji"
if key2.String() != encodedKey {
t.Fatalf("derived key doesn't match serialized")
}
decodedKey, _ := NewHDPrivateKeyFromString(encodedKey, "m", Regtest())
if decodedKey.String() != encodedKey {
t.Fatalf("decoded key doesn't match encoded string")
}
})
}
// These tests are based on the test vectors in BIP 32
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
func TestKeyDerivation(t *testing.T) {
testPath := func(t *testing.T, privKey *HDPrivateKey, path, priv, pub string) {
key, _ := privKey.DeriveTo(path)
if key.String() != priv {
t.Errorf("%v priv doesn't match", path)
}
if key.PublicKey().String() != pub {
t.Errorf("%v pub doesn't match", path)
}
}
t.Run("vector1", func(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(vector1PrivKey, "m", Regtest())
if privKey.PublicKey().String() != vector1PubKey {
t.Errorf("pub key doesnt match")
}
testPath(t, privKey, vector1FirstPath, vector1FirstPriv, vector1FirstPub)
testPath(t, privKey, vector1SecondPath, vector1SecondPriv, vector1SecondPub)
})
t.Run("vector2", func(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(vector2PrivKey, "m", Regtest())
if privKey.PublicKey().String() != vector2PubKey {
t.Errorf("pub key doesnt match")
}
testPath(t, privKey, vector2FirstPath, vector2FirstPriv, vector2FirstPub)
testPath(t, privKey, vector2SecondPath, vector2SecondPriv, vector2SecondPub)
testPath(t, privKey, vector2ThirdPath, vector2ThirdPriv, vector2ThirdPub)
})
t.Run("vector3", func(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(vector3PrivKey, "m", Regtest())
if privKey.PublicKey().String() != vector3PubKey {
t.Errorf("pub key doesnt match")
}
testPath(t, privKey, vector3FirstPath, vector3FirstPriv, vector3FirstPub)
})
}
func TestSymmetricDerivation(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(symmetricPrivKey, "m", Regtest())
pubKey := privKey.PublicKey()
t.Run("basic check", func(t *testing.T) {
if pubKey.String() != symmetricPubKey {
t.Fatalf("pub key doesn't match")
}
})
t.Run("first path", func(t *testing.T) {
newPriv, _ := privKey.DeriveTo(symmetricFirstPath)
if newPriv.String() != symmetricFirstPriv {
t.Errorf("priv key doesn't match")
}
if newPriv.PublicKey().String() != symmetricFirstPub {
t.Errorf("pub key doesn't match")
}
newPub, _ := pubKey.DeriveTo(symmetricFirstPath)
if newPub.String() != newPriv.PublicKey().String() {
t.Errorf("extracted and derived pub key don't match")
}
})
t.Run("second path", func(t *testing.T) {
newPriv, _ := privKey.DeriveTo(symmetricSecondPath)
if newPriv.String() != symmetricSecondPriv {
t.Errorf("priv key doesn't match")
}
if newPriv.PublicKey().String() != symmetricSecondPub {
t.Errorf("pub key doesn't match")
}
hPriv, _ := privKey.DeriveTo("m/0/2147483647'")
newPub, _ := hPriv.PublicKey().DeriveTo(symmetricSecondPath)
if newPub.String() != newPriv.PublicKey().String() {
t.Errorf("extracted and derived pub key don't match")
}
})
t.Run("third path", func(t *testing.T) {
newPriv, _ := privKey.DeriveTo(symmetricThirdPath)
if newPriv.String() != symmetricThirdPriv {
t.Errorf("priv key doesn't match")
}
if newPriv.PublicKey().String() != symmetricThirdPub {
t.Errorf("pub key doesn't match")
}
})
testBadDerivation := func(t *testing.T, path string) {
privKey, _ := NewHDPrivateKeyFromString(vector1PrivKey, "m/123", Regtest())
pubKey := privKey.PublicKey()
badKey, err := privKey.DeriveTo(path)
if badKey != nil || err == nil {
t.Errorf("derivation should fail got %v, %v", badKey, err)
}
badPubKey, err := pubKey.DeriveTo(path)
if badPubKey != nil || err == nil {
t.Errorf("derivation should fail got %v, %v", badPubKey, err)
}
}
t.Run("new path is not prefix of old path", func(t *testing.T) {
testBadDerivation(t, "m/45")
})
t.Run("derivation path is invalid", func(t *testing.T) {
testBadDerivation(t, "m/123/asd45")
})
}
func TestHDPrivateKeySign(t *testing.T) {
seed := randomBytes(32)
privKey, err := NewHDPrivateKey(seed, Regtest())
if err != nil {
t.Fatal(err)
}
data := []byte("foobar")
sigBytes, err := privKey.Sign(data)
if err != nil {
t.Fatal(err)
}
sig, err := btcec.ParseSignature(sigBytes, btcec.S256())
if err != nil {
t.Fatal(err)
}
pubKey, err := privKey.key.ECPubKey()
if err != nil {
t.Fatal(err)
}
hash := sha256.Sum256(data)
if ok := sig.Verify(hash[:], pubKey); !ok {
t.Fatal(err)
}
}

36
libwallet/hdpublickey_test.go Executable file
View File

@ -0,0 +1,36 @@
package libwallet
import (
"bytes"
"math"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
)
func TestHDPublicKey_DerivedAt(t *testing.T) {
priv, _ := NewHDPrivateKey(randomBytes(32), Mainnet())
_, err := priv.PublicKey().DerivedAt(math.MaxUint32)
if err == nil {
t.Errorf("derived a hardened pub key")
}
_, err = priv.PublicKey().DerivedAt(math.MaxUint32 ^ hdkeychain.HardenedKeyStart)
if err != nil {
t.Errorf("failed to derive unhardened pub key due to %v", err)
}
}
func TestHDPublicKey_Fingerprint(t *testing.T) {
pubKey, _ := NewHDPublicKeyFromString(
"xpub661MyMwAqRbcF3YgLe8xTTTrDHf5bmEQuj5XfQP3bvwHqBpYvt99tcMSXXzroWJoQM4eMDNZNzNYZEJfTqxq5S82J644buASmW4Y7VnwUeJ",
"m/schema:1'/recovery:1'",
Mainnet(),
)
fingerprint := pubKey.Fingerprint()
if !bytes.Equal(fingerprint, []byte{207, 227, 7, 97}) {
t.Fatalf("fingerprint does not match, got %x", fingerprint)
}
}

View File

@ -0,0 +1,791 @@
package libwallet
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/muun/libwallet/hdpath"
)
func TestFulfillHtlc(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
// stub
swapServerPublicKey := randomBytes(32)
invoice := secrets.Get(0)
paymentHash := invoice.PaymentHash
amt := int64(10000)
lockTime := int64(1000)
htlcKeyPath := hdpath.MustParse(invoice.keyPath).Child(htlcKeyChildIndex)
userHtlcKey, err := userKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
muunHtlcKey, err := muunKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
htlcScript, err := createHtlcScript(
userHtlcKey.PublicKey().Raw(),
muunHtlcKey.PublicKey().Raw(),
swapServerPublicKey,
lockTime,
paymentHash,
)
if err != nil {
panic(err)
}
witnessHash := sha256.Sum256(htlcScript)
address, err := btcutil.NewAddressWitnessScriptHash(witnessHash[:], Regtest().network)
if err != nil {
t.Fatal(err)
}
pkScript, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatal(err)
}
prevOutHash, err := chainhash.NewHash(randomBytes(32))
if err != nil {
panic(err)
}
htlcTx := wire.NewMsgTx(1)
htlcTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *prevOutHash,
},
})
htlcTx.AddTxOut(&wire.TxOut{
PkScript: pkScript,
Value: amt,
})
nodePublicKey, err := invoice.IdentityKey.key.ECPubKey()
if err != nil {
panic(err)
}
fulfillmentTx := wire.NewMsgTx(1)
fulfillmentTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: 0,
},
})
outputPath := "m/schema:1'/recovery:1'/34/56"
addr := newAddressAt(userKey, muunKey, outputPath, network)
fulfillmentTx.AddTxOut(&wire.TxOut{
PkScript: addr.ScriptAddress(),
Value: amt,
})
muunSignKey, err := muunHtlcKey.key.ECPrivKey()
if err != nil {
panic(err)
}
sigHashes := txscript.NewTxSigHashes(fulfillmentTx)
muunSignature, err := txscript.RawTxInWitnessSignature(
fulfillmentTx,
sigHashes,
0,
amt,
htlcScript,
txscript.SigHashAll,
muunSignKey,
)
if err != nil {
panic(err)
}
swap := &IncomingSwap{
SphinxPacket: createSphinxPacket(nodePublicKey, paymentHash, invoice.paymentSecret, amt, lockTime),
PaymentHash: paymentHash,
Htlc: &IncomingSwapHtlc{
HtlcTx: serializeTx(htlcTx),
ExpirationHeight: lockTime,
SwapServerPublicKey: swapServerPublicKey,
},
}
data := &IncomingSwapFulfillmentData{
FulfillmentTx: serializeTx(fulfillmentTx),
MuunSignature: muunSignature,
MerkleTree: nil,
HtlcBlock: nil,
ConfirmationTarget: 1,
}
result, err := swap.Fulfill(data, userKey, muunKey.PublicKey(), network)
if err != nil {
t.Fatal(err)
}
signedTx := wire.NewMsgTx(2)
signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx))
verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 0, 0)
}
func TestFulfillHtlcWithCollect(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
// stub
swapServerPublicKey := randomBytes(32)
invoiceSecrets := secrets.Get(0)
paymentHash := invoiceSecrets.PaymentHash
amt := int64(10000)
lockTime := int64(1000)
collected := int64(1000)
outputAmount := amt - collected
htlcKeyPath := hdpath.MustParse(invoiceSecrets.keyPath).Child(htlcKeyChildIndex)
userHtlcKey, err := userKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
muunHtlcKey, err := muunKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
htlcScript, err := createHtlcScript(
userHtlcKey.PublicKey().Raw(),
muunHtlcKey.PublicKey().Raw(),
swapServerPublicKey,
lockTime,
paymentHash,
)
if err != nil {
panic(err)
}
witnessHash := sha256.Sum256(htlcScript)
address, err := btcutil.NewAddressWitnessScriptHash(witnessHash[:], Regtest().network)
if err != nil {
t.Fatal(err)
}
pkScript, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatal(err)
}
prevOutHash, err := chainhash.NewHash(randomBytes(32))
if err != nil {
panic(err)
}
htlcTx := wire.NewMsgTx(1)
htlcTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *prevOutHash,
},
})
htlcTx.AddTxOut(&wire.TxOut{
PkScript: pkScript,
Value: amt,
})
nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey()
if err != nil {
panic(err)
}
fulfillmentTx := wire.NewMsgTx(1)
fulfillmentTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: 0,
},
})
outputPath := "m/schema:1'/recovery:1'/34/56"
addr := newAddressAt(userKey, muunKey, outputPath, network)
fulfillmentTx.AddTxOut(&wire.TxOut{
PkScript: addr.ScriptAddress(),
Value: outputAmount,
})
muunSignKey, err := muunHtlcKey.key.ECPrivKey()
if err != nil {
panic(err)
}
sigHashes := txscript.NewTxSigHashes(fulfillmentTx)
muunSignature, err := txscript.RawTxInWitnessSignature(
fulfillmentTx,
sigHashes,
0,
amt,
htlcScript,
txscript.SigHashAll,
muunSignKey,
)
if err != nil {
panic(err)
}
swap := &IncomingSwap{
SphinxPacket: createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime),
PaymentHash: paymentHash,
Htlc: &IncomingSwapHtlc{
HtlcTx: serializeTx(htlcTx),
ExpirationHeight: lockTime,
SwapServerPublicKey: swapServerPublicKey,
},
CollectSat: collected,
}
data := &IncomingSwapFulfillmentData{
FulfillmentTx: serializeTx(fulfillmentTx),
MuunSignature: muunSignature,
OutputVersion: 4,
OutputPath: outputPath,
MerkleTree: nil,
HtlcBlock: nil,
ConfirmationTarget: 1,
}
result, err := swap.Fulfill(data, userKey, muunKey.PublicKey(), network)
if err != nil {
t.Fatal(err)
}
swap.CollectSat = 0
_, err = swap.Fulfill(data, userKey, muunKey.PublicKey(), network)
if err == nil {
t.Fatal("expected 0 collect to fail")
}
signedTx := wire.NewMsgTx(2)
signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx))
verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 0, 0)
}
func TestVerifyFulfillable(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
generateAndPersistInvoiceSecrets := func() {
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
}
createInvoice := func(amountSat int64) string {
builder := InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(&RouteHints{
Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869fd",
FeeBaseMsat: 1000,
FeeProportionalMillionths: 1000,
CltvExpiryDelta: 8,
})
if amountSat != 0 {
builder.AmountSat(amountSat)
}
retry:
invoice, err := builder.Build()
if err != nil {
panic(err)
}
if invoice == "" {
generateAndPersistInvoiceSecrets()
goto retry
}
return invoice
}
t.Run("single part payment", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
t.Run("multi part payment fails", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createMppSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected failure to fulfill mpp payment")
}
})
t.Run("non existant invoice", func(t *testing.T) {
swap := &IncomingSwap{
PaymentHash: randomBytes(32),
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected failure to fulfill non existant invoice")
}
})
t.Run("invalid payment secret", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, _, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, randomBytes(32), amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with random payment secret")
}
})
t.Run("muun 2 muun with no blob", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, _, _ := getInvoiceSecrets(invoice, userKey)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: nil,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
t.Run("invalid amount from server", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt - 1,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with invalid amount")
}
})
t.Run("validates amount from server", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
t.Run("validates invoice amount", func(t *testing.T) {
invoice := createInvoice(20000)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with amount not matching invoice amount")
}
})
t.Run("validates invoice amount for muun 2 muun", func(t *testing.T) {
invoice := createInvoice(20000)
paymentHash, _, _ := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
swap := &IncomingSwap{
PaymentHash: paymentHash,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with amount not matching invoice amount")
}
})
t.Run("invoice with amount", func(t *testing.T) {
invoice := createInvoice(20000)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(20000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
}
func TestFulfillWithHardwiredData(t *testing.T) {
setup()
d := func(s string) []byte {
b, _ := hex.DecodeString(s)
return b
}
network := Regtest()
swap := &IncomingSwap{
SphinxPacket: d("0002dc29e8562cbd4961bbe76ebc847641fba878b5dda04a31d17c5c4648c4e8f614380397b83978e2f12161c7a010d494f16ca5dc96a06369a19ccfadf9ee3ec0ecdcac9479b25459d01670c629175e8cc1110f328ec6d0e21ca81c5a7f3b71023b10ca287985695fc4c757ea25c9d49bd6b4e43bb85abe043fbcb2ef473bfd1830dbdad7c3e6de26d3a703bd307cba5a33ba56d8398e22c87034b6794ecd4c2d4157a90520b78171b1860c69c302b25f7a10edf9ac3ad87d10cf7cbe8525ac4b3ebc6544787b1b010e61ab73ee86ae8752f44687753af3b31678a7fe1e85c57c6e1de33878f43ccbba1478fbd8c055a5e2c55cadcae05537f6478ba13391343c7f1063138ba9c38803ac8fd6b9eb5b5114559df1746593df1d4d9a6883f835758dc583bb9dea72ad3079df653e73efa915c629ba8056d945cf63dc316ffd118aa7e8d20430de12ac9beaf9f472b68bdf278dccd6a84f2b6c37c25ddb3abc3583094613a07f277ed80840a33ae34d62e3dd17d21e2faf82221375914444460e38ebe5ef67d9fac02de507d7964a2191b0de43c0c7115840e863f1ca03e0a5b05dedb90826b79b1b1ce5aa7666c37bae08bbe8032a82ed1d9c15df4195e408be16844dc2b5e5868a38bd560e87d629d1c6ec11e3dbb112dc1d2692ad4b7c28b5904bf49c1efcb87562f48ec5e7177f2034dadd2c08c4a02d718ffa16585738489d89f01d350123e621e4bd8927879bd3c4cccf1fe44f7b4daf4466a60b7197dbb14c5ffd23e477343fa79a8d8818804280757b1f98439749927de21545d1a9434c59c1d0e093ab3c1936b4db3b4c67dd9cae55cf2ee55066490a602a74cf88382d35db442b7e57b869fd43360ca0c9ef03bc89784e340450fcae81fb2080c97f9852124900a71bf68921e5a6e690a5ee73c266df2344106aec8de601f8a14254c97ee96dd3f858df1cb727ee51bc8ebeb6dea5253841bd2a13aeba1bc3846c9cc45d7124f9f9aa61a6c3a7b15424c5dfadfb7644392bf0843f643d97b2e08c1a3d6ebfcb7aafcd78cd2d904645cf043e1a42b60390647f24d6663fc74dc77d06bb691d12b09bb4afc3b55427f5bac76748b73b6debb17ca6bb890f2005f39e714aa0e7a584e57a41a78f1d3f4981ce4e22a49caa389360eabc9f623b923c864eb74a2a860a061d6ecbe6f4c55596907ba342836c7607117f405e098af1f73b8ae2542a59d30c58fca8ee37c6482bd87069b142e692f54a04fd6d3a5e22595eb2de31c830cea4395b085b7c8725971df657c5af5501fa8cc9cefda4f1ae8862b6229ed74b045e17587f68ab55c9176c256c69564274502d0ec6e5e3be8ea93e14428d328963ca4671ee2f629ae8f2c2ff8f2b2145f218d8a3707715bdfa5b2bb5211b9cd8775e33ce5546f618bc998b5953c5d2a2f7932873fd248be3a504ce7f7d4b731bfb4fea363e5e281ff3c314b997d8c89d90c8bf15d983da26e75bf52e98b92d108e6f4aee0b25561d0ce8f22a8400b2085e713d909c20b2c84d5ba36dbe94f324690ab207070bfb7247510e78263989dc04669ea273ca44d2de31aa8a950bc120fcec0c627ad78b59f635ddd657d97d56fcc9ebef32b3ee1051e003c0b617a1196d6c387f014fd47e7f1c64b27d43cadfaf25a7849a77392a63470665e5e3bb0c28b66b9de938c805fab01de62cd63b0d200f97156236fcd412f1eadc125371bd09726e65da8ee8e77e7fa0070bb4f6090a2afd7a33e3d37aff7a5dac62830a7f79aa28f6bce305fc6eb96dd53cd2448b618bdadfc79dcee815d6dd6935d9cece06f810df6cbd529b01361d97f3c50d749739d9598edd53c9bd984a5348a5345c25c13fc7c6d48b7412f4ab6de74e6b7fd4945f710562c312a2903680c387a7364920e435db7777fe66b60a49adb656cdd12f"),
PaymentHash: d("31b35302d3e842a363f8992e423910bfb655b9cd6325b67f5c469fa8f2c4e55b"),
Htlc: &IncomingSwapHtlc{
HtlcTx: d("02000000000101896c8b88d8219cc7dae111558626c952da6fc2a542f7db970e8af745c4678bdb0000000000feffffff02d006032a01000000160014b710e26258f27a99807e2a09bf39b5d3588c561b089d0000000000002200208fb1ed3841bee4385ba4efe1a8aff0943b3b1eeadada45e4784f54e2efa1f30a0247304402205e6a82391804b8bc483f6d9d44bdcd7afb477f66c4c794872735447f1dd883480220626fc746386f8afed04a43776d661bab1d610cdebcb5d03c7d594b0edd3612ed0121037d4c78fdce4b13788efb012a68834da3a75f6ac153f55edf22fadc09e6d4f67700000000"),
ExpirationHeight: 401,
SwapServerPublicKey: d("028b7c740b590012eaffef072675baaa95aee39508fd049ed1cd698ee26ce33f02"),
},
}
data := &IncomingSwapFulfillmentData{
FulfillmentTx: d("0100000001a2b209d88daaa2b9fedc8217904b75934d280f889cd64db243c530dbd72a9b670100000000ffffffff0110270000000000002200209c58b43eff77533a3a056046ee4cb5044bb0eeb74635ebb8cc03048b3720716b00000000"),
MuunSignature: d("30450221008c40c9ef1613cfa500c52531b9fd0b7212f562e425dcdc4358cc3a6de25e11940220717ab86c13cb645dd2e694c3b4e5fd0e81e84f00ed8380570ab33a19fed0547201"),
OutputVersion: 4,
OutputPath: "m/schema:1\\'/recovery:1\\'/change:0/3",
MerkleTree: d(""),
HtlcBlock: d(""),
ConfirmationTarget: 0,
}
userKey, _ := NewHDPrivateKeyFromString("tprv8eNitriyeyGgaAe7teh17j8mvqN3MuzkFy5TzdfS4KUATjgdP29jN7w9A8iQ5PDUZMqsb2aiJjEgjuPGCRjoDbJsCZ5iFGpb4uJCXkksjXM", "m/schema:1'/recovery:1'", network)
muunKey, _ := NewHDPublicKeyFromString("tpubDBYMnFoxYLdMBZThTk4uARTe4kGPeEYWdKcaEzaUxt1cesetnxtTqmAxVkzDRou51emWytommyLWcF91SdF5KecA6Ja8oHK1FF7d5U2hMxX", "m/schema:1'/recovery:1'", network)
invoice := &InvoiceSecrets{
preimage: d("52441108d7144b82ed13a18b7572fa78fa6f6a3f85fdbf4752dcce985430e43c"),
paymentSecret: d("79d011595d443897b46c2811bd4e5aa7f3fa225b880249edb64b52601aa7f963"),
keyPath: "m/schema:1'/recovery:1'/invoices:4/1159744029/738246992",
PaymentHash: d("31b35302d3e842a363f8992e423910bfb655b9cd6325b67f5c469fa8f2c4e55b"),
IdentityKey: nil,
UserHtlcKey: nil,
MuunHtlcKey: nil,
ShortChanId: 123,
}
PersistInvoiceSecrets(&InvoiceSecretsList{secrets: []*InvoiceSecrets{invoice}})
result, err := swap.Fulfill(data, userKey, muunKey, network)
if err != nil {
t.Fatal(err)
}
htlcTx := wire.NewMsgTx(2)
htlcTx.Deserialize(bytes.NewReader(swap.Htlc.HtlcTx))
signedTx := wire.NewMsgTx(2)
signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx))
verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 1, 0)
}
func TestFulfillFullDebt(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
invoice := secrets.Get(0)
swap := &IncomingSwap{
PaymentHash: invoice.PaymentHash,
}
result, err := swap.FulfillFullDebt()
if err != nil {
t.Fatal(err)
}
if result.FulfillmentTx != nil {
t.Fatal("expected FulfillmentTx to be nil")
}
if result.Preimage == nil {
t.Fatal("expected preimage to be non-nil")
}
}
func createSphinxPacket(nodePublicKey *btcec.PublicKey, paymentHash, paymentSecret []byte, amt, lockTime int64) []byte {
var paymentPath sphinx.PaymentPath
paymentPath[0].NodePub = *nodePublicKey
var secret [32]byte
copy(secret[:], paymentSecret)
uintAmount := uint64(amt * 1000) // msat are expected
uintLocktime := uint32(lockTime)
tlvRecords := []tlv.Record{
record.NewAmtToFwdRecord(&uintAmount),
record.NewLockTimeRecord(&uintLocktime),
record.NewMPP(lnwire.MilliSatoshi(uintAmount), secret).Record(),
}
b := &bytes.Buffer{}
tlv.MustNewStream(tlvRecords...).Encode(b)
hopPayload, err := sphinx.NewHopPayload(nil, b.Bytes())
if err != nil {
panic(err)
}
paymentPath[0].HopPayload = hopPayload
ephemeralKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
panic(err)
}
pkt, err := sphinx.NewOnionPacket(
&paymentPath, ephemeralKey, paymentHash, sphinx.BlankPacketFiller)
if err != nil {
panic(err)
}
var buf bytes.Buffer
err = pkt.Encode(&buf)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func createMppSphinxPacket(
nodePublicKey *btcec.PublicKey,
paymentHash, paymentSecret []byte,
amt, lockTime int64,
) []byte {
var paymentPath sphinx.PaymentPath
paymentPath[0].NodePub = *nodePublicKey
var secret [32]byte
copy(secret[:], paymentSecret)
uintAmount := uint64(amt * 1000) // msat are expected
uintLocktime := uint32(lockTime)
uintFwdAmount := uintAmount / 2
tlvRecords := []tlv.Record{
record.NewAmtToFwdRecord(&uintFwdAmount),
record.NewLockTimeRecord(&uintLocktime),
record.NewMPP(lnwire.MilliSatoshi(uintAmount), secret).Record(),
}
b := &bytes.Buffer{}
tlv.MustNewStream(tlvRecords...).Encode(b)
hopPayload, err := sphinx.NewHopPayload(nil, b.Bytes())
if err != nil {
panic(err)
}
paymentPath[0].HopPayload = hopPayload
ephemeralKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
panic(err)
}
pkt, err := sphinx.NewOnionPacket(
&paymentPath, ephemeralKey, paymentHash, sphinx.BlankPacketFiller)
if err != nil {
panic(err)
}
var buf bytes.Buffer
err = pkt.Encode(&buf)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func newAddressAt(userKey, muunKey *HDPrivateKey, keyPath string, network *Network) btcutil.Address {
userPublicKey, err := userKey.PublicKey().DeriveTo(keyPath)
if err != nil {
panic(err)
}
muunPublicKey, err := muunKey.PublicKey().DeriveTo(keyPath)
if err != nil {
panic(err)
}
muunAddr, err := CreateAddressV4(userPublicKey, muunPublicKey)
if err != nil {
panic(err)
}
addr, err := btcutil.DecodeAddress(muunAddr.Address(), network.network)
if err != nil {
panic(err)
}
return addr
}
func serializeTx(tx *wire.MsgTx) []byte {
var buf bytes.Buffer
err := tx.Serialize(&buf)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func getInvoiceSecrets(invoice string, userKey *HDPrivateKey) (paymentHash []byte, paymentSecret []byte, identityKey *btcec.PublicKey) {
db, err := openDB()
if err != nil {
panic(err)
}
defer db.Close()
payReq, err := zpay32.Decode(invoice, network.network)
if err != nil {
panic(err)
}
dbInvoice, err := db.FindByPaymentHash(payReq.PaymentHash[:])
if err != nil {
panic(err)
}
paymentHash = payReq.PaymentHash[:]
paymentSecret = dbInvoice.PaymentSecret
keyPath := hdpath.MustParse(dbInvoice.KeyPath).Child(identityKeyChildIndex)
key, err := userKey.DeriveTo(keyPath.String())
if err != nil {
panic(err)
}
identityKey, err = key.key.ECPubKey()
if err != nil {
panic(err)
}
return
}

14
libwallet/init_test.go Normal file
View File

@ -0,0 +1,14 @@
package libwallet
import "io/ioutil"
func setup() {
dir, err := ioutil.TempDir("", "libwallet")
if err != nil {
panic(err)
}
Init(&Config{
DataDir: dir,
})
}

View File

215
libwallet/invoice_test.go Executable file
View File

@ -0,0 +1,215 @@
package libwallet
import (
"encoding/hex"
"reflect"
"testing"
)
func TestParseInvoice(t *testing.T) {
const (
invoice = "lnbcrt1pwtpd4xpp55meuklpslk5jtxytyh7u2q490c2xhm68dm3a94486zntsg7ad4vsdqqcqzys763w70h39ze44ngzhdt2mag84wlkefqkphuy7ssg4la5gt9vcpmqts00fnapf8frs928mc5ujfutzyu8apkezhrfvydx82l40w0fckqqmerzjc"
invoiceWithAmount = "lnbcrt10u1pwtpd4jpp5lh0p9amq02xel0gduna95ta5ve9q5dwyk8tglvpa258yzzvcgynsdqqcqzysrukfteknjzcqpu8kfnm76dhdtnkmyr3j42xrl89axhqxmpgusyqhn28u2uaave3nr8sk3mg5nug6t8hcnj2aw8t2l5wtksh6w0yyntgqjrrgqk"
invoiceWithDescription = "lnbcrt1pwtpdh7pp5celcayxvuw9pm9f8420n2dyd3css8ahzlr4nl69uczhf2sf99ydqdqswe5hvcfqwpjhymmwcqzysx7gwcf9a559rxrah9yp0u7dnk4vuvq2ywy6dyqtwzna9c92q058qppmv9p094vq9g6nv46d3sc7jd8faglzjj2h0w7j06wcu2h3e27cqc5zm4d"
invoiceWithFallbackAdrr = "lnbcrt1pwtpduxpp57xglq4thtrerzzxt8wzg4wresfclewh8pk8xghahwq8kgek3qslqdqqcqzysfppqhv0a0uhrt2crdehgfge8e8e6texw3q4hpmge888yuu6076utcrhgc97wu7vydmudyagkz25ahuyp4fqrc9e945ff248cpa3krn7vvgcqq6spyuqltd245sjvwh23gz220cegadspkn3lx0"
invoiceHashHex = "a6f3cb7c30fda925988b25fdc502a57e146bef476ee3d2d6a7d0a6b823dd6d59"
invoiceWithAmountHashHex = "fdde12f7607a8d9fbd0de4fa5a2fb4664a0a35c4b1d68fb03d550e4109984127"
invoiceWithDescriptionHashHex = "c67f8e90cce38a1d9527aa9f35348d8e2103f6e2f8eb3fe8bcc0ae954125291a"
invoiceWithFallbackAddrHashHex = "f191f0557758f23108cb3b848ab8798271fcbae70d8e645fb7700f6466d1043e"
invoiceDestinationHex = "028cfad4e092191a41f081bedfbe5a6e8f441603c78bf9001b8fb62ac0858f20edasd"
)
invoiceDestination, _ := hex.DecodeString(invoiceDestinationHex)
invoicePaymentHash := make([]byte, 32)
hex.Decode(invoicePaymentHash[:], []byte(invoiceHashHex))
invoiceWithAmountPaymentHash := make([]byte, 32)
hex.Decode(invoiceWithAmountPaymentHash[:], []byte(invoiceWithAmountHashHex))
invoiceWithDescriptionPaymentHash := make([]byte, 32)
hex.Decode(invoiceWithDescriptionPaymentHash[:], []byte(invoiceWithDescriptionHashHex))
invoiceWithFallbackAddrPaymentHash := make([]byte, 32)
hex.Decode(invoiceWithFallbackAddrPaymentHash[:], []byte(invoiceWithFallbackAddrHashHex))
fallbackAddr, _ := GetPaymentURI("bcrt1qhv0a0uhrt2crdehgfge8e8e6texw3q4has8jh7", network)
type args struct {
invoice string
network *Network
}
tests := []struct {
name string
args args
want *Invoice
wantErr bool
}{
{
name: "simple invoice",
args: args{
invoice: invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
name: "simple invoice with scheme",
args: args{
invoice: lightningScheme + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
name: "simple invoice with uppercased scheme",
args: args{
invoice: "LIGHTNING:" + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
// -amt 1000
name: "invoice with amount",
args: args{
invoice: invoiceWithAmount,
network: network,
},
want: &Invoice{
RawInvoice: invoiceWithAmount,
FallbackAddress: nil,
Network: network,
MilliSat: "1000000",
Sats: 1000,
Destination: invoiceDestination,
PaymentHash: invoiceWithAmountPaymentHash,
Description: "",
},
},
{
// "viva peron"
name: "invoice with description",
args: args{
invoice: invoiceWithDescription,
network: network,
},
want: &Invoice{
RawInvoice: invoiceWithDescription,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoiceWithDescriptionPaymentHash,
Description: "viva peron",
},
},
{
// addr bcrt1qhv0a0uhrt2crdehgfge8e8e6texw3q4has8jh7
name: "invoice with fallback address",
args: args{
invoice: invoiceWithFallbackAdrr,
network: network,
},
want: &Invoice{
RawInvoice: invoiceWithFallbackAdrr,
FallbackAddress: fallbackAddr,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoiceWithFallbackAddrPaymentHash,
Description: "",
},
},
{
name: "invoice with invalid fallback address",
args: args{
invoice: "lnbcrt1pwtpduxpp57xglq4thtrerzzxt8wzg4wresfclewh8pk8xghahwq8kgek3qslqdqqcqzysfppqhv0a0uhrt2crdehgfge8e8e6texw3q4hpmge888yuu6076utcrhgc97wu7vydmudyagkz25ahuyp4fqrc9e945ff248cpa3krn7vvgcqq6spyuqltd245sjvwh23gz220cegadspkn3lx0",
network: Mainnet(),
},
wantErr: true,
},
{
name: "malformed invoice",
args: args{
invoice: "asdasd",
network: network,
},
wantErr: true,
},
{
name: "simple invoice with muun scheme",
args: args{
invoice: muunScheme + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
name: "simple invoice with muun:// scheme",
args: args{
invoice: muunScheme + "//" + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInvoice(tt.args.invoice, tt.args.network)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInvoice() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil {
// expiry is relative to now, so ignore it
got.Expiry = 0
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseInvoice() = %v, want %v", got, tt.want)
}
})
}
}

276
libwallet/invoices_test.go Normal file
View File

@ -0,0 +1,276 @@
package libwallet
import (
"bytes"
"encoding/hex"
"encoding/json"
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
)
func TestInvoiceSecrets(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
routeHints := &RouteHints{
Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869fd",
FeeBaseMsat: 1000,
FeeProportionalMillionths: 1000,
CltvExpiryDelta: 8,
}
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
t.Fatal(err)
}
if secrets.Length() != 5 {
t.Fatalf("expected 5 new secrets, got %d", secrets.Length())
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
t.Fatal(err)
}
t.Run("generating more invoices", func(t *testing.T) {
// Make sure the secrets list is already topped up
_, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
t.Fatal(err)
}
// try to generate more secrets
moreSecrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
t.Fatal(err)
}
if moreSecrets.Length() != 0 {
t.Fatal("expected no new secrets to be created")
}
})
t.Run("create an invoice", func(t *testing.T) {
builder := &InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(routeHints)
builder.AmountSat(1000)
builder.Description("hello world")
invoice, err := builder.Build()
if err != nil {
t.Fatal(err)
}
if invoice == "" {
t.Fatal("expected non-empty invoice string")
}
payreq, err := zpay32.Decode(invoice, network.network)
if err != nil {
t.Fatal(err)
}
if !payreq.Features.HasFeature(lnwire.TLVOnionPayloadOptional) {
t.Fatal("expected invoice to have var onion optin feature")
}
if !payreq.Features.HasFeature(lnwire.PaymentAddrOptional) {
t.Fatal("expected invoice to have payment secret feature")
}
if payreq.MilliSat.ToSatoshis() != btcutil.Amount(1000) {
t.Fatalf("expected invoice amount to be 1000 sats, got %v", payreq.MilliSat)
}
if payreq.Description == nil || *payreq.Description != "hello world" {
t.Fatalf("expected payment description to match, got %v", payreq.Description)
}
if payreq.MinFinalCLTVExpiry() != 72 {
t.Fatalf("expected min final CLTV expiry to be 72, got %v", payreq.MinFinalCLTVExpiry())
}
if payreq.PaymentAddr == nil {
t.Fatalf("expected payment addr to be non-nil")
}
if len(payreq.RouteHints) == 0 {
t.Fatalf("expected invoice to contain route hints")
}
hopHints := payreq.RouteHints[0]
if len(hopHints) != 1 {
t.Fatalf("expected invoice route hints to contain exactly 1 hop hint")
}
if hopHints[0].ChannelID&(1<<63) == 0 {
t.Fatal("invalid short channel id in hophints")
}
if hopHints[0].FeeBaseMSat != 1000 {
t.Fatalf("expected fee base to be 1000 msat, got %v instead", hopHints[0].FeeBaseMSat)
}
if hopHints[0].FeeProportionalMillionths != 1000 {
t.Fatalf("expected fee proportional millionths to be 1000, got %v instead", hopHints[0].FeeProportionalMillionths)
}
if hopHints[0].CLTVExpiryDelta != 8 {
t.Fatalf("expected CLTV expiry delta to be 8, got %v instead", hopHints[0].CLTVExpiryDelta)
}
metadata, err := GetInvoiceMetadata(payreq.PaymentHash[:])
if err != nil {
t.Fatalf("expected invoice to contain metadata, got error: %v", err)
}
decryptedMetadata, err := userKey.Decrypter().Decrypt(metadata)
if err != nil {
t.Fatalf("expected metadata to decrypt correctly, got error: %v", err)
}
var jsonMetadata OperationMetadata
err = json.NewDecoder(bytes.NewReader(decryptedMetadata)).Decode(&jsonMetadata)
if err != nil {
t.Fatal("expected metadata to parse correctly")
}
if jsonMetadata.Invoice == "" {
t.Fatal("expected metadata to contain a non-empty invoice")
}
})
t.Run("creating a 2nd invoice returns a different payment hash", func(t *testing.T) {
builder := &InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(routeHints)
invoice1, err := builder.Build()
if err != nil {
t.Fatal(err)
}
payreq1, err := zpay32.Decode(invoice1, network.network)
if err != nil {
t.Fatal(err)
}
invoice2, err := builder.Build()
if err != nil {
t.Fatal(err)
}
payreq2, err := zpay32.Decode(invoice2, network.network)
if err != nil {
t.Fatal(err)
}
if payreq1.PaymentHash == payreq2.PaymentHash {
t.Fatal("successive invoice payment hashes should be different")
}
})
t.Run("amountMsat gets stored", func(t *testing.T) {
builder := &InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(routeHints)
builder.AmountMSat(1001)
invoice3, err := builder.Build()
if err != nil {
t.Fatal(err)
}
payreq3, err := zpay32.Decode(invoice3, network.network)
if err != nil {
t.Fatal(err)
}
db, err := openDB()
if err != nil {
t.Fatal(err)
}
defer db.Close()
invoiceMetadata, err := db.FindByPaymentHash(payreq3.PaymentHash[:])
if err != nil {
t.Fatal(err)
}
// Note that we sent 1001 msats
if invoiceMetadata.AmountSat != 1 {
t.Fatalf("Expected persisted amount to 1 found %v", invoiceMetadata.AmountSat)
}
})
t.Run("two route hints are encoded", func(t *testing.T) {
builder := &InvoiceBuilder{}
invoice, err := builder.
Network(network).
UserKey(userKey).
AddRouteHints(routeHints).
AddRouteHints(&RouteHints{
Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869ff",
FeeBaseMsat: 123,
FeeProportionalMillionths: 1,
CltvExpiryDelta: 23,
}).
Build()
if err != nil {
t.Fatal(err)
}
payreq, err := zpay32.Decode(invoice, network.network)
if err != nil {
t.Fatal(err)
}
if len(payreq.RouteHints) != 2 {
t.Fatalf("Expected there to be 2 route hints, found %v", len(payreq.RouteHints))
}
for i, hops := range payreq.RouteHints {
if len(hops) != 1 {
t.Fatalf(
"Expected hops for hint %v to be 1, found %v",
i,
len(hops),
)
}
hint := hops[0]
var expectedFeeBase, expectedProportional uint32
var expectedPubKey string
if hint.CLTVExpiryDelta == 23 {
// Second hint
expectedFeeBase = 123
expectedProportional = 1
expectedPubKey = "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869ff"
} else if hint.CLTVExpiryDelta == uint16(routeHints.CltvExpiryDelta) {
// First hint
expectedFeeBase = uint32(routeHints.FeeBaseMsat)
expectedProportional = uint32(routeHints.FeeProportionalMillionths)
expectedPubKey = routeHints.Pubkey
} else {
t.Fatalf("Failed to match route hint %v: %v", i, hops)
}
if hint.ChannelID&(1<<63) == 0 {
t.Fatal("invalid short channel id in hophints")
}
if hint.FeeProportionalMillionths != expectedProportional {
t.Fatalf("Route hint %v proportional fee %v != %v", i, hint.FeeProportionalMillionths, expectedProportional)
}
if hint.FeeBaseMSat != expectedFeeBase {
t.Fatalf("Route hint %v base fee %v != %v", i, hint.FeeBaseMSat, expectedFeeBase)
}
pubKey := hex.EncodeToString(hint.NodeID.SerializeCompressed())
if pubKey != expectedPubKey {
t.Fatalf("Route hint %v pub key %v != %v", i, pubKey, expectedPubKey)
}
}
})
}
func TestGetInvoiceMetadataMissingHash(t *testing.T) {
setup()
_, err := GetInvoiceMetadata(randomBytes(32))
if err == nil {
t.Fatal("expected GetInvoiceMetadata to fail")
}
}

View File

@ -0,0 +1,87 @@
package keycrypt
import (
"bytes"
"os"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
)
var (
key *hdkeychain.ExtendedKey // set by TestMain
path = "m/123'/1"
passphrase = "asdasdasd"
)
func TestEncrypt(t *testing.T) {
_, err := Encrypt(key, path, passphrase)
if err != nil {
t.Errorf("Encrypt() error = %v", err)
return
}
}
func TestEncryptDecrypt(t *testing.T) {
encrypted, err := Encrypt(key, path, passphrase)
if err != nil {
t.Fatalf("Encrypt() error = %v", err)
}
decryptedKey, decryptedPath, err := Decrypt(encrypted, passphrase)
if err != nil {
t.Fatalf("Encrypt() error = %v", err)
}
if decryptedKey.String() != key.String() {
t.Errorf("Encrypt() expected key %v got %v", key.String(), decryptedKey.String())
}
if decryptedPath != path {
t.Errorf("Encrypt() expected path %v got %v", path, decryptedPath)
}
}
func TestBadPassphrase(t *testing.T) {
encrypted, err := Encrypt(key, path, passphrase)
if err != nil {
t.Fatalf("Encrypt() error = %v", err)
}
_, _, err = Decrypt(encrypted, passphrase+"foo")
if err == nil {
t.Fatalf("expected decryption error")
}
}
func TestEncodeUTF16(t *testing.T) {
tests := []struct {
name string
input string
want []byte
}{
{name: "no data", input: "", want: nil},
{name: "one char", input: "a", want: []byte{0, 97}},
{name: "multi byte char", input: "€", want: []byte{0x20, 0xAC}},
{name: "complex string", input: "€aह", want: []byte{0x20, 0xAC, 0, 97, 0x09, 0x39}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := encodeUTF16(tt.input); !bytes.Equal(got, tt.want) {
t.Errorf("EncodeString() = %v, want %v", got, tt.want)
}
})
}
}
func TestMain(m *testing.M) {
var err error
key, err = hdkeychain.NewMaster(randomBytes(32), &chaincfg.MainNetParams)
if err != nil {
panic(err)
}
os.Exit(m.Run())
}

54
libwallet/keycrypter_test.go Executable file
View File

@ -0,0 +1,54 @@
package libwallet
import (
"testing"
)
func TestKeyCrypter(t *testing.T) {
key, _ := NewHDPrivateKey(randomBytes(16), Regtest())
key, _ = key.DeriveTo("m/123'/1")
testPassphrase := "asdasdasd"
t.Run("simple encrypt", func(t *testing.T) {
_, err := KeyEncrypt(key, testPassphrase)
if err != nil {
t.Errorf("KeyEncrypt() error = %v", err)
return
}
})
t.Run("encrypt & decrypt", func(t *testing.T) {
encrypted, err := KeyEncrypt(key, testPassphrase)
if err != nil {
t.Fatalf("KeyEncrypt() error = %v", err)
}
decrypted, err := KeyDecrypt(encrypted, testPassphrase, Regtest())
if err != nil {
t.Fatalf("KeyEncrypt() error = %v", err)
}
if decrypted.Key.String() != key.String() {
t.Errorf("KeyEncrypt() expected key %v got %v", key, decrypted.Key)
}
if decrypted.Path != key.Path {
t.Errorf("KeyEncrypt() expected path %v got %v", key.Path, decrypted.Path)
}
})
t.Run("bad passphrase", func(t *testing.T) {
encrypted, err := KeyEncrypt(key, testPassphrase)
if err != nil {
t.Fatalf("KeyEncrypt() error = %v", err)
}
_, err = KeyDecrypt(encrypted, testPassphrase+"foo", Regtest())
if err == nil {
t.Fatalf("expected decryption error")
}
})
}

View File

@ -0,0 +1,771 @@
package lnurl
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/fiatjaf/go-lnurl"
"github.com/lightningnetwork/lnd/lnwire"
)
func TestWithdraw(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestWithdrawWithCompatibilityTag(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdraw",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestDecodeError(t *testing.T) {
qr := "lightning:abcde"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrDecode {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestWrongTagError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/channelRequest", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
Tag: "channelRequest",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/channelRequest", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrWrongTag {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestUnreachableError(t *testing.T) {
originalTimeout := httpClient.Timeout
httpClient.Timeout = 1 * time.Second
defer func() {
httpClient.Timeout = originalTimeout
}()
// LNURL QR pointing to a non-responding domain
qr := "LIGHTNING:LNURL1DP68GURN8GHJ7ARGD9EJUER0D4SKJM3WV3HK2UEWDEHHGTN90P5HXAPWV4UXZMTSD3JJUCM0D5LHXETRWFJHG0F3XGENGDGQ8EH52"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrUnreachable {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestServiceError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusError,
Reason: "something something",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusReceiving {
t.Fatal("should not reach receiving status")
}
})
}
func TestInvalidResponseError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foobar"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestUnsafeURLError(t *testing.T) {
qr, _ := encode("http://localhost/withdraw")
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, false, func(e *Event) {
if e.Code < 100 && e.Code != ErrUnsafeURL {
t.Fatalf("unexpected error code: %v", e.Code)
}
})
}
func TestWrongTagInQR(t *testing.T) {
// LNURL QR with a `login` tag value in its query params
qr := "lightning:lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhgct884kx7emfdcnxkvfa8qexxc35vymnxcf5xumkxvfsv4snxwph8qunzv3hxesnyv3jvv6nyv3e8yuxzvnpv4skvepnxg6rwv34xqck2c3sxcerzdpnv56r2dss2vt96"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrWrongTag {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestOnionLinkNotSupported(t *testing.T) {
qr := "LNURL1DP68GUP69UHKVMM0VFSHYTN0DE5K7MSHXU8YD"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrTorNotSupported {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestExpiredCheck(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: "ERROR",
Reason: "something something Expired blabla",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrRequestExpired {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestNoAvailableBalance(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 0,
MinWithdrawable: 0,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrNoAvailableBalance {
t.Fatalf("unexpected error code: %d", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatalf("should not create invoice")
}
})
}
func TestNoRouteCheck(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusError,
Reason: "Unable to pay LN Invoice: FAILURE_REASON_NO_ROUTE",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrNoRoute {
t.Fatalf("unexpected error code: %d", e.Code)
}
})
}
func TestExtraQueryParams(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete?foo=bar",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("foo") != "bar" {
t.Fatalf("Expected foo=bar in query params. Got URL: %v", r.URL.String())
}
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestStringlyTypedNumberFields(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&struct {
Response
Tag string `json:"tag"`
K1 string `json:"k1"`
Callback string `json:"callback"`
MaxWithdrawable string `json:"maxWithdrawable"`
MinWithdrawable string `json:"minWithdrawable"`
DefaultDescription string `json:"defaultDescription"`
}{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: "1000000",
MinWithdrawable: "0",
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestErrorContainsResponseBody(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Write([]byte("this is a custom error response"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if !strings.Contains(e.Message, "this is a custom error response") {
t.Fatalf("expected error message to contain response, got `%s`", e.Message)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestErrorContainsResponseBodyForFinishRequest(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Write([]byte("this is a custom error response"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if !strings.Contains(e.Message, "this is a custom error response") {
t.Fatalf("expected error message to contain response, got `%s`", e.Message)
}
}
if e.Code == StatusReceiving {
t.Fatal("should not reach receiving status")
}
})
}
func TestForbidden(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write([]byte("Forbidden"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrForbidden {
t.Fatalf("unexpected error code: %v", e.Code)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestZebedee403MapsToCountryNotSupported(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write([]byte("Forbidden"))
})
server := httptest.NewServer(mux)
defer server.Close()
// Super ugly hack to emulate that local endpoint is zebedee
zebedeeHost = "127.0.0.1"
t.Cleanup(func() {
zebedeeHost = zebedeeHostConst // after test reset to its original value
})
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrCountryNotSupported {
t.Fatalf("unexpected error code: %v", e.Code)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func encode(url string) (string, error) {
return lnurl.LNURLEncode(url)
}
func TestWithdrawResponse_Validate(t *testing.T) {
type fields struct {
Response Response
Tag string
K1 string
Callback string
MaxWithdrawable stringOrNumber
MinWithdrawable stringOrNumber
DefaultDescription string
}
errorResponse := func(reason string) fields {
return fields{
Response: Response{
Status: StatusError,
Reason: reason,
},
}
}
tests := []struct {
name string
fields fields
want int
}{
{
"invalid tag",
fields{Tag: "blebidy"},
ErrWrongTag,
},
{
"negative withdraw",
fields{MaxWithdrawable: -1, Tag: "withdraw"},
ErrNoAvailableBalance,
},
{
"valid",
fields{
Response: Response{
Status: StatusOK,
},
Tag: "withdraw",
MaxWithdrawable: 10,
},
ErrNone,
},
{
"already being processed",
errorResponse("This Withdrawal Request is already being processed by another wallet"),
ErrAlreadyUsed,
},
{
"can only be processed only once",
errorResponse("This Withdrawal Request can only be processed once"),
ErrAlreadyUsed,
},
{
"withdraw is spent",
errorResponse("Withdraw is spent"),
ErrAlreadyUsed,
},
{
"withdraw link is empty",
errorResponse("Withdraw link is empty"),
ErrAlreadyUsed,
},
{
"has already been used",
errorResponse("This LNURL has already been used"),
ErrAlreadyUsed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wr := &WithdrawResponse{
Response: tt.fields.Response,
Tag: tt.fields.Tag,
K1: tt.fields.K1,
Callback: tt.fields.Callback,
MaxWithdrawable: tt.fields.MaxWithdrawable,
MinWithdrawable: tt.fields.MinWithdrawable,
DefaultDescription: tt.fields.DefaultDescription,
}
got, _ := wr.Validate()
if got != tt.want {
t.Errorf("Validate() got = %v, want %v", got, tt.want)
}
})
}
}
func TestValidate(t *testing.T) {
type args struct {
qr string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "plain",
args: args{"LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "lightning scheme",
args: args{"lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "HTTP fallback scheme",
args: args{"https://example.com/?lightning=LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun scheme",
args: args{"muun:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun scheme with double slashes",
args: args{"muun://LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "lightning scheme with double slashes",
args: args{"lightning://LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun + lightning schemes",
args: args{"muun:lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun + lightning schemes with double slashes",
args: args{"muun://lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Validate(tt.args.qr); got != tt.want {
t.Errorf("Validate() = %v, want %v", got, tt.want)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More