From ba9a3201246297dec07b4f90bb97f826bab263b1 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 12 Jun 2024 13:00:09 +0900 Subject: [PATCH 1/9] internal: Add libsecp256k1 c library. Add a c library that has some primitive cryptographic functions needed for working with adaptor signatures. --- .gitignore | 1 + internal/libsecp256k1/README.md | 10 + internal/libsecp256k1/build.sh | 10 + internal/libsecp256k1/libsecp256k1.go | 149 ++++++++++++++ internal/libsecp256k1/libsecp256k1_test.go | 215 +++++++++++++++++++++ internal/libsecp256k1/secp256k1 | 1 + run_tests.sh | 6 + 7 files changed, 392 insertions(+) create mode 100644 internal/libsecp256k1/README.md create mode 100755 internal/libsecp256k1/build.sh create mode 100644 internal/libsecp256k1/libsecp256k1.go create mode 100644 internal/libsecp256k1/libsecp256k1_test.go create mode 160000 internal/libsecp256k1/secp256k1 diff --git a/.gitignore b/.gitignore index 0e82613dfd..c3460f2bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ client/cmd/translationsreport/translationsreport client/cmd/translationsreport/worksheets server/cmd/dexadm/dexadm server/cmd/geogame/geogame +internal/libsecp256k1/secp256k1 diff --git a/internal/libsecp256k1/README.md b/internal/libsecp256k1/README.md new file mode 100644 index 0000000000..5112e5c0f6 --- /dev/null +++ b/internal/libsecp256k1/README.md @@ -0,0 +1,10 @@ +### Package libsecp256k1 + +Package libsecp256k1 includes some primative cryptographic functions needed for +working with adaptor signatures that are not currently found in golang. This imports +code from https://github.com/tecnovert/secp256k1 and uses that with cgo. Both +that library and this package are in an experimental stage. + +### Usage + +Run the `build.sh` script. Currently untested on mac and will not work on Windows. diff --git a/internal/libsecp256k1/build.sh b/internal/libsecp256k1/build.sh new file mode 100755 index 0000000000..b19343f0c7 --- /dev/null +++ b/internal/libsecp256k1/build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +rm -fr secp256k1 +git clone https://github.com/tecnovert/secp256k1 -b anonswap_v0.2 + +cd secp256k1 +./autogen.sh +./configure --enable-module-dleag --enable-experimental --enable-module-generator --enable-module-ed25519 --enable-module-recovery --enable-module-ecdsaotves +make +cd .. diff --git a/internal/libsecp256k1/libsecp256k1.go b/internal/libsecp256k1/libsecp256k1.go new file mode 100644 index 0000000000..637435cc79 --- /dev/null +++ b/internal/libsecp256k1/libsecp256k1.go @@ -0,0 +1,149 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package libsecp256k1 + +/* +#cgo CFLAGS: -g -Wall +#cgo LDFLAGS: -L. -l:secp256k1/.libs/libsecp256k1.a +#include "secp256k1/include/secp256k1_dleag.h" +#include "secp256k1/include/secp256k1_ecdsaotves.h" +#include + +secp256k1_context* _ctx() { + return secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); +} +*/ +import "C" +import ( + "errors" + "unsafe" + + "decred.org/dcrdex/dex/encode" + "github.com/decred/dcrd/dcrec/edwards/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +const ( + ProofLen = 48893 + CTLen = 196 + maxSigLen = 72 // The actual size is variable. +) + +// Ed25519DleagProve creates a proof for checking a discrete logarithm is equal +// across the secp256k1 and ed25519 curves. +func Ed25519DleagProve(privKey *edwards.PrivateKey) (proof [ProofLen]byte, err error) { + secpCtx := C._ctx() + defer C.free(unsafe.Pointer(secpCtx)) + nonce := [32]byte{} + copy(nonce[:], encode.RandomBytes(32)) + key := [32]byte{} + copy(key[:], privKey.Serialize()) + n := (*C.uchar)(unsafe.Pointer(&nonce)) + k := (*C.uchar)(unsafe.Pointer(&key)) + nBits := uint64(252) + nb := (*C.ulong)(unsafe.Pointer(&nBits)) + plen := C.ulong(ProofLen) + p := (*C.uchar)(unsafe.Pointer(&proof)) + res := C.secp256k1_ed25519_dleag_prove(secpCtx, p, &plen, k, *nb, n) + if int(res) != 1 { + return [ProofLen]byte{}, errors.New("C.secp256k1_ed25519_dleag_prove exited with error") + } + return proof, nil +} + +// Ed25519DleagVerify verifies that a descrete logarithm is equal across the +// secp256k1 and ed25519 curves. +func Ed25519DleagVerify(proof [ProofLen]byte) bool { + secpCtx := C._ctx() + defer C.free(unsafe.Pointer(secpCtx)) + pl := C.ulong(ProofLen) + p := (*C.uchar)(unsafe.Pointer(&proof)) + res := C.secp256k1_ed25519_dleag_verify(secpCtx, p, pl) + return res == 1 +} + +// EcdsaotvesEncSign signs the hash and returns an encrypted signature. +func EcdsaotvesEncSign(signPriv *secp256k1.PrivateKey, encPub *secp256k1.PublicKey, hash [32]byte) (cyphertext [CTLen]byte, err error) { + secpCtx := C._ctx() + defer C.free(unsafe.Pointer(secpCtx)) + privBytes := [32]byte{} + copy(privBytes[:], signPriv.Serialize()) + priv := (*C.uchar)(unsafe.Pointer(&privBytes)) + pubBytes := [33]byte{} + copy(pubBytes[:], encPub.SerializeCompressed()) + pub := (*C.uchar)(unsafe.Pointer(&pubBytes)) + h := (*C.uchar)(unsafe.Pointer(&hash)) + s := (*C.uchar)(unsafe.Pointer(&cyphertext)) + res := C.ecdsaotves_enc_sign(secpCtx, s, priv, pub, h) + if int(res) != 1 { + return [CTLen]byte{}, errors.New("C.ecdsaotves_enc_sign exited with error") + } + return cyphertext, nil +} + +// EcdsaotvesEncVerify verifies the encrypted signature. +func EcdsaotvesEncVerify(signPub, encPub *secp256k1.PublicKey, hash [32]byte, cyphertext [CTLen]byte) bool { + secpCtx := C._ctx() + defer C.free(unsafe.Pointer(secpCtx)) + signBytes := [33]byte{} + copy(signBytes[:], signPub.SerializeCompressed()) + sp := (*C.uchar)(unsafe.Pointer(&signBytes)) + encBytes := [33]byte{} + copy(encBytes[:], encPub.SerializeCompressed()) + ep := (*C.uchar)(unsafe.Pointer(&encBytes)) + h := (*C.uchar)(unsafe.Pointer(&hash)) + c := (*C.uchar)(unsafe.Pointer(&cyphertext)) + res := C.ecdsaotves_enc_verify(secpCtx, sp, ep, h, c) + return res == 1 +} + +// EcdsaotvesDecSig retrieves the signature. +func EcdsaotvesDecSig(encPriv *secp256k1.PrivateKey, cyphertext [CTLen]byte) ([]byte, error) { + secpCtx := C._ctx() + defer C.free(unsafe.Pointer(secpCtx)) + encBytes := [32]byte{} + copy(encBytes[:], encPriv.Serialize()) + ep := (*C.uchar)(unsafe.Pointer(&encBytes)) + ct := (*C.uchar)(unsafe.Pointer(&cyphertext)) + var sig [maxSigLen]byte + s := (*C.uchar)(unsafe.Pointer(&sig)) + slen := C.ulong(maxSigLen) + res := C.ecdsaotves_dec_sig(secpCtx, s, &slen, ep, ct) + if int(res) != 1 { + return nil, errors.New("C.ecdsaotves_dec_sig exited with error") + } + sigCopy := make([]byte, maxSigLen) + copy(sigCopy, sig[:]) + // Remove trailing zeros. + for i := maxSigLen - 1; i >= 0; i-- { + if sigCopy[i] != 0 { + break + } + sigCopy = sigCopy[:i] + } + return sigCopy, nil +} + +// EcdsaotvesRecEncKey retrieves the encoded private key from signature and +// cyphertext. +func EcdsaotvesRecEncKey(encPub *secp256k1.PublicKey, cyphertext [CTLen]byte, sig []byte) (encPriv *secp256k1.PrivateKey, err error) { + secpCtx := C._ctx() + defer C.free(unsafe.Pointer(secpCtx)) + pubBytes := [33]byte{} + copy(pubBytes[:], encPub.SerializeCompressed()) + ep := (*C.uchar)(unsafe.Pointer(&pubBytes)) + ct := (*C.uchar)(unsafe.Pointer(&cyphertext)) + sigCopy := [maxSigLen]byte{} + copy(sigCopy[:], sig) + s := (*C.uchar)(unsafe.Pointer(&sigCopy)) + varSigLen := len(sig) + slen := C.ulong(varSigLen) + pkBytes := [32]byte{} + pk := (*C.uchar)(unsafe.Pointer(&pkBytes)) + res := C.ecdsaotves_rec_enc_key(secpCtx, pk, ep, ct, s, slen) + if int(res) != 1 { + return nil, errors.New("C.ecdsaotves_rec_enc_key exited with error") + } + return secp256k1.PrivKeyFromBytes(pkBytes[:]), nil +} diff --git a/internal/libsecp256k1/libsecp256k1_test.go b/internal/libsecp256k1/libsecp256k1_test.go new file mode 100644 index 0000000000..fd16d6e97f --- /dev/null +++ b/internal/libsecp256k1/libsecp256k1_test.go @@ -0,0 +1,215 @@ +//go:build libsecp256k1 + +package libsecp256k1 + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/decred/dcrd/dcrec/edwards/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +func randBytes(n int) []byte { + b := make([]byte, n) + rand.Read(b) + return b +} + +func TestEd25519DleagProve(t *testing.T) { + tests := []struct { + name string + }{{ + name: "ok", + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pk, err := edwards.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + sPk := secp256k1.PrivKeyFromBytes(pk.Serialize()) + proof, err := Ed25519DleagProve(pk) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(sPk.PubKey().SerializeCompressed(), proof[:33]) { + t.Fatal("first 33 bytes of proof not equal to secp256k1 pubkey") + } + }) + } +} + +func TestEd25519DleagVerify(t *testing.T) { + pk, err := edwards.GeneratePrivateKey() + if err != nil { + panic(err) + } + proof, err := Ed25519DleagProve(pk) + if err != nil { + panic(err) + } + tests := []struct { + name string + proof [ProofLen]byte + ok bool + }{{ + name: "ok", + proof: proof, + ok: true, + }, { + name: "bad proof", + proof: func() (p [ProofLen]byte) { + copy(p[:], proof[:]) + p[0] ^= p[0] + return p + }(), + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ok := Ed25519DleagVerify(test.proof) + if ok != test.ok { + t.Fatalf("want %v but got %v", test.ok, ok) + } + }) + } +} + +func TestEcdsaotvesEncSign(t *testing.T) { + tests := []struct { + name string + }{{ + name: "ok", + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + signPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + encPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + h := randBytes(32) + var hash [32]byte + copy(hash[:], h) + _, err = EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestEcdsaotvesEncVerify(t *testing.T) { + signPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + encPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + h := randBytes(32) + var hash [32]byte + copy(hash[:], h) + ct, err := EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + ok bool + ct [196]byte + }{{ + name: "ok", + ct: ct, + ok: true, + }, { + name: "bad sig", + ct: func() (c [CTLen]byte) { + copy(c[:], ct[:]) + c[0] ^= c[0] + return c + }(), + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ok := EcdsaotvesEncVerify(signPk.PubKey(), encPk.PubKey(), hash, test.ct) + if ok != test.ok { + t.Fatalf("want %v but got %v", test.ok, ok) + } + }) + } +} + +func TestEcdsaotvesDecSig(t *testing.T) { + signPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + encPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + h := randBytes(32) + var hash [32]byte + copy(hash[:], h) + ct, err := EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + }{{ + name: "ok", + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := EcdsaotvesDecSig(encPk, ct) + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestEcdsaotvesRecEncKey(t *testing.T) { + signPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + encPk, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + h := randBytes(32) + var hash [32]byte + copy(hash[:], h) + ct, err := EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) + if err != nil { + t.Fatal(err) + } + sig, err := EcdsaotvesDecSig(encPk, ct) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + }{{ + name: "ok", + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pk, err := EcdsaotvesRecEncKey(encPk.PubKey(), ct, sig) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(pk.Serialize(), encPk.Serialize()) { + t.Fatal("private keys not equal") + } + }) + } +} diff --git a/internal/libsecp256k1/secp256k1 b/internal/libsecp256k1/secp256k1 new file mode 160000 index 0000000000..e3ebcd782a --- /dev/null +++ b/internal/libsecp256k1/secp256k1 @@ -0,0 +1 @@ +Subproject commit e3ebcd782a604f228784b10c50ffa099d9796720 diff --git a/run_tests.sh b/run_tests.sh index fa1923bb57..1ec7227cab 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -9,6 +9,12 @@ echo "Go version: $GV" # Ensure html templates pass localization. go generate -x ./client/webserver/site # no -write +cd ./internal/libsecp256k1 +./build.sh +go test -race -tags libsecp256k1 + +cd "$dir" + # list of all modules to test modules=". /dex/testing/loadbot /client/cmd/bisonw-desktop" From 9001625b9502ddca0c0da6005be2fb19c1171fd2 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 22 May 2024 22:44:34 +0800 Subject: [PATCH 2/9] harness,doc: first commit --- dex/testing/xmr/README.md | 239 +++++++++++ dex/testing/xmr/harness.sh | 601 +++++++++++++++++++++++++++ dex/testing/xmr/image-1.png | Bin 0 -> 43382 bytes dex/testing/xmr/image-2.png | Bin 0 -> 30440 bytes dex/testing/xmr/monero_functions.inc | 394 ++++++++++++++++++ 5 files changed, 1234 insertions(+) create mode 100644 dex/testing/xmr/README.md create mode 100755 dex/testing/xmr/harness.sh create mode 100644 dex/testing/xmr/image-1.png create mode 100644 dex/testing/xmr/image-2.png create mode 100644 dex/testing/xmr/monero_functions.inc diff --git a/dex/testing/xmr/README.md b/dex/testing/xmr/README.md new file mode 100644 index 0000000000..42ba87663f --- /dev/null +++ b/dex/testing/xmr/README.md @@ -0,0 +1,239 @@ +# Monero Development Harness + +Monero development Harness - documentation and notes + +## Useful Info + +Monero is very different than btc in that it does not provide a single rpc +tool like `bitcoin-cli` but rather a set of json 2.0 & other older json apis +which can be accessed by sending curl requests for each one. + +Monero wallets are accounts based + + + +## Architecture + +![alt text](image-1.png) + +Embedded in Tmux + +- **alpha** is a monero p2p daemon + +- **bill** is a miner wallet .. you can top up more funds from bill if you run out + +- **fred** is a normal wallet user + +- **charlie** is a normal wallet user + +- **charlie_view** is a view-only wallet sibling of charlie full wallet - no spend key + +## Using + +### Prerequisites + +- **monero-x86_64-linux-gnu-v0.18.3.3** or later +- **linux** (tested on Ubuntu 22.04) +- **jq** (1.6) + +### Setup + +**monero-x86_64-linux-gnu-v0.18.3.3** should be in PATH + +`export PATH=$PATH:[path-to]/monero-x86_64-linux-gnu-v0.18.3.3` + +### Background Mining + +By default background mining is set up and mines to bill wallet every 15s + +To disable: + +`export NOMINER="1" to your shell` + + or else invoke the harness with: + +`NOMINER="1" ./harness.sh` + +You should disable if attempting the manual offline cold signing using `monero-wallet-cli` + +You should also ctl-C charlie & charlie_view wallets for this + +### Run + +`./harness.sh` + +### Data Directory + +![alt text](image-2.png) + +### Known Issues + +The transaction spend locking needs more investigation + +### Commands Help + +run `./help` from the tmux harness window 0 + +```text + +Commands Help: +-------------- +alpha_get_transactions + +- get transaction details for one or more txid +- inputs: + - tx_hashes - hash1,hash2,hash3,... + - decode_as_json - Optional - returns more detail but in a escaped raw json format + +alpha_info + +- get running daemon details - height, etc. +- inputs: None + +alpha_sendrawtransaction + +- broadcast a previously built signed tx +- inputs: + - tx_as_hex string - can be generated with charlie_build_tx or fred_build_tx + +alpha_transaction_pool + +- get mempool details +- inputs: None + +mine-to-bill + +- generate 1 or more blocks to bill wallet +- inputs: + - num_blocks - defaults to 1 + +bill_balance + +- get bill wallet balance details +- inputs: None + +bill_refresh_wallet + +- update bill's wallet from the daemon latest info +- inputs: None + +bill_transfer_to + +- build, sign and broadcast a transaction from bill wallet to another address +- inputs: + - amount in in atomic units 1e12 - e.g. 1230000000000 echo = 1.23 XMR + - address - recipient primary address - account index 0, subaddr_indeces [0] + - unlock_time - unlock after n blocks and make spendable - defaults to 0 (no lock) + +charlie_balance + +- get charlie wallet balance details +- inputs: None + +charlie_refresh_wallet + +- update charlie's wallet from the daemon latest info +- inputs: None + +charlie_build_tx + +- build a signed tx for later broadcasting using alpha_send +- inputs: + - amount in in atomic units 1e12 - e.g. 1230000000000 = 1.23 XMR + - address - recipient primary address - account index 0, subaddr_indeces [0] + - unlock_time - unlock after n blocks and make spendable - defaults to 0 (no lock) +-outputs: + - signed tx_blob + - tx_hash + +charlie_incoming_transfers + +- get a list of incoming mined transfers to charlie wallet +- inputs: None + +charlie_transfer_to + +- build, sign and broadcast a transaction from charlie wallet to another address +- inputs + - amount in in atomic units 1e12 - e.g. 1230000000000 echo = 1.23 XMR + - address - recipient primary address - account index 0, subaddr_indeces [0] + - unlock_time - unlock after n blocks and make spendable - defaults to 0 (no lock) + +fred_export_outputs + +- export fred outputs hex +- input: + - all - defaults to true - otherwise only new outputs since the last call + +charlie_export_outputs + +- export charlie outputs hex +- input: + - all - defaults to true - otherwise only new outputs since the last call + +charlie_view_export_outputs + +- export charlie_view outputs hex - charlie_view knows the outputs but has no spend key +- inputs: None +- only useful in offline, cold signing process using monero-wallet-cli interactive tool + must be hex decoded into a file to use in monero-wallet-cli + +fred_export_key_images + +- export signed key images from fred wallet - an array of key images and ephemeral signatures +- input: + - all - defaults to true - otherwise only new key images since the last call + +charlie_export_key_images + +- export signed key images from charlie wallet - an array of key images and ephemeral signatures +- input: + - all - defaults to true - otherwise only new key images since the last call + +fred_balance + +- get fred wallet balance details +- inputs: None + +fred_refresh_wallet + +- update fred's wallet from the daemon latest info +- inputs: None + +fred_build_tx + +- build a signed tx for later broadcasting using alpha_send +- inputs: + - amount in in atomic units 1e12 - e.g. 1230000000000 = 1.23 XMR + - address - recipient primary address - account index 0, subaddr_indeces [0] + - unlock_time - unlock after n blocks and make spendable - defaults to 0 (no lock) +-outputs: + - signed tx_blob + - tx_hash + +fred_incoming_transfers + +- get a list of incoming mined transfers to fred wallet +- inputs: None + +fred_transfer_to + +- build, sign and broadcast a transaction from bill wallet to another address +- inputs + - amount in in atomic units 1e12 - e.g. 1230000000000 echo = 1.23 XMR + - address - recipient primary address - account index 0, subaddr_indeces [0] + - unlock_time - unlock after n blocks and make spendable - defaults to 0 (no lock) + +wallets + +- wallet details exported to the harness environment - useful for building commands in the harness window 0 + +help + +- this help + +quit + +- shutdown daemons and quit the harness + +``` diff --git a/dex/testing/xmr/harness.sh b/dex/testing/xmr/harness.sh new file mode 100755 index 0000000000..404c5fb60c --- /dev/null +++ b/dex/testing/xmr/harness.sh @@ -0,0 +1,601 @@ +#!/usr/bin/env bash +# Tmux script that sets up an XMR regtest harness with one node 'alpha' and 3 +# wallets 'fred', 'bill' & 'charlie'. Charlie also has a View-Only sibling. + +############################################################################### +# Development +################################################################################ + +# export PATH=$PATH:~/monero-x86_64-linux-gnu-v0.18.3.3 + +################################################################################ +# Monero RPC functions +################################################################################ + +source monero_functions.inc + +################################################################################ +# Start up +################################################################################ + +set -evx + +RPC_USER="user" +RPC_PASS="pass" +WALLET_PASS=abc + +LOCALHOST="127.0.0.1" + +# p2p listen and rpc listen ports for alpha node +export ALPHA_NODE_PORT="18080" +export ALPHA_NODE_RPC_PORT="18081" + +# for multinode - not used for singlenode +export ALPHA_NODE="${LOCALHOST}:${ALPHA_NODE_PORT}" + +# wallet servers' listen rpc ports +export FRED_WALLET_RPC_PORT="28084" +export BILL_WALLET_RPC_PORT="28184" +export CHARLIE_WALLET_RPC_PORT="28284" +export CHARLIE_VIEW_WALLET_RPC_PORT="28384" + +# wallet seeds, passwords & primary addresses +FRED_WALLET_SEED="vibrate fever timber cuffs hunter terminal dilute losing light because nabbing slower royal brunt gnaw vats fishing tipsy toxic vague oscar fudge mice nasty light" +export FRED_WALLET_NAME="fred" +export FRED_WALLET_PASS="" +export FRED_WALLET_PRIMARY_ADDRESS="494aSG3QY1C4PJf7YyDjFc9n2uAuARWSoGQ3hrgPWXtEjgGrYDn2iUw8WJP5Dzm4GuMkY332N9WfbaKfu5tWM3wk8ZeSEC5" + +BILL_WALLET_SEED="zodiac playful artistic friendly ought myriad entrance inroads mural duets enraged furnished tsunami pimple ammo prying january swiftly pulp aunt beer ticket tubes unplugs ammo" +export BILL_WALLET_NAME="bill" +export BILL_WALLET_PASS="" +export BILL_WALLET_PRIMARY_ADDRESS="42xPx5nWhxegefWEzRNoJZWwK7d5ofKoWLG1Gmf8567nJMVR37P1EvqYxqWtfgtYUn8qgSbeAqoLcLKe3seFXV2k5ZSqvQw" + +CHARLIE_WALLET_SEED="tilt equip bikini nylon ardent asylum eight vane gyrate venomous dove vortex aztec maul rash lair elope rover lodge neutral lemon eggs mocked mugged equip" +export CHARLIE_WALLET_NAME="charlie" +export CHARLIE_WALLET_PASS="" +export CHARLIE_WALLET_PRIMARY_ADDRESS="453w1dEoNE1HjKzKVpAU14Honzenqs5VKKQWHb7RuNHLa4ekXhXnGhR6RuttNpvjbtDjzy8pTgz5j4ZSsWQqyxSDBVQ4WCk" +export CHARLIE_WALLET_VIEWKEY="ff3bef320b8268cef410b78c91f34dfc995c72fcb1b498f7a732d76a42a9e207" +export CHARLIE_VIEW_WALLET_NAME="charlie_view" + +# data dir +NODES_ROOT=~/dextest/xmr +FRED_WALLET_DIR="${NODES_ROOT}/wallets/fred" +BILL_WALLET_DIR="${NODES_ROOT}/wallets/bill" +CHARLIE_WALLET_DIR="${NODES_ROOT}/wallets/charlie" +CHARLIE_VIEW_WALLET_DIR="${NODES_ROOT}/wallets/charlie_view" +HARNESS_CTL_DIR="${NODES_ROOT}/harness-ctl" +ALPHA_DATA_DIR="${NODES_ROOT}/alpha" +ALPHA_REGTEST_CFG="${ALPHA_DATA_DIR}/alpha.conf" + +if [ -d "${NODES_ROOT}" ]; then + rm -fR "${NODES_ROOT}" +fi +mkdir -p "${FRED_WALLET_DIR}" +mkdir -p "${BILL_WALLET_DIR}" +mkdir -p "${CHARLIE_WALLET_DIR}" +mkdir -p "${CHARLIE_VIEW_WALLET_DIR}" +mkdir -p "${HARNESS_CTL_DIR}" +mkdir -p "${ALPHA_DATA_DIR}" +touch "${ALPHA_REGTEST_CFG}" # currently empty + +# make available from the harness-ctl dir +cp monero_functions.inc ${HARNESS_CTL_DIR} + +# Background watch mining in window ??? by default: +# 'export NOMINER="1"' or uncomment this line to disable +#NOMINER="1" + +################################################################################ +# Control Scripts +################################################################################ +echo "Writing ctl scripts" + +# Daemon info +cat > "${HARNESS_CTL_DIR}/alpha_info" <_build_tx) +# - do_not_relay to other nodes - defaults to false +cat > "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" </dev/null +EOF +chmod +x "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" +# ----------------------------------------------------------------------------- + +# Get one or more transaction details from monerod +# inputs: +# - txids as hex string - "hash1,hash2,hash3,..." +# - decode_as_json - defaults to false +cat > "${HARNESS_CTL_DIR}/alpha_get_transactions" </dev/null +EOF +chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions" +# ----------------------------------------------------------------------------- + +# Mempool info +cat > "${HARNESS_CTL_DIR}/alpha_transaction_pool" </dev/null +EOF +chmod +x "${HARNESS_CTL_DIR}/alpha_transaction_pool" +# ----------------------------------------------------------------------------- + +# Mine to bill-the-miner +# inputs: +# - number of blocks to mine +cat > "${HARNESS_CTL_DIR}/mine-to-bill" < "${HARNESS_CTL_DIR}/fred_transfer_to" < "${HARNESS_CTL_DIR}/bill_transfer_to" < "${HARNESS_CTL_DIR}/charlie_transfer_to" < "${HARNESS_CTL_DIR}/fred_balance" < "${HARNESS_CTL_DIR}/bill_balance" < "${HARNESS_CTL_DIR}/charlie_balance" < "${HARNESS_CTL_DIR}/fred_refresh_wallet" < "${HARNESS_CTL_DIR}/bill_refresh_wallet" < "${HARNESS_CTL_DIR}/charlie_refresh_wallet" < "${HARNESS_CTL_DIR}/fred_incoming_transfers" < "${HARNESS_CTL_DIR}/charlie_incoming_transfers" < "${HARNESS_CTL_DIR}/fred_export_outputs" < "${HARNESS_CTL_DIR}/charlie_export_outputs" < "${HARNESS_CTL_DIR}/charlie_view_export_outputs" < "${HARNESS_CTL_DIR}/fred_export_key_images" < "${HARNESS_CTL_DIR}/charlie_export_key_images" < "${HARNESS_CTL_DIR}/fred_build_tx" < "${HARNESS_CTL_DIR}/charlie_build_tx" < "${HARNESS_CTL_DIR}/wallets" < "${NODES_ROOT}/harness-ctl/quit" < "${NODES_ROOT}/harness-ctl/help" <3jz4u-dL5LDPdN73pY;_7juxMgQLCq8&^{o zb8`n*D@V5@^mb8T5DRLMq>H(Uo3*3EOR%-QIY`so&cxN^B`3#AWpnG7oa~(3FWLD7 zxj6*6xn8O%TiP2jfFX7XNwa{9r!LwzRYe13rXWRb+eKz+UoefsyUl}~7iP~YE^DlnivJ2?OUpCCZHRn6VKHZ?5` z>Lc*?qNITss>#X8LF|{}p6Y3772MwXVPIhBp17d?9P|}zY+R9^{sQCCBaeRg8^)ZW zp&@KMJZ%q;I%2lR260&TCsif3Wc$&8E1LKNA(CPN??2N$IWqSo%9h;N-0a_Qai4>~ zNsGquib$Q4*)W6jkBnG*0*)CCA-&*Xf)>>ZJz92V$Ps70y-h!b0Im!=W4|f`84zjr z{$KCT|8hXZn$ba^{i%jk-PYUwS3Kk=WasF+}fi0g_GFW3JA2(}I8ruIVasORm)C7su+N~X~?>a^l zMfz_r4G1*+5y0;^qT*kZ>=cXK`pqRhw(oz8S5@nv3{Nxq z`quvMC^GD!7FkmV1))~6CT2IU9jc32-|Cvp0;=Cvmox6tdRDS+>>!X)Flt^iCZAFM zsiVyD+gjTG20?Yml-k*MY#kPD|3Tb-g>;A{G(k;g)&-h&QCGLQQJP!wR>tYmha1D< zGR7DiHJizk10I_h=CzBy)Y#PX8_V_k>!TV!>+)!A-aauwWswR$1szq&Vol4MCgvII zwX&JSdJff%MEBl$-^54C-eBG0So-?rGe;d8cxYAX?Z7-3KF3xQ;M?dUc2Zkt@N6{N zkED;F;Ft5r9(>fI^=8}v8hDS6dwU`o;Vu`a&oeJJ+V5QrZYWjLsvU9(rDH0;zaaD2 z%TqX_bek)jt82GxNpAO6DxYoFayYcmnieD)cTEq4-O=F6GfvdS1V9$1$hC~#guISX zABK%sd-;Hc0v$*$tIKC=Ad|J$>yL$;v zZgAZZ-Wb1<#`_yC;>k;mrIQ%jcM1s$vV1o4$yD0|B-*8`ON*lo$eg$*0(ZCO4fs|! zdtzY2$sw|{UwPQh2Q!BP?PDb|fklPC@!iiFlAhXWvrdw<-^SKWU(5vAW=>W*n7AZ6 zjty57?g6vyY;#rY^+&SH$4!i=x=>AIlD1Fh1eJ2{PkVKHc-C<(6|fU=o;%|z-x(Mm zWkL>as7ALlRd^2k)F%RO>X=@IG23?HC;YWj(KBwa!R9CFiXBmqb*Hft`$nrm&Hj4Eg97h5 zgRNZ3U({h-xNv!5bn86M8x~&uBbN6jEE0z#vKBLt%Owh51 zQYsqg#TIz~cb@_`c8$Aq+X8)?9pz{=@t!$6wJ0)Qo3+wzvB)qjT48K0k%u2q<0ueM z$4E1!THaYO=ewV9YV(W9{d{|kcJo)V53`_}i%wF>wB77s^aU+vTu+IPS*;CIO!}=c zlt#AL`mM`>VYxqjy{#O!T7S`=3S%}&%2BQW5g8$Ums{!KyGA7e;=rK_x0}Gbi?jze zP3BE7?EPtdYMJVieg(Z}J~o$t)qM{yC{z9WjT3{3QvPBKBgkLRfwc9iNWX@E>tfLv z;<)T?XCt|qMFTlervueD-R!l}XvcZG^-75VQ=5PyBQnn0sjcoHscFf*HTQ+aEa?ab z^JQ_%^cz_plX^(cO6`p^A;bju<%lHFD{q-4`O^M-{VV*K8*lsmL&q)q9CZ(G&s zA4atAwOZaU`n;D7k4|iEpDiC#9y@^;o6ffRCHuVN-tgd2(?K(0O3vZP5(BOrYS%B_?x5EG19`1jG^KeESkK(RS#DkT2ACsJ~fV zO2)a}2Z0*?0tCY{xlHc3Ow4H9OuJeD@-mT=U#)<7t90^+)6iAdlhj7=Mj)!zhLe_t z{at`1^w9J-8@n9m-qfVmE-6n~Bo6f9q#DFR)8)KTHQs@=AZVJ>^dZx$pVS%rjRt)x?RYT*k7Y#b9^YSww;x;n)N^Kcv}7HT1$9NLM;|X?K#$rI3Kzu zX>5lG&iGoV-;1J>D1>A_gDY8)`A!mxWESoxT{N zVV6P+HKSxxhb);+f;A;u%|7>ujeeVR4I9h;>zX_+{Y2q68MNr@8_|P)&+ycGo6F6o zDZ@V%cFA8)y%}w)z+R8IPofjK$x^_&-OMR(Voxb-)Ag^*gV#gL{ER^$evBDFF{qGm z>XAyCPF9-dz=5nVl^|ybKxU8Y2--K{k-eX%cL0=}i9Dlxj z?#>$qx&`XXkHQq?Le(v)HYMegDQuSJZPkt7sw-N1!F}!b(?bd|&cDkRDok6|TML|M zTtBd1bULqb2r#L>ov6@0I`+s^KO4}V`zA%|u2|z>@=>LEuF|IJ8Pe~Q(e+wy z)3T35M&c28lOvG@5iS}Km`D~JW<5zkNNgh~BbWaGx37#r{en>-?JLCkWIXtk>$Q%l z>A>|$^xRV2k5zv(P&Tm=prO*cE0%~ZRBy#gJ$0B?o3>QbWPIx0U!)_-5RS<3x^|>t z<0UL-GM}9%xoj+n&9ao`rL5`E_Y$~g%F6d#rMB|HjRE(qCxl=-W%kvig`<3D~Y_k zromwh(%3b3m@6Bz37mZH#|gz7J#88;b|Vgd$aYB><-{^_jAT(RY}2*WV@qk{P`Jt@ z>;-ZKdoL6EQIno=E0sLqdBAReEsSS7mIQn@LQ?C!bHp9!T=dE(W@@?DXnfQykl6S> zMPV}#N=!wDEl*YceLH$~scyF{iyQ>n0`?EH>V&?-#Vsuc($doX!^4OB$q!g(ztcmk z|9lHLy<8In_5@B2GX7Da=TT#=GvIU)IC_-XR5e zJO+WX?^4~4gN3;57mZQ~;@#IOkg|85MgqWV5tbLH%!)+*XTD9Bg*7!%85ic$(FaprS?gHxEHUkUW4i zgjFTWPqm5q?D@^WZ!{L0XmVkO>)#T>D=7rLMXSAMB8+>~Wlf@goO6MH7$Z2X} zq)Ob_+X{x__)9EMP&tbIj@b)$`TWW<67jlYnsw0zEUlK}MzjPF4$>ZS%-lq&-Z4Gt z?%EUJwM-qJwUA^j_5Q3lEpFOyNa7S#3hcJ z>rWPYk>$g65?-RwElS)z>^R+4qnod(Y_|z@KkTzWph`-Bcb3%$gIyEq^WF|mhVL%r zg(0F|&;?%NZLq;ubRg|M*$;xz&A#K>*l-HjmBJ4RL7?eel=*V;cyCgm8am%JdX&^Y zn=sgIN9|I4C~?_PI7j>p*Rnhz@(AH{{Q$1?%c-;TlksPb*}s}`=PN2V%c~nf)=m?pVYlAu?*`62t!GUf zZqW=(_eKuYAd-!n;aDJ08fq6!*$IY)#%QTAhD4_3PF4~!!#|g7eYYj9TSY(6JuN5+ zTPmzn)EnFq4jIP;n+WTM)HFA9+tX z9o;mD|QJScRh~@`(xRin9ionv?)ftT5&77Hb@`wKeQ1)In%6*@_k2N>!9ls6w9C)p% z99UhbU-ui=8Fo1gG<=rEe-eV%ph+{L#US*}9YCD;%v(U6cm>f>7;x&-IzU!??jSOF z5rQ|=7q+#V2iAfutAxZ$-*XZ`dzy7Ie5fFi$N+b^`Qw%**1NP1cBqs23_ulPY&O!f z1q?O|y;GYvT{kvF3KDWmrnl_y9T(_9pt&FbklU+DQ_%Z&Ld*q~Ri3p*)35u!fR{?? znfwm85A;qGZNe73ZeLoK_?)F7n&=cPD-PDczeZ)b-GZh{M~rSoLn_4yKpkD`Ku`?R zyyfOe*ffL3WChk_cm23D?hs3bWeGcanfKv!$;#S=ult})hOV^%I<_OnI+x#tc+AAj zm@#Sz4Le0~!?*eYtXOg>O>hg{NcRs6Xo11``}_OD0gTIWQQj`8p}0Hrf$h;v1R@~N zvCMx)gkq5YHs>q<#{~HA32srRPRF=W%$ZXI6)YNO*2X-q!`dHoJ|&*_EPJ$gT=}S= z*ZtO54t7(!cZM-OdNakBL!s{|nhoR1njtxxE-AmnKVOb%hi+?UF1k5=oE{8Nov(M$ zG}p>|K^-1(;92lUIY3I+KhHcJu{>u#$9Z!!S8352tv%UCkd*stKaFQKsX6iKM$6Sf z9Ss`@y`7!KNY-G(q`e=KN z1X+YW{I3`Dck|=UkSWfwT>~uMFNbjVXz9x56^+b}+=1;@( zMg?t~`O~GnJo{$%wV1$?NzA;vd3#wS;1C6sC+~i%S#64txoXalS_&!i9C~YU8F}eq zOc;%2nuzJxyno!1K3inr83eA8Hg{kwfsT}BmU`cxX&xMTK-(( z&r&1y;&|4@jHk7B-U6FL^^{=K3uNHER+?A_WPr1+z^MxwQE^|iAfhpTP_bJlZ{3UE z8Dk|k9k^p$F*rWs3Mtt#6gah=@tBV%0fBD*&ZMB6Akj-;Oj>GlKr5Vthq=~xfVy9+ z&S1}?6${**Vd6^;X43Q&u3 zkc?xL@4va*=y!FS`9?K7K6N+Zis|;4!Qs}?ePc+6S;N@&Qh>@6oM_I&c^6tLdz3xV zwjuXC|Fe^1SPW5V|CA6Omc!L~5Zacp@#>el&Izz>>KUTMa9S;2{T0~?Sqdn|q zW}{m`AU8AskK!Z*MIFl^Qj6f!R6T_}?{&?gyQQq+?6zb?bf7_(M((<3llpwyt+;Vp zr5^D;JhbNmU!)NOl9jbCS}4L50F^}3vHk-c!~P?}xU$(`mHTv2r-EziJt6Ci$eh1k}T`OKg+{;(eGpsfz*&g^=}d+GiY|Jtkf&4&ie2)JHnn{;y)8$T@eql*J^t6PJ zmO@i%iFwLVP1cBmG*^_63LJwQIF=fCt7qZ~E69X(u8XEZ?757mjV`9XF%lA6$XR$- zMe`^kIl{38_de-BIzOxd=P)IwUlezz;Rg>u%hss+16Z+Wk08=)LxgnV-m z<19BsUTZs{&WKPk$Bh;}U>weu@0Yu4#yij}Y3)7K1W$(ZTKu!0jwTr$-cq`*bGSCi z-fTa&^oIvdsBAjP;7%PMwP%l^>kUt);_}#V;$A;~RR3c;j`}|PJ!y%SY3pqH|6Br2 zBUtcE<<8lHd5QA@amraForn`u;z?iG0_0S${>6?zvFql(E{)0<5E74PrMZ)|(itxN zTuqFEqH@~Stf_^Kfyj9Hb|g(Ao|O$#I9w5e1<(#QfKKk!)F>|2zgzoKcF&Fy6Rs+-0@k6l1PsW!rkmhBdw5w$tJoVhmlh*ZugL z{WVW<;~1=pl;%;1ev4)~%-Bk{kMvTeShx6-JrAb@=TvL6CTA0){iHc|k$x_Z@p|db zI(izoZJlQ$`I<4{+>BOmzR?f_5`Hq_3_K28>7(MWeaCk%HE(=hzHoD>W~1>vRdL#C zAh}xW*|)qZPbK>Vx_3>-7wia*xmF6UMd-N(WL)=8+$#?FCutUpHPB z=j(FDu+Gep_7XA3M@wB~Izco#xwzNieRIhAS-sOvQ$d%hhqT>ceTG`uB45S8Z>95n z7B49Z9@Vd7Qr$$#K#|eUMtls8o~eNKvaEP03`*=9^*45}zm;b?`KA8I6PdSHHA>=m z0QwVyQba!@xA)xdTw^2d*Ks%4uB$zCc=|98%0EYj@p$hBD;yDw79Kpr^OnkU#jg2q zW(O4**W7XDW6T?Aq_i&yn1S?Xq(LTz)3n!ZCKeWw?vDn+%iBT;PY>0$N4MNB@gG}X z-0IG?E=zCd@bzOxYq}lnac>UGa&rq^r&-GeSd+oe_pmeW;-;Yp1Kaw^0n$Z%`I*~# z85p-#_)mI;559HtnJoa%2HiaYw9?&=xYJV?Vq#)OA))4KW9$Yq)6Dcj6G=qhQjE0& z2(-Kqi6Fz>ANEtn{yV&i`nS^g|ME7iC6dwl z2&vI*3L-H#Rxeg+mIQ$W6SH7wLFGLrazF9vZ!;+F`BD*~78FcEg^ThW6m9m)pAn&S z6ru*@XsxUJ(GP0oSH@i~_Mhb}XM>vaItGZIQE6R%#>7B)F7z&)hW6miKU-F0$mt-J zF?XN&8El^M;{aXOX8nkJ-N|`WVyoEJE{R(622Sw<@Vx_G3e$q;yPUU35802&i~@S4 zKYn1-N`<*?d-%G4-5xzaYXhoL`pQg9%wEF5p}@Oh`ha?ID&(q62*%k8c828`;?(INynYp%(d!mS|gw zEBP4%-7C2~&gOXv4AK0W+@z=2F-b%t2?lQEVf2=#qEI4I?mD;ONH&EGnOelcwlZL_SDfT<=Q!&wB!awFckxFz4w+0yar1 zFz*tPkg`|=cE!{7hLtuX>`9C@Ql=&p@NL@1&ac}syypQtfyGopevbTP?{i!D6NqOF z;|84N_`#b?>0XPr~cz9i@ zWZhW1tOUprJuPog?!^Y&D=u`Y!{E7R0`rBTGDbF$lY+#kV}(88IYn-;IS*hM{&)74 zWQBg)+N$iyLx!LcV1rDt-MzDSzMa4l^6(lJl#;Ss6=xQuhiu;l-E%6xbeU`;vCuCz z{f0vyXan!hDa;PwT}({Sab?pRlrd%I?gr&pAzO+OIGWC+_0}JG9H@Ca+Wy@O@FKqC z(;e|#-BA%t9h4=Gg&`PZtq_>nrlJaqqX`s4>i!fhAYRDDSfmc`4u4NA29fkM77}+1 zOmYM~L4Rybx(zZen`Q1|cN?lSLK#qMZ>m`j z1h!ep*>yR(j>Gf0K;D^p5(OnXuV_g;xFdSH4Wa4XMa}aRfK6Hte5$?e4}jo~Qq-f4 zhb^z0g!9r6myvZs*x7LrdfGi9Is(qTM3v(rGv-!@{US`I>-aClEdh6v>{w~Fir(6U zwfUOX0WOr+&^6Ck{T_|O7Dq%?cvrAch%p_3g+Go*+!s0^>>K zBi4*^Dn}#yEWN?X5j8vfkL%ZRl? zmYSKZnnVOp#<3u3G%))k|I41Y*x1KFNS(&3Qe`k6@e^+!)c{0pvid4mOLOU03d5hx zl|Eg>7K#4U1e_;hoJ!rxSh&oNzx2E3EUC>v;pLEuJV+xAV zAoSI+QAyYdl-sbwwe7}U^P6zYRI{HnlE-C zX@3KeA1i{jsOCpBK$8jpsuq}u>Tck_SXu!dafq+Z5tjv0(f;_S&iqupY(nUTD$3ZB z9}0zQokAQGB!)kW1`h#;OpGgcqk{GT2wKo?6J4u_n6c_&900!MbQIH#xEXj+9}-8D zujYiO`1m_y!Z<(`K-RG?L$CGSx|d%Uz`p=9JE<(t5CJP^RSJs+4-Dn9SI%llYJiuv zH4f_}f%%1j(@OrB@d!cxmIm>qlsJ1}{uTwGI#BiT@DUoF_fcf zYZa<>nhmf*qtwQ?s*9;KVO^C%c%yVm zy&?5q@$eG{%y-|}=K=G?ACifQfsHNm5%B!g1ceX!y&Eerw*^~T0%nmx==q&@YH!9;aWq$wi($G^^RayTx zK%F9hT*)`BjY7$R=T__ai42UF z)zCTCK=bhkDz%AWrjqXK?^;t+QL1EkyjlPAkDKnD_=08>y**T5@Y8$i9FpWHkbP4= z0>H*e{apfPTjvft5f$;E2=u;2MLZybZZ6jSmNcb{25O46{s?>I8 zXg)0iE9W}^A^>5I7N|?C#6K&8K@c*@|MYh)$0caHC^Psy22g^9(sLCfuvhW%IUNGP zP*(#7&A_Z$dDSIRDZF4`=FaLwZKto#uA%{k2GWAnaWI?m)RjyxNyiT86dT9|yz1AI zFU~>mmW-pa88At4{Li|Ef1ctBH7WyHV$(YS5(<6n@OyI4^%Q6~bJI;wyGm#ZwP+Q! ztj=un2b4gNqQ@$nC4Drk0u%Q%xjjo$wsaR&NoKBc7)#1+$D1f8x$3fuxC>bzXM%q2N@Xep{-a-o?7~6l9 zMdBDI5Xk~ExWPoq@j%2+={iIf2(%j@;;tfRL}6v*tVhs<&rSdcDLw)9`)k}0bQ?rU_dCgrzi)e)L$!p1b;MkGBs-P65;17!%aI zS~G42fYMZX*k}<1I0R?U3#^w^)fxhrD>ID9;*QO-n+9#_=xzd zz5GYJZL0y`D~ra-BBY`y>rZ_&#m5VH!7BdS%RpFwdDG(Ehf)*-fwy55bsje+6bz-) zL#8)#fTNt~4+*iao`#te`PvoXQ5K^}!ot&V-q^T&76Q}(lr=!jF&f}55K}gW&^z|U zX2l!3dnKnL==!(?y703SUI220R;e#!>&p||$%L_D1c{|2nmk}t)rc#O`SZ{o`~x7B zv%3${2061!j2jKM_yVvkqp;?O$_Y>tMVSOd4Z6*N zRc8WwG(*wKGz-60AnAgj3^~VwM}s3XzdypJ+AV7UKdNZ<9Oy200a@slU8d(SfvVUJ z_^SQm?QIK%z|I9_3cy=h(b`ie^#n-lwH#xhN0Mn!JS~A`**bN8)Q5FHwNXEwFcK}UH>YttcBYxiF zC@{42v~~5KZ_N24;Q2}@o62iYdop|Zla)E+PQcA6kKoZ-Bfp&dlk`#BjAww6QOOn# zldXiDM5JWEud+2$EP!1G03hQeqTUoWuevZg^Bt(SenTB8YCl%-&DcvS%_DKA{NxS= z*(TFkTx@J7rqLx7NAm)bCiCzIGCbl5&SdU|G>Qv=#M8MNkR?9{Y6Aww6+m{|Zh+j+ z#8@`UIevt1lvO;lxgH*$a}0FUd9|3Q;TPrSYHP|s07zC!0sG~r-y?9SW`N`Z)TJeP zTE^=z6DxBh8em-#z(aYB%9?CL3)x3Yf>J7BCY~=ko|+Ta0x6$GLP(GwDwAxpn+EvK zfV=D4VS7NfQAR7OE))Qfv~txs1lUw_?GZhYAeBniw9IP{;wEXL_as)X%PFQc6M3Ta z+N*y7!+{>+<9EtK4tHx4Lq3S;98P{LY$^>sbqGJ89yw`~fwbImdhP>gY9H6dXQ-9s zeC{+=zOiW@kIPM*U|U2zvY!c%W;{6^C*9!+pTk0Wj_E2f4 z@d79aBA*`Iy3Jf6eYRYOLpyJ-XDL7p&+!-8M-MT#K{(SSd#xe!`_U`0Nsc0Ro}XEn z0hLtxqj_(*^agNUMwzeOcZa6jEEtCqrD|KlJ6z=j83=U2K~w)S!Rj)H>G}pxP7~&| zy`kGX^Px$YrTGT(!uHPrwGSD;F!PZQM0L6y7~33`lg?c)MG%;Y`$wNshdyGc4XAh%c4wb zD~Ei=#MeBD)uVN?6Nd22<5@4Qm`31PIxDD3W7X;cwVfSleEt}P#^6^KkZ@Y(on>d+IJA&@`=y7JKsf11s{|G<+h$Pkp+LPfY!%hsZ?MpZ( za#gQW89hxym~eDToKCX94Hn@&$lfLH7M+feti5urgB)e1L0@AWUe?JrS)+-BG5Mui zIAplAp87?!tnT>`vA6~ zcYPvkF6tjI5m_fZ^B+556kLvGHgz(mPN7rS1J``YNH}blz_~xnyjn&!>0TzQO+?Yw z7+IW~dH?l?zR1^AHSjcpbYMb7+?&Rdv~PZTtkq_Ta7qsbI!0aM5N0i~e?4cHs&`Pc z+RZ6`pBtX~(Z^VlvqEO8461S$ zAiiumM5wz;*Ah{U?K1Pn$&~8s@9F7OO5E%@s{0&=1ATR~O&aFAkbxfj@g&bJ+oNbb zb37c8YlCWGog2J{^=$z?pi?z&p=1{MQF}P8pwln}*!U!IajQGuRm@cCQ)&5JW_z@| z9`?J>@$b2B*bi!Yn`cBWKkSA#D+Ct`my}KZUc<;yxDIU6KI7w7h&v4kY&?Ad<5>@@ zc5@HJdkbIr{+=%yOZ!8*WxA|Rm*b;f%Y)1NXA7(;4w|Q){Z_OzfpJ+MD(jR+ z`-nND`;u|TV(}sMx0jU!HrG~;B?0pa6dnRZTvj!&C^RO9NqFw+9ZO%iH2k2u6~~H2 z+OQ01Av^iFA=&Q`vuuf!v+xW)W|G~n#z;B+J%nuskE+ePdiKVFdaEs+iHi-KQn&Mz z8FzD;@A-cfEL?pm$GQt{(+iO5(+n(3H_C}n5OigtZNmFI&VQ{5uL(C1$q3cFcd~GT z>s~PFpSO9;kYe{!mU|3Exc;lrDIh}LNugXsu%=%7lpJbw*CqH;Kgj0^S$RiyRxyKP z=|ZrJ-mox_{pccvJN9#a16XG@c(GTtxvpHJ*!Odj!0Td_ZhhX8+3g%7-C~#D1vEE! zj5Q++1xrZZC zwqByHZ@O@j8}goOhAKp)XN$J#hhAUV1{BO!2)J*-IF&U&!hD#2y(=#H@ifI|?I`d} z=HQ+2E1Dg;i-481tD)S=e+W9NGMK*PMLQQmZ=uub#5m{`yF9=}qB0d3*!k+c|F8 z0bQC2+UmUAFr#wOyW3jZ8>R!HQ^W4wG{GkKsFEnRjVE7{*455CRwBM%x4|8lr;aJZ zIWH@;)eDHYT_T&Zo^QZp-Iw=t5}VD6@m}!fBYG@S=p_Y-Ry=A2;=Ju*qP3eh%CDLi zec?w?vES;srW+275!FJ5guM@=u@*y}Go2|jKR?~&dY(?2?w_K0!q*P*RW+0=Fo|mO zK|0$PMT1WpT<;DQ<0Prk9ke{|38|)mHb(mUQHKa+s^Uz>D#2?)k6(-|-w5MHA?*0|-z+(}969k2Z{oL*TgRc;abPRMPu_)>>Zoc)KYls2TSxvxNVg^(4inJWY_n*DGP0ZZD$U zld)Z`+{O_*X!$hddR~jIFCgi=)soxVg;Z>n)W6wqDqh{&xvx%axaj@;Z> zoGJgKl6D!&i}s})ENne3*dTn|0=4`H-^pOn7GYjdA@Q8G92+A@n{*ZUSuWU&!I zVf!r|-F5P=CXJ>E+~yi@qg*Vavq!q(PegdAHiqgv=OT8WyAx94(V~i{VbMrGI$?i7 z_}W?NnDy_>F1dOY&yam0^+s}#DM*q=Ps;hE`peX0U4=8jDlM z(PX_`PpOW7uVL?d&o<{eyfjmHMs@Y22QNHU_cV4Bxq{EcP%L(b_OBLnE7w{<-d>IQ1>Mw*d8v-x?_#0cQ8XGN%YB`b)eymVTWSm0>VnQ+rD$Gj zd<-?yPAEbo7}~LB7IrJ*_m6xth>Eh$V%W^+MI(#I#Ahw=BgArJpU>sN2$g#OHHdc$7IhhyZkw^G3Tn`DTn>DIK6ZcOdF_$0 zP*SmR-}bbEhURw2I!LqDd!jaAiira1IMDv-pAPy+&zj0t0TOqEsA~SFDMuxUtq|X2*v0jXS(SEems&6!N>v@+LPg#!0rJZXjQ)@lE&wLSh zp9o)9o@E)wyZ-b|prT4Y3&?*%C6=>O;A#Ed$CFfRzQ%U!$w9Fkc-kL|JFKtw zXzK@22xO@+QZ#4Fm*CwiaTpy^%pB^cLEi53wv;tcR_EOIUU^rQFulNJ#4p>(7Gm2q zUI7L-7|hpA$)k-$Y;!2hdM@VX=tOC{32Eb8C{pDmWY5m2Z>bsdIE7?ggJlCRUi<~+ z`Py&xD+YX|c_d)vHj^!Dq&6gGJh@rWK5^Tf6R|C#9ZcmbFFA3i<#%VvlyYCI?PtaC z+TV*j;GUMorKQ~W(42NKKvJK#El+yD#_UlKbKwz@;l-#a*~0xfqvJX%mLc=;8UJ&xrc|Gg7UUPoFwel9|7h&@+AHqmT zPA)AflAE2K&A8K`+^VN?7+qCjK}4B;^2omG-P7*b>mw9!BT-LCVol zTQzOc`Fpm4Bfu?GafW&E==}~FWs~{yV2uw8Fye~Dv@}H-nMXf={v3@jnBT)d`sX8OUnt>iJkoid8=v zyI;HQEwbM{A!&cZOPK;spZ#O3{}*`**`9SFIaN$gPc~_(d@iCj=_cTLjojFytEDdr z?5=xT{hG>OwEt1hk0ezEOJOGUU^88oedyf{ta|B5U@qpD8IDe~0MjR!8BKq=D8XTr zuUL$kIIJ1;jLKpO;g|$v?iJf3aJa8X`yx!A8jw|mp?W;6=@@^%xo0Mf1aS;ET>Z4` zI6UsPaF@`}Y!Gd1u~zwuE%!OwcrEy|0)$cg>ye9Eb0CR(Vbs;nc)dt}5YAgteD)2b zl1(QkraiK+u9k8V=twKI(a{}yev4MEo3 zCd3Z9r;5;8GPSZ}h>jStyFaB64gA0(OXU}fg+7ebE8`{SkFL6bEop9w#pW3AMF(~( zB(cr?5`SbV|LTmmP;xsogcKJf$1|yPYm6)^4E<1m?EZmm84Mr)rYiHw;$gSTSp?V z&mt9`@LwR@%_5a77ul$2?qQyN$XP=vUCXyy^Mrw$Ir=@Drdx9>w~jOM7oTKjD&#EL!o>8k((R5ja=I zHQOpA-WP!@=e|3=zE7npl9CEmz66=gCRJs z?C;FyU9&WzCi&4#pTyCXETO9Ttw0=-QE?DtlGuo25dRuP@R#Dq+ld_kD?!j^xPJnR zZxY**QAg7jGR)!rk~`(X{f~0x>!rSnYMF^$VsjbLiU`YAtHqgP<{jBXb7RF1%NiaP z#yuxk)tD>m1XQwrYV$2K1rtZD_Xw@UhOkznfFw;_IQl|7?0u1{i?^XYWATcy`Yb|`fT7#fdv_1f8w@^ zkP?&w9LEJXP7(47-1PZDjTg@iFZQBeZ0@dI4=nI82XqngDP z!am#b`wV$Ar!zF`@prCI6s;Z}G;M?v$c%rfdbrMNv9*bfnDl#_LX)(wE;;3T!A#d-Q7;cQ@lDm zjmMY2iuC&t(n6Jd#tlALUjukjwlaOPOrbSXWcQJb=eR;|cS2Pe`2FF9X5RMf=fdCaTcVolRaMk9ZPpjrQ=8zhcUy5~U03FzOvq*~69tmkBZ zgO}IzEyK8V;IKc_Y*C=<&XmLH;oW0t^#-K(^fVuV`$6rwFnz8N%7(Fj+khfqb%1~^ z!y7aAs8s0>2KZ*<+N1AmH!JhX(I%cLsnx&sdD5QDp|hyq<1(3|sS^2xxp6bra);xd zmeh`9Kdx%v`%~)Oit-L%IP_qqNO9qCimWD0+deHOXUtK+Q1R)M2LMj;B!8ezh z^TOhg$eh5hAl&65gOSi{wkq!~nP}}yyzgtj(-9PpE{+!MW@X?l+##3Amml`;TL!;0 zoQ+V{liaegA+ODXD9^6QBX|9goWll78?S!b{4kvfSoE_iX>J}bTxzR|*3!q19p{^d z84|EPwd0vJ^b69VYF&gG?o`HB8(R*3L3_uNY)zXl@RNq(4fEDV(%H!8%X9Dc*iF{= zXP%1*b`|UqA9+@ugYaRTk#89}4i6({`iF*SX=s9%mzPiB^9=IM3&HeuZYhsH~EZUuP(NLup83obQ9VusBoe zYZHo69G!Y2CwVVldYI`F5M-HXJ2QCVT@R3G{;D@X(2FIu`+}!w`kz9@*w1kieuy5_PTsU)BT!9^~VE1uP-2%9v!=#|lk0!XU z*qLJge2gb`yR$z(AQ(Lf$V8IOrCaLh1}fGO7~F+f_C<5E*5&`k05VQ-x-EM{HcM#B zbwW$Tx@G<4d=N&F{yJhIX`%O;(vEKXY7C9UM>F3afNi`1i7)>1MY4?*cdPR9fE@`ywpw!%3_Z%08^V`G@A~$Wo zzsN8nJHpsTOkvgSFHfu5V=30k>^jz^kT-S}KmOthphGe*njej;7L&P>6F~7&aRy02 zldk7q?>C_{+LgO^F@0AnPd;U|Id;9|-gBVaW+B`8z$DQvFGyCyO8eV`PTZ*IucGzk zACRl{DDN`=&BxEh0%DzusSg~q@+VkCX!5rj;Gdnc&)U{ zCp>Vfto4r2lFJmHac`6QOL&jn&#<&EU` zr`HM3hD`U6`p=1%#?%SL>reRNg*|EYFNvHQtRG?ZuWxV`d}pNG27klls61j^cG4s% z?|&*kauVY_(K< zcyM=jZ=`90OXChr_cY1#zUQ2oGuO;D-_MVKz4xxYYpq(fYSq2&RUkb}MG%s6izqF@ z`#azgBBuP{we43%!@m@rzRC)vb5Jm7loF>4Y;YlhUy%g)fYP_eu?v{HmfdaJ#&rj( zKWN5gomwJ)p5nhAmM>yq-#R}0Y~_ALEq1eq`znphc;VzH>GY0nDe zl7mp~ttIh)taB0zNq@Y{?ZWjt`0Y+Cnd~#ZVlCd3)(Z;jqbDKl9tpb6D1P@L(~_Dk zI(<&K`XV2yUuGP9duiD-Y4(ODIjfg^SsNL>NlZEOdsu$Xy>E(`<;gERiF!O7uFYts zK;}4HWqSrgs`a@*(choCY%m-B3Iv%Ks=l&$nqkv4O3te_8M5Jx8&iI->+3<=lQTCk zyOQFuH$NN|;jKUTK1ODAVF|4H`9x-d4h1_(%)L*3M}W(rsmi#Q2C+uQZs4(=VI`~w zIcj{#_AIMi(0I2w_8r2qsH|FVnR;3)_rz&1Iw``sB#9X0uABMhT~WgzViuL7#S%(1 zy)#brED~1B+du0GV+o9Li#_RHSGu=0;+y;6Xn-uNF4C(V!(swNR<`f$DB{z`vqz_g zP!t4$&NP)MpYmsLu;)c}6qLxyH+5it_2g_;-#`@G5Gn4Rg3V-JJc-Gc22V^Qa|8xf zsS`9gU^l0DD~%pk;CYMzojzz^W0ySBF7N3G9e4>?yvJ-aT@;MDbYR1H%QVl;u_@sg zpETjEG8DHFUySyGv2j4kYIk@*f5gygYS%L2VIIBn<+0~kKthiAERJE%5a6qqFH17V zg`U+LTT-#Q-tQxEc&1DM_lb#H9x{F2V~+Wq4A=K>}33A=F7|)*vU+C$_LNe*3eEmsg}{LE*zkT9bva|M*ZSYbi{3~ z*A39>)RJaLw2ir++xM;c7C%WL1>;n2qIE=q2HKt{mc<}ZgJLUvdc*P7kvB}M?|=W~eVzi8Q&N>Bk5SeO}u%Ls-WG?whE69 z)mp*t`dNI=jN{w+YkS9tI1)so&i4%B56F zHpL;!Z>t_w;)}u(woVnoZuA z^H{U=MW9|Tv)!+ zsKwzn;E^;1$!eJ}y9Wu9JFfS$XxuS9eF`odkBwMH8wvfg%6e7Vcr5n}3?`%vZrJwdEYB0HFJ1E$lAOAk!yDII^O`FYEa)yXDxP-Sp8P}~tYh-P zAe;&I22tek0@LYsO|O>P>6NlToW>C9UA!y}wGEaBbw+BSET28Ejl!w2nr0rky9sep zCX==rw*ivkIGWsjeLoyy9Mza!0^27li|KUXE*|#GIrxn0(OX7jGaQ-=F4#xYz9JM( z@s>MSC1XoZ)LyeO?Ao|#x%iFhb>zhWsxhQI3a#G=33(!@*?$SzU>zqox~9ce9}tZGY_;JEo}QOMkO zAI{5l*U}x%N>zDq{E3Tgu|d3Q{+>Urq^&=gJ;&f8RGm0w3X1* zZPbA!E?TvZm2&5!Kg5C+*(5@uqtf66gO-Oa!*9BRar>v`F3FE+>fT1EjQAjqv@3*L z8VXpxfdW!y)Xe6X9kA&k?}4pxc+%&Uw)Pj5wzYQ;!5H;)+C{~#84PlH{N7=4x`94O zC1#sz?;Ol|prJk?TYh`5RlcVnuI$()LhDfcP(U%Wjn@p zyV_(O>Em6mu9sn)%d=nU5MXT%KUK!wVR_ANy=|%S?3`r5d6cuOdQZd?ZABL%s?}MU z3RlE##0nAWlW-e7B8-Sm3p*qKZ?*xqYt5KaLTmp!m`-=9?Xcr7&PK!b{}6beXZ`{9 z;vzU>DmQmYSeA(gS3|(YAK`r2R+i)IB{`ts+dWPN*uk_3xli_g4uOR(p8xn))a+g5 zyd-URL6uD}y!O&vd1Q6?AS}`wiY0wzaT9qa{a!C?Ojw_s!DDNGS&%j<8OQ6F?JY06 zh#=A3F=NA(pC#H3u>R%R^M3$Ew!R&uq3f_Gw%4iOzLXFz`G^p`mL#hmLDYFZiAsO> zwE(Gq=_^p;L%XHi5t+MQr&};@+|Hwpowr76uam9cJ+~F?C=zQOzmGIV^{l7t>gk+h zZTzIHOb^X`3Q4pb{GR+KzB;)0(Tng`iqc!qBziaGyNh7PJmuHchElDb zt*CyEwIA^d;G2F@))Qk{h>ZcbDjvX?-fryRwf`DBL1Hn97QMse6x@C!=tlbjXo`XNa;C*DKcs2}u3LEU7r4l^Fb zrCXpXTU2;&yv;_f+SU4|zk7l=V`HuIUG~3XXKCg}PmPjQlC{c?KuT!dEWe-s3mn&^OlBq8C!me#zxjO{ z6{$cxvgD1( zUkjxTA;#c2(0ey%FTRGdwQ4Ih>G?+d+JM({n7rrmmv5`@TG!XmR0W6l_mAw^quxBA z)N$Cy#AQ$tPMGA{t>3W%;iA|*ESWlGmsw}VI*<+a~ zdZib>4Qau-4_cI!xyGU~wt99$#bR{Uhbl)&iS5h`Y^fv9pBFXxUVDN$c}5&)>YZSJ z&?J1g8{@=CZTV=9HR!o*nSuE^`lZbRvS_rRQ)QcLu$-2BISXRIVs z-1}ds>+_wXFYNiUu-P7*!W6aIxBiiWlmp`C6xOu{i;pSdIl8=jYki80O~c$)7+sWfm1D&0>rP$v8MF?b3 zNhVbXMld_|uF-{=qkmamd?kG>6DP>k7GWbhV|3pto$%Z^&)bahi}2~I>X0d6+Vzo( zV%+!84uw))GGM)M44|2;iXn!cdgMhbfknpO>_S{LS5q99H~!lw_vfsDoNrwOPp}RL zu)^5lzKhF|aN0SEX3Se{1hQuI%&cxv(9r?tCi}$Rr?9NO$qL#joh(9mp`2^^Hzy&~ zDelOyJ4qGSzUq$Y$+EEC=d>CuRhTwob;}IPtW!^>2Otwz zVM>l4>K&2Xg_m^jM7vD6=<@FxD}F?MS6=lB-(!)&A@+u;D9QWaPvvXe%^F6gs?han z9g+WnDGFcNlLvcjPqy6DEj*9vY*bLgiZaxxJm{ z=}RRGFm}@aKcr*++f`x|M1hNEU-xf^K30PA5D^^6U3sf>9531?BoQ7!MeU;j5a>Au z@hR5rg*hT;FyxF#eBeJNGL(H@`}mQ%Uzhwu=0&w(IF7WABs5 zJLW4NOUyYJ>)OyzN2W`D{Y&RM>jf8wh1hJVLM1xO#a0tK8J)#UhhU?29nu$hV$w*r ziHj0^<8a0s%QFEj%@Fz;9MNhsE{8em^Dq(q$k%W5%UPZjDlyXX?m^_EwZ0DX{VH5l zYy(z?wqCV#)Nr0yL&;n+HLD_>k6;-C*Go&hW25;wmB;llAKiEoDJ6e!)hN?N)9y2A zT@dEj>dI`wJ;P4!B_El;b`QGBaSOknW8o_Ij*SzQsH6f(O@6DtfrPyv_dW(IkP>_U zIOw~_&^WDN1iE$j>5~C=%$r9STw4dejbE*DWZkfaGngwX&?mp|GkK^rW`bcaI9sW|E=+4u)a-Sgd)siBdUjD&=nR zP&LHk^m}9f+RWNs08hji;KgW5(MdaW2CxIvS0Uatr`P zGlvBS{p>>|~-I zP3A!xNtHe7zh>LrVAui$0>qPUuVOwgpTkT#^+E)|beYxI)kj%*-pn z#zqW5J5_7|%d?3UW?$czKA6?neSHb*QzEC-7_cLImrU6La8k80C7xiDU)T8d@eP6H zm>|t#gW81-`$*6cESn1+hS|#b41U}OtP>qgVUC{gyj|a*3&te@_sO|Ri`?773GTH$ zD~hB0qzUr11&~k-c9l$Ga3e*(e?pjBg(z#MNu=-0kwr#<+Aj)u&V zXs-%i$U7<<2B#@;;F2+VfB!XkRM43kGEG2WN}%o_L^`Uagkt!?RjkS7`6zY;i4+I4Xfw z#ELhNVxK@^Z-KL!NgUJ{vv8DXycOHWOq$lZVGD$)pP9kNw&MGk}{guUwY85f-)`Kz@)A4!E0(zrwL4cK=# zT*A%HPH%FVT_0zCyXok@Hh}OS^b6E^qPsh0f1g>*`K?+(4cswl4!>WMLSzGV5CV^e ze~ez}tk70|JdOcWlW6WY5WxQ6T!3!$BDXa!!yK_(9 zl5zw+)MRjR+s0IsyratuHpJKt*jErQjgusX=__+Eh;B$k)fF2b2o>p_$RyjMn|55a z%Y3Xv>KAG(UR!AK65Ba-;iYoJ){nfuO!B$}gUzNnQHeXHOu<7l`ra8edZRA*OqUU4 z2(&xFi|$RvMZX0P7WA>dR^wo>pS}SuN-a+&hw#jy+_Q=7?_rXTwp+!GWi4Nx;KrT3 zocB5V>W{@ya`{nZzaxGw#)cJVfOt~s0<9CM_kJ^vA=HYHFG}N^(PR5;l-KIwoh>O_ zA1UBiHrExl)7@}L^JRR!#_@c6<-9FVce_2s2`4qrXTo3m!}A08cgQn7+awH|Qm)r1 zToe}brO?D)TlQ`tXno|c1^5NPA?+mYCf9Ol)@xNP{Qi-~nrx~aSzS75bc(f!Z6`C! zPciA~RvgZkW9{7suP-C2|M+vV+@)ik$}*wM9mBtrkhnpFCK9!%S#`}>HGRg z?jp^lXizEqDgjbXHM$`KSLjO%0qDKm3+;R&0;4#Eow>$OQ_(ikinm60yw2GE7Z;$0 z%r23_G1~-{Ak|^`bD`31{BXw680t2T{&sE2O-I)AJ7sSyx%2ju{z4^w{$lWjh$H-( zB?rbDIiP>NX`#GYr6IIrl6 zYvbWYwj-GWC|nW}=6$mar!;&XM}B}lZrY)mQyCjeF*}=#r|nU2k0)#MewxQB46vf` zyy7z;;fQOCV_m@Si-t6t{#@~CWkzYyQk6IpS)bvw7Xsw#9dmeo06t=>E~GU&#dbzQx>k*W?i4AsJo}!Q~P7K`7ZR#dTtRvk*d=iE*?Ghp!@mg zLPC$9%1N#{9fyrskMItwUF-=30^k1$kL9<2o*(Z&&a(GPzvG8uKc{~mKABmNZ+X}~ z%>jaavAFdGiIg^ZX0lu3d4Ux_h3n%^a(3UBpd%@EHSH-wXyU*Dd)ENk>i*ws@4fg{;LJ|?5T#R9(r zqL2_sFJ8DY#yg?HMw@mgk?~*P3dR?3A4@O^%9IT=ex332rU640Tn@b zz6WY^O3P^U5E}krOHA9_M-0wOTb17HZ^<7;w<{h@=;U|6`x|+PjWvlS;oEhaZ>~i_vD7j4 zOWm5KngTJp`XZyr=_rx8=HKGW4I!gf9CXcB#k8>uj4s}?hyIVpddzl}<+Nww9FM^h7e+1%QsauY zl)QkuOkK4q{bkvAMLLPDPt7}$hbE}nWCx&T8tu2LbJk*hyewhYCEkNOB-0t7NC~7{ zhfVQYId6`p^_*%Ks@`MU7^CZVDd)7i+7Z{2^;+6DkAk5FY?6!B5=*lRo@^D!qBXdg z9s~rcwnl63k|Ua6FDP4977p~@T(j6f=S#=DKC*G^@ghQ55n7u(AdUgOz$OC&Ox-j2 zGlb}hQ(+pi+{z-9`|X>5d_4ZI>gr~X&na2rZCT^-QK+&9MB9hup+xBacbMXmZjfi> zzsUtn-+u~>PyTYCOpO~2MJkV~#U_v*^Nu>ncRnZ)nD3lI7I7NpK>6O&(9EpI%+H?^ z8Hwp15WwDM$7o)d^?+7{hW`JD2=IsdfXd7y3OQtZkX#_?0hGY0^E`UX0Rmw>z(v~+ zc;Q+nOR!(}rxy=6-b(iadq79wq)n2puiy9gVRmTsQy_~57J*MhH%RwHBiL}&jK{Fn zJjc*ekX-Tb(7!>>*_ooV6G@}&w$<(N`ixKb$$@ctr7K(of4#Y}nVGVp?X5k`p^#=~ zRN`@|h6T+)C4$XKSz-dFo?4@=BK|3{R8su=lvp_*^^&f=C~_)306`1c=2Corm1JCN z4t13>b*O1>o?t>jq4q{0y!Uh+y3P8IIVSs(j-vz7qgR`XhjKtHx0* zAHzl`cMi+lT3-5)foT1gL0E_yB?*v~mm2#eWl_PdR918IS`~|LYq^?do$$_H9{S;7 z*Sn~(o!*^Dy~D#`nT*qQSNGeHgdNKA!8sz6tB4=Dl|T@v!78iBw!qwsiEor>yOJ1% zGwv{qdK`8&YomFzS(H~5CWq5DIxOBkhTzBSIv~EtLfa_Zem^GQ>e`*Jd$GDC;&El( z6>tv%y}(O3tJbVsa#k#=a68PiTm}UVFOJ3l1_-MABCwzz`{W=pmk-lj) z=^{EUVr2E+A#x}pCA}Z;UW>^K8$OU+mk`v}7_(|EbB7(-*lUJAG}tw31V^D?6`!q5 zVO<~bP)k(!hZ8ro5~cnZV8kl)?<;&IK@6bOcV3C3Unh$GUJreEQV5@}ZDjoG>@^X> zyehsSQc&54(adcMG3F2UW{W^-{<&cG_`xRsh@&jmaLpCvgSh{<-@Atmf7n0&zVF)q zLjT7f9PDqO3u)i34fDVdT-|?uH?N1YBl2($28^(JHxUqpx>wr&q6w|aCEh=cJpTwj zz1y(XUCQ_O!)y-PzZdP*K-Eu2PN+Qnmic;?Rl|9QrOF%r?YzvkoQs&n$?Z;L-> zQAnrbmDbn(U+*_D{D%QF3721UQJ}y0_o&9(oqQpM|N9Ho52M&^rH0|s{P#6Df2@=e zeF~xW3@5t(c+>l?-joHazfE=jEx@jm)4#R*#~_@9n*O6VLWqw?7}_1xuqezvu(#Lf ztqu=;E1FJHsO--*tP92V?R-Hi8yl4FTnW<60i0k!^)xFFAFm${{fUYplGklv#xf`h zub$21A!`4$`K<@&8nXhAdtDLd$0Z4 zx>0B(+TqN08DB|)=LX2j0*VZI^?FMx00aWzB2asb4|sdhNJy|JC{EwdE;tFhJ9reK zpWNJDi2E@6`N0rADOBd#T#+-VH%YgRa+*ib``e^tMRmQfLUvkxTJEFm@BGdeE)d0( zs|6x}vEPy`z(G8dMc$0;EOiwh!D?`Fm-U6 zFO$ktdYCoc0%-za2<8}(`0L!Qz^q3^kklHjQsNa4ljW`Pv0q;c`7El45e&bp1sSr4 zp;TVBq!rFQ8%;Q@n^(#;Cf7`%Kzr`!lb8!%I=9osf0NS(`R53a&$K!zV}nC zW|rOcO42J1;gk?olkm{w`HKEZL>2sui*`+?O25hPU|h)r*5t$h+j0-?0V>~TbtWl8 z6Eba5&e~2ete1A%*|)9F3`}2yIBdtZdvVfxUVhMZd<^YE+l`aRZXyA8X?-qJIYy~cG(wSRbXPJb7-I$Y8c z)@!D`G8iG=;@`f^9oasT-E-Cs#inZKPMLbI>=nIIY$8J2EjQ>!;@(a+m~+&amPD^u z#xfy0STcXIQ6GFZgt}zGshD3cuVr~FW3Rqud-C2=#wj;{IBrvttUV8`J;k}Tv@j8Bc8zgAB zOa_u85sx!Cd-QHtZE~XTTa@;eiW9Lv$~X=0)Oh2Az!c~g4p_`SJ=NoYpw`jv&>hEZ zFPo;HX6j<{4fi8lr`f5UDN6(-_XuX{p|N$<8(R7brR^+sYy#0U=fli6554w-4DV44 z<*{BdDEc8_mGrUB?A4sWW!2d$ zFMrY$*(F50F4?dR!`qONZlzje@vCLdbDSuiXXh?eCgM6Mz+P0+BG<2vA^H$zd5v1n$JXQ5M-(NMJk{skksffU-4Qu?2nU3DBJeMClsrvWZbRv>{p0N}zx7(G6Qx2+i2w3C+p%Ow zGT<&`Ia}lO>~L@VOd-v$UJ;H=B;qOb-k4z^eUtm>B#1C)L{0AWQ7SpODZIOY12V+I z*IpO9_Dzc`I5_jBR5NaPhxBAwNzF+J#pQMr`m`V8$Ei#?{d z8)d>RvorFmzx>4<1b@*OhDio0geKW4SR9BVivL7qVR24e<@eNSm-sI1*^-lYIMc)@ zeBAvxBEqHichfCr{VBzf#7bKmR1>saF~O3JR+{JdR0pqu`h6s|dIs?<{wM4lQCy>z zTdUeV{zZ*-tr`qXEOWk>Yv1$4jKbeZQLbb2AI+R#9g?h* zg0m$C-G&uMUs@ROpC*@@jYoS-{ZgU`<1AUIZ}k0fY=NL4q|(~9JDwip(F!)^%x*~i zevQqixs%Krj3(b-*VwvP59mB^{Ec4z%srl6ojmXAynj#*lbUPgyJw)N)&s4xFBaHd z)>VoXZT);+WW-v$F{nsY-*T?wjo$smSz@Wal#h%TE{@Q3JtA)gsyjW;zbd1X z+9@SL>|(cbM9DHhhbUfIt-(-xG@CgUg}itbjnHHwWHGG zrBtB2zD!x;pb0w!4KXRE427+eE%WyAwuuJCpCm(flXx?I>2p4Otb8G(Ha1Ah8FllV zX!f_boj{dg+9$dtV52i{2sv$7?V}l$CU<0)e4?;4K-EM~oQtiar?bJ`Z8T%v@zYh} z-}el$@tYQ|Zq5~pqy)4@jCp=pQ4@IQ%^rPeoHzzy<1ZN=MXOPKn+tr?{eI+g`}&WP zbijy{K{^9cmE04uk|Hsk=)ql1gGaD)5&#xO(|F8(Ey+@>R@$84=Z($EP^-KG@`r_6 zWI5E{P^;5I7SJz5!av`<8*$sb#--p1qht3%!S!uwFiy5 zW~q{rt`~9cpvuTW!WK3=lz|8g!Fu5|uDe{9N9^lT0j&$VJFJ1iS`ZCRqpCLa0e$w%9hwMDZ zm{ep}Z7PgIB7rf+#0oVfWXJuGaYREfDaRcBdh%!q%*o!3IsInZMkRI8PSh0` zzD}Nfs>^4jOp_9@wEgZD$`I{+iL7^9+B+Nz5VZxMz^w_HlxYj3DLHTW@k_S z2C|?;$Io9MgE7Y8p?JQd?E&}272P)e^fyZ7IlSp=<3ITyquGv+f}tpJoP0r0oQLHu zfDQ*Ir0Io~o_6%)KIN~eMYmvcnL8u7mxzDOmZZOBpL?l9qqL#o!4ay*|HE+KG*7+0 zy{gchC_1u@u>uu~RQh9kM5(@4Fd}O`(UaH6e>Ss>(PI5C%5!nMwj8+-mwo!>$7tSE z;z@wHdtbf4#m|8rM)0cT^g_=i|Kw_d+ zC9G9)hQz3R)t2jqzxw-i+!!<51M^W?q%OnvuJIb0zL&xsP!&1r?e1f|C2~yfW;6CA zII}Ja0y+$!m>r~&F(aHQ#9C!iF==uYF|dxqFA)M_Rw&8P8_3TpJbr7TNuoP^p&24F zPUH~0^WBf{uXy5hT9X6%9!=dn_2&(`_I|rTWWCxzarxr2fjr5(maiWK5qOPqCGN}> z=~O)XaVViYkNs#4(xlefL|j<)-V+xB%=SlCtm@_%Jzs0MoBpLYB+SPiQ8ZsPhJ|Iu zjRt1bk5AUcsjqGd!{F`xve->;2(t8Qx%>DVTqBv~E}y*b(>A82F~$V3kTEeYORdL# zeIK5IMk%~zg@5>mF_jVFz+_1-cacm(?%gd*X}33)rCT*hu_$)mwiwT`^;JT>RsKX~ zTt(#Ej4r6=@(Z6z<2C1s7k$o-w*Kb+Je~6nc%*c(zsjQ$dxZKh(@`U%2-p^JQY0?y=`D#JGqf;nhV5?rCzo2T1IC%nXvDtDdG+V-Si#yQ<7MS&7y|P?SQF8Sa zmrS3&)1B}lk@k@68uFH-!5!bFx>d*IKA}et$(xr|7P}l~h#g9LB4y2lW0#!}VAj`- z0>Po_e93c1$)J0V$7)T^y4%rXF?Vas-CkaQ5gG#TE!$?uSox5mV4qs(T@?z_Y%_5)#JhV;v59~ zw^)#+ESZZ#>C!=CIW*z>BJ^$B>gotA{Vo1u%5q{TJ;r=%zdvI@su$cqdQ<>7%8~+8hl4N3L!frQiB3W^7HQNg6z_d?vkPEJvsYds> zz$d)FHFW8PCn6H{CB2UM>@z1e1(7*D@la(Zty{6V2hv$9f^;Udl+uw$cV1g*c_-pR zE-G!;x2npI#f`R?BKc_@^*OuEw`It1rJtzicG~%xi;xcOfrH1IQ)arr_?IaSRp%dp zyj|;P7u*~;DIaEyRQmlW$ekx_7!%MiE`-lsEyc;Ci6*X;(c|$lCWHcr-qYZ-WlA|o z$+V$1F7r+S4UVRGb@r}NqE60J$jbgS(9wfJwSA=*}G&I-Itz@$qOsJ>NtWrD}2>} z4BHpfyR>0^mnuyrc&G@9npu>Xqj(_O!dA3R&YyIi5QqKkGumXNE0Nx72qn*U+fdB?#?|YjD<$gb!(GcBU-@iVd_wllt#SxkP_jMn{cz3JNUAY8;7+j8^@xk>7bUgHhx;_S zYTwfD;15QYy$dn7bVk#stOd434R7#IaIh;hx{4#{@KU@8t69yMlcb#Fc^h-8L|Y$s zJCo-D4t$4?;jUpPSh_F!UVyVI#7NjHZs)KBp7E&2^k=7@%5{5 z`E3Ia((koMnAd~(n0Bo?tZhBl!D@YO5*Uj!f^xrfXz15Rf5ni>r=Cf0y=J-Yj}*Cb z;Y{ufl)0<4SU`{TH4#vO@#7IJtkr{s zi7BrcLF2q~SM|r}*yqhY=*oQc%>b_Xwx%>oGO88fppBd#vCAWe09*Hz5L=7$j+^KR z+5pqi-^FJz-TpPv%pQwwxy-q)dWW}!P}|!;Mw_MyvC;bcTI(vQu??83S$1>hF^%aK zsgV}YXcvJ)yCmDVsr}m2&qNvP&fc1n5z(mHVdf$nHHuM1^Fw#F??#?9fm)D+SY4F3 z)@_^yV$0Lun-A(@YiGP<3Nz21r0)VtyKi~%nessqwJ79v=a2v&sAy!i6P0}clx^P` zBjq`GVu+Y(?vS(m`cNV&I$XTOqnCR_L63u#ET%gm}(tF2*=Kq{F#Fg+0WUS3cX ztXRs?j|?e9ZEK?>0&5l|{o=woT^vCVM0Iy@QPqOfZMXGIFJb8CTxzVyY*^iKIk?@2 zm7rrlGC8j%)ss~;3~70K>Q(jhJ?g%-cu{5wC2jy?BaT>D>+IL1jf{=$om&$dS+@(B z10P`WjbFP$DF&gk>Uj?tl!62j;?$3}>UyO-0WiX-6-y9jD4({kh}uyxZ5th0GLhcdZWgg{S%lxN}j*l3Uwy z;C@F)ug!;!$~$fWf-hJr4DZm!!Q4KAUnl<@1_3duC}@~3z$Tj|zDZ+$vsh9ak7&C_O;sDwgifu-t(2I=?&0{RA{t-PH8_fg?s|E69 zXUu7~Xm80Dq4LSm%B>%c65{^vi44FCSUY8ylJES_^@0s|qV*8;c54j@)zD+l7}mQv zhoV!$Z#txPshNwd(FB-uyL)*B<4W|5`I$gGo5CDRLYs|MJm%!%=QJ&>zEP^4;>I9# zjDfMCA2lt6!u2nqsLG~2WjOdR)|U4!7sBf+0iB)&*2?*n4UfC&aoha}h=nfRuHAYX zei$lOub_6;=6z&QEgffyPJOcbREJk%)7MGl9{*2qhXeIx)j8Q(gi2!&1Q1oe$0IM8 zm>oRJo!utajqbuya(6d^jVKb4$)5E;nt0yA@*&}d)KL%0Y!M=|!*9VraCG;A385j- zg_u6<;>zTC{UJ}8W=o(gTqIzVECRjZ)V|#P*u6ZRi!!C#@%k_*M5C%1(~cezOq43X zM;`H8`dZOvy)OrroR{ZvaCQ?~6R!mLlLpX_gM7KZy>11d^~xFDVgck8z!Gt z1x)BVDea$2FDBG_iuPOQqt`w9WG4U+u9c>uS-Umv92yMhw~J6<*t^uEli}5&_F$fb zeQ&utqPGGk_UDFm60?3)r*G7UM(GBr(7|sh`lNWskA#e5WU5zP$E&Olb=KflVqUi~ zv|i)QZ_u-m`?&3jBt3m(X;^>g|0V3Rb=e-)U+p=aCAY>XzpLixLbKx8-xL8sa1hICZM{<(JXLA zGWGJcfjHw&YV&+eWrU;{Im{~`TIyNyF0y%i4tByMDTYx=YN7U@MZ{|A#M zj+bI_O@kFlIO_7p^+y=OL|rDtnyceSPcnG%CPWCX@0C)wFcw#nq#dAkFjzAVqe`ye)dd6 zH&v>L<98_jq&+>JBY)STad0OF#P1Rr5gFOB6P zYYRH%qBIXw1$xeW4lzBUe*d$BO~enkuurD$V`^i-sT#Ikunb)rC83qf)PSW2vZ}(@ zp>T_v4ZXPNfB8RpmVjL%vPVy~WxKL;RM!3?w@9S6=WUSY%zcXu`-pRGx z4~ZyM$eR$q!K$6DposFWs&hVFpe*z%-^h%0bL<7h1-9(T`@rvACOW zzZ|}IDIa4nS~S*Jxy3-8Ld374=Tyl+|4RV^mvwgH_)E;)Gs-VxCsdqtub<>Q;b;zT zIpn?yZSZ7RwRogqcY)QD?et4k-=sT?L*$zcEyGk9K%6Fy5RpzqQLYvl_Ag|XZlWvUWt+3kjm1P&18De?i! zlpGPTGcNS5ZFBH09sJd;Fcv!fJU(M#?kd^U{W@71XGEi0RWVw0C$qofUy+MTT%B+q zhu4-@u)Ya@+_2(&pbJg4^L|d)IdQPW15=}5{^K9DAT&eq?_*D#i2rrC*_7emSvs(3 zOl&?V(x`)ptPrGL|94$YJWP#9)hd-o*Z|%`gQ7B+wDmL=;*gMS z|7ztYostZkRzoqnR68^N+oT5e9h0JH?MUhz8>l!OJct9fiFqRg+fKOa%Jh_MT=>3! zNm^a?(>8dXcY>hAj$QJ$uF0 zr9SH^r62(QG*jy84tn67r(v;J=itZ%M7JeQbXd%+Co-jxYwXCM(`I=5TW((m>!u2W z0!ff=vlnH`7>iEtRwKMi_`F;i(3LGV-#${7wfJjA_5`%@96a^82<^ay=Vg6;Hs8w( zkb`D&@<%A*2O+#Q#!nlf8`)~{fxG>uy2|nW_>I#g>D=gDI4}nkSTY&Eo&JgHn$c0; zEdh|zHtL7hUf51xAlx%npJOt5$hAz6y>^=eO%=9pv>fo{%B#nt z@}-)!asF$hI1QfK-7L0+ruE6Qecdz zCG)XOW#zj6IdlTfL7qGBhw)lz9wRkRyQqB93i^#73hp@RQ3S;1-M8-FdF(IADY9us zfKASVqsYbe-e9pcphyvjnUW69M|I0DkcHd!);rnW+8)xOq^xIalE(7Bbj1m_&V%-W%h@cJDe5V+No=IbN;4pMTyt=J^yc4B z8Jb#0rg#~5J=f3Zq#Axxd_^5)VKUCv=~)vO9d8>#L(vfpz%yI{KC7$VZXQPjdRWgz zFT~bSnroY~Qt2V^9Yey>pLDNoLgJ>t`0$OIzOW8JQ(>LOSEZ<;UZQK{{(g^sz9X`#!i-NQY+KISNN|@vxxSN+B|GMwMt9BQw&Q!O%!EYT6e8%>{ zf7|y2liZ_izg5}Z09FEFKB2v>a&54Y>gItnVTR#sOp8Oa${fWQVG9;++FH{}9Zgx0 zqwX3g@Nlvp;4m3UNKA7Y2VB33lRcwr(LS(d_2YTA;z`+c@}a^z^O$c)n#c~eKd#lG zSJDiHmtK8BXAsNy(R8EmUuyA5_D@QVY8BcJUT12399a5UGyoo33G(?eZAehB;>z_u zy1VYErk-t0N2IABf|MvyrGtiEq)A8QC!q8i6$l^@dJ!;6mxzE6K&48Dh#(MJL_|tx z0s%rGG||vIgycoN_pbNudiUP-{(bZBnRU*dIWx2N_w8@*nHZ-Tqxsrv!jgGux_Od^ z8kI^C7YF5*$`X&}=IKJZy2TYcFip7;Qz@}zb(ROyC9IkZGlv%&Ot1N|gS(E)ux4W^ zG#ejd8=W;k-gV5m+9u=%rHuLnI4o=fob_`C2DwPZMG=#(x-p1;^$HFiW%q!po^2rK zJ{t=MvR03HujiH@ScMy#7YV+f8j7x<{-|h4lprm70l)6|Ib-vRQ+c@^YIV&cvOam1 zoFfcP-{q0$UdDAuZ0BIyl;&P;&{?h_i%F9Yjj;rpk;z?Y<%Wa#IqbDuzfS`*RJKyU zcFCUNnm(wo3Qe^L;0cm^tMi1H^bn(dt{)%EnK4zP%i7+slFonvS}Xbz#9CIZ&LEwfhNL^3oKW4+xScKCx@NmNc9*N?35jEeA4xptS-SX(w<&%L z*Oy&N0X#q4ueLeqvDVdyRA1{SIdV7Z;X1NBtaHGf!>O~80^B!8Fm3a6oAWdSZC?=~ zu$Tg_mg56EVq>K<;={&!Lj|~<-?jFNb>5WBNf|RhbNBr<=DE`3ODHf~Vy|mvtVpw+T+K;BT&^apIP5hZ`%7R-N2bF+7PT zSk-2H%MaJXgmlLqQ6Dbx2;tOX7jgiX4?nL6nYQG8`OKsQ=XqWkNV`S_URkkmg6+O}6AE4BjqDU?92=gfG7X{Gv;7{D<3lOQZaWnQEW|uk(LinAf!)#gN}urRQ)g5@Z^gz2>A_6gHgzGKCc1bV{Ps;mDvYl|0xL zB))~6V%zF!U7{0J8248qb3&-cWjCWDjnji{2mDpfz=MU?DBjt4iOjj+DOS4149l7b zV9wr?Gx&@i3xre<4@4MWoG*8OdHqW;nYv8g=TaxYNWRr&d@p=jpO|)*_9raB8mC1t z6jj~T1HwEQJ z6Wv~#c$^B_qc=?R&7<|d1W~cRg_MNgqQXj*xcS$j&zO9ka%f=T_+~kZv!HoZJM0YW zm*fXpYu=pXWXc@0r_KJ1G@EYf#L*eX?^<0QKF7yqYioJ-qS3~)2PWA=`RY-29Yd3o zK+`*l;U&IUL>^rqrNl-KssD#%z0!d`N63ieXEUBCmB`$GCUUQjh_-LM>RSLIPaWVR zAeBVTz05JE#wTaEx2eW)!2pYhNhKO(QNP8JsBYJeHcajwQ@M5eEzic;9{@cZfEbmO zy+unL6}}aoZ@MXkO->jT(4&3iiiBvb-I0g|79aRjjDL=j!DkWW6<)~Hz39C!LJH*I znA)1&mQ&j>Z_4+Ct@+GT|FB`u?2ld7z!ElNMK!wlIr3FOmV=Ip3Zxy!zY)vhEXQtD z5rGQhz>o>)+0*)(0rZrpnrt^W6b(mjh_JI~*FsCO1<*b$R{-3oFUz=j_QMwgjAaQ;fu>4rOw(!&L3ZZyMj8_pX1O#$fTaWiX3YRh&H4I%%y(37a zZ(l1(L3e`4f62(E33SQMZdF4rQF?j*jWm_!#jaROi{|& zjw#5M-=qh+1NlKKn3)R;Sn25NyyKO3EK|2tk&ER_15(8O*nAl4%IYYi?!a3i`%w8` zaO2vvu_A~9q!f~HUF8F?%-7eaxPxp#?dVag@(((pVg;@uk@N|FU6wLsljrzJW;RKyn zVcVAc)>6cbQur)*)ubxxOd`VS6%0bjp=eSU<^?GPK8~y|(73c}W*C}kG+gg%%mI;$ zyBSs+cmhmGe{~Qtc2e@Hi$Xs2oxRyDpW^1`!pmi?>0XoFf>bT7O%xWKq>aw*8_(Lt zGjfI`3xS7CXpnrR8m(#e#xu*k5l`n1H8~c)Z8^y%W^SzJ=Q{^=IlUFt7At|eBgu`$5dVvK1W#3YY>?ufA z$i9qL0`uLX2m1c;QS?zX+|;lMwjZS5JR!+Bw9@@Vd7j~CQLXR<>3Li@#TH;?U|GPb02)Zx(C%u+xd3W$|Fpo_$~cHMDnrpv=>V}3`5T2D|)f%rgE}4oMTEk{u z@F?k}icEk*uoHPooXp=ARxxJYxaMbxfo|wxDL(NwUKvGN#C_hU+wGpiVl7fsf7oJk zuK}zZuR!&TO=+`8(WMJdj&@Q3yj24|CWk9mTSq`uo{}fe5v}gPe^68JSY?Z@@ z^?jph?4)xOZr$jC!dNW;w*b!(;=c?>@HkvyTK9CuwtA;$>1+b`U*uTCh1t=NXr&Hp zHD`Sxta7y}fjD}g=3Cc-fr{@BBxb*CMb-H&UQOXs)^`u|F2Hd~22b8=70bB$C^|-- z_ru$U1M~}@>uoQD^I_V&!3}sfj~-{EGE)|^Y<3=PJ>4E^D#Zie%pMtGm!r0nY#puo zc}C6XwAbr{3=xu;33oW9&@7l`e^g;>4^&!c*xSLOFAS+fW8Ms*M zMJu-DkAur&VuT2(oy~|`Lej~E%kg-BxS!!$998#`sP&_TZJxfnEr60zfc(*8Kf1#( z7azHr#Q~3@w%fMj8D*y*n%3yfpwew&eqLYziV3a7{N6*rB2c(qgqzK5LUEOAZ(xF_ zf9HH6vzeJGK(Qt$fCh2Vv+}dm1He^n`oGyZ%2>!GykayQb}ftsDm1s%Z^Q-~KXbP! zUhN9>JN1B*;`2djX|LXifp_Av!;|<1&v|b7 zcj6Kq__eOX>OD=gpSq53kgTlv!fh|@dDLXeTEXj&Vabyzx1U?nzmZPPzNNbvFK=Qj zR1a5xgt4tS`^= zX!HIH3z7{mpFPnB27@bp)4sc%JyZy>eEIFZxb#U>ex1uV3j%A6m35o?oGTU8sdPi# z8-F!~2YmR=oU3M3x%I*#Sh7^Uo5VOY#Rx|QXP>-5`|hK`9V4&7e#Q?=E5XuQynaaq zKAMbsYxes3V^`u>${dTZl9xPJ6w@bW;rFi__c$Qq>OTGS^ZAYz&3OLmPOPv{e5*!+ z&jUb$kaDFoMy$*j2=VD2107&`^ar?77QD-ifi5|Qz)p0&*L!`PI_Gk8;DE<%ODmIJ zQ6NwS{KBoEHc^BZbp6d`%qgq7Ayg^*wQZr@$1{~0hBAG2?X5}Y>==Qj!$Hpx;T(q^ zQG(LX3l$qjg|MZi#>1YxJ`(mFFq}Dl)q2Td*tgwEWaR_V-OcSzUPy}tEHVWugtVA{ zSr}vqm2uX2D~o7#@yWY#*+QpCv1{=N1Rz zum{XgIvZt`b&I7IGMlPZRL{U@W)`^wgt^1{sl8le^!)2Tod4Qm2}v6Hw*lgKVeq{OGSy^835v6HO{FvBo&3 zA&KfoHItc-$8pfMPTR?NZNq_Eb$-&qyvW3(_K%dqLGjO)V|I|cvRR?5mu$}x!3aDG zoRWqP8i+o;detwU$5d-RACr6rY8Lf)VeUqHMQHqJ5b1LUa*risfwjt{F{Q94zVW3b zB}}mVhydq^?^kH4TABpm7Rs39MvJ4q)a~cH_H?d`vBJN`IkuobIct>c2O7c6jI_Bu z_OyW!$Y&_}`eoIte*^}_L+GEUcoIEo!)Me|KH@r9Q)hIdnX^7XcbUB=Du6X{@(XY7 zCT&gi@1Y~n;|NW>EXp8!7GQGm{A=4shRs6(cFF~pF*Y0L!^-*NTZ$gkZJI->uXg+h z3Pbehn;nG}4nQtl{u=vr`X#|G*Ywl-?wXd}3v7k`&ObxHYf8SMXMJZ59L~EOK~WSb zldeE@pE#wjUG~J-v2oI$4Kg2byMmO)D4m7g9QFRB+jM=K^aGZh9JY+u-R1eqUOoQO zzR~F*X`zCvDYB{Vd>TG=X?nyBF?jhtV!b(4$!66!9hRJTGeRpuE4$Quc)8f(e&(nb ztku<17+jETK5YIUg)tAgFErK;zS0>JUHMm{m}`bOiY|{C6Ce*4Bj-Ge_u4DnHEd#( z;EK@u{Pr%v@APF^O4X>~OW8@9QuDWTVBl+J!yp$)67NsVV|Kj~7I-W)?V#N<=?-jW z)kYBXh}^7z#ySO`Rq$t(TMo&a%hld8w=@8I*y3zw7Uyg)s>`~vn&*;GX*WP8*ixw( ziogU&(V1L$@ZOaL1sVVy=t0Wolh#W`KZcCHSoUHHaaNe;T~$0N`N=cAv20Jiv{c1~ zj5f$AJ@r!mzn#3|!YiOXOPI9aum%f)7F=v)V`BgYOK)y&W*!$cC70*HjZOY?w6lZ% z_V+7oUJQJ0SYIP}z{hi=h^Il;io_E&)n@Tw+C_rqC+1YUj_AR$Y2A8%EQ#o%rl!8J zyZfC`Y{33L3U|ZdaD`4VMitZ-L9AbdOO^}2PjxJ5Y2ESa>Q411v%B!9zBdOc7Tv@& z`dz85t!->-atL1r4(gVf4NvEu(Va|mZeP?yjzXYNfnNd+2jf?iqJTSM43$lO$&`q2 zfb2hqpD20FPbmR03Z|eme*K1DX~8RQY!EI!LOW&+e+=v%pvkfiyHO|IMHQA9|7}cF zqoyo-(hO;)GT0kWjFGP>Nl>jlU+I3!(lW7s472)OfaUer!-CI~n*bZILp&Fe0R00^ z>LgrZ4F6jGY|jg zj+q#Lb`sgkhmvE8?RtaAxlDOSW_;aSGyLjWTtl-ZNKvQIvQb%ecBw*8=ht8qfAIU? z__qW5_y4#U1s-zA76Il|7C4ywo*&)G_G|sUp+q)uZF@Vr(yA(t-5=lhHy=?fp|oLr zFc_?)qT=vjo_A?c+W*g|b$_hl@96Zs{G?-r~eNf`^dC)iulz?y_@;Ju4*>a LGt;foc8d8gad)*? literal 0 HcmV?d00001 diff --git a/dex/testing/xmr/image-2.png b/dex/testing/xmr/image-2.png new file mode 100644 index 0000000000000000000000000000000000000000..70e06a3734577813a56ba5b08bcf76177fa27172 GIT binary patch literal 30440 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 Date: Fri, 24 May 2024 06:30:29 +0800 Subject: [PATCH 3/9] harness,docs: add new function - alpha_get_transactions_details get transaction development details from monerod including tx lock time; which is different from 'locked'/'unlocked' which refer to when a tx has 10 confirmations. Tx locktime can be any number of blocks in the future. --- dex/testing/xmr/README.md | 9 +++++++-- dex/testing/xmr/harness.sh | 15 +++++++++++++-- dex/testing/xmr/monero_functions.inc | 26 +++++++++++--------------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/dex/testing/xmr/README.md b/dex/testing/xmr/README.md index 42ba87663f..30df8ab7cd 100644 --- a/dex/testing/xmr/README.md +++ b/dex/testing/xmr/README.md @@ -80,10 +80,15 @@ Commands Help: -------------- alpha_get_transactions -- get transaction details for one or more txid +- get transactions info for one or more txid +- inputs: + - tx_hashes - hash1,hash2,hash3,... + +alpha_get_transactions_details + +- get transaction development details from monerod including tx lock time - inputs: - tx_hashes - hash1,hash2,hash3,... - - decode_as_json - Optional - returns more detail but in a escaped raw json format alpha_info diff --git a/dex/testing/xmr/harness.sh b/dex/testing/xmr/harness.sh index 404c5fb60c..ea96f0d15d 100755 --- a/dex/testing/xmr/harness.sh +++ b/dex/testing/xmr/harness.sh @@ -111,7 +111,7 @@ EOF chmod +x "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" # ----------------------------------------------------------------------------- -# Get one or more transaction details from monerod +# Get one or more transactions from monerod # inputs: # - txids as hex string - "hash1,hash2,hash3,..." # - decode_as_json - defaults to false @@ -123,6 +123,17 @@ EOF chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions" # ----------------------------------------------------------------------------- +# Get one or more transaction development details from monerod including tx lock time +# inputs: +# - txids as hex string - "hash1,hash2,hash3,..." +cat > "${HARNESS_CTL_DIR}/alpha_get_transactions_details" </dev/null | jq '.txs[] | .as_json | fromjson' +EOF +chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions_details" +# ----------------------------------------------------------------------------- + # Mempool info cat > "${HARNESS_CTL_DIR}/alpha_transaction_pool" < "${HARNESS_CTL_DIR}/fred_transfer_to" < Date: Thu, 6 Jun 2024 22:19:29 +0800 Subject: [PATCH 4/9] harness: update per @JoeGruffins suggestions - Rename monero_functions.inc -> monero_functions - Tidy extra whitespace at eol for harness.sh & monero_functions. --- dex/testing/xmr/harness.sh | 74 +++++++++---------- ...{monero_functions.inc => monero_functions} | 16 ++-- 2 files changed, 45 insertions(+), 45 deletions(-) rename dex/testing/xmr/{monero_functions.inc => monero_functions} (99%) diff --git a/dex/testing/xmr/harness.sh b/dex/testing/xmr/harness.sh index ea96f0d15d..876fef3e2e 100755 --- a/dex/testing/xmr/harness.sh +++ b/dex/testing/xmr/harness.sh @@ -2,7 +2,7 @@ # Tmux script that sets up an XMR regtest harness with one node 'alpha' and 3 # wallets 'fred', 'bill' & 'charlie'. Charlie also has a View-Only sibling. -############################################################################### +################################################################################ # Development ################################################################################ @@ -12,7 +12,7 @@ # Monero RPC functions ################################################################################ -source monero_functions.inc +source monero_functions ################################################################################ # Start up @@ -78,8 +78,8 @@ mkdir -p "${HARNESS_CTL_DIR}" mkdir -p "${ALPHA_DATA_DIR}" touch "${ALPHA_REGTEST_CFG}" # currently empty -# make available from the harness-ctl dir -cp monero_functions.inc ${HARNESS_CTL_DIR} +# make available from the harness-ctl dir +cp monero_functions ${HARNESS_CTL_DIR} # Background watch mining in window ??? by default: # 'export NOMINER="1"' or uncomment this line to disable @@ -93,7 +93,7 @@ echo "Writing ctl scripts" # Daemon info cat > "${HARNESS_CTL_DIR}/alpha_info" < "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" </dev/null EOF chmod +x "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" @@ -117,7 +117,7 @@ chmod +x "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" # - decode_as_json - defaults to false cat > "${HARNESS_CTL_DIR}/alpha_get_transactions" </dev/null EOF chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions" @@ -128,7 +128,7 @@ chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions" # - txids as hex string - "hash1,hash2,hash3,..." cat > "${HARNESS_CTL_DIR}/alpha_get_transactions_details" </dev/null | jq '.txs[] | .as_json | fromjson' EOF chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions_details" @@ -137,7 +137,7 @@ chmod +x "${HARNESS_CTL_DIR}/alpha_get_transactions_details" # Mempool info cat > "${HARNESS_CTL_DIR}/alpha_transaction_pool" </dev/null EOF chmod +x "${HARNESS_CTL_DIR}/alpha_transaction_pool" @@ -148,7 +148,7 @@ chmod +x "${HARNESS_CTL_DIR}/alpha_transaction_pool" # - number of blocks to mine cat > "${HARNESS_CTL_DIR}/mine-to-bill" < "${HARNESS_CTL_DIR}/fred_transfer_to" < "${HARNESS_CTL_DIR}/bill_transfer_to" < "${HARNESS_CTL_DIR}/charlie_transfer_to" < "${HARNESS_CTL_DIR}/fred_balance" < "${HARNESS_CTL_DIR}/bill_balance" < "${HARNESS_CTL_DIR}/charlie_balance" < "${HARNESS_CTL_DIR}/fred_refresh_wallet" < "${HARNESS_CTL_DIR}/bill_refresh_wallet" < "${HARNESS_CTL_DIR}/charlie_refresh_wallet" < "${HARNESS_CTL_DIR}/fred_incoming_transfers" < "${HARNESS_CTL_DIR}/charlie_incoming_transfers" < "${HARNESS_CTL_DIR}/fred_export_outputs" < "${HARNESS_CTL_DIR}/charlie_export_outputs" < "${HARNESS_CTL_DIR}/charlie_view_export_outputs" < "${HARNESS_CTL_DIR}/fred_export_key_images" < "${HARNESS_CTL_DIR}/charlie_export_key_images" < "${HARNESS_CTL_DIR}/fred_build_tx" < "${HARNESS_CTL_DIR}/charlie_build_tx" < "${NODES_ROOT}/harness-ctl/help" < Date: Thu, 8 Aug 2024 14:35:33 +0800 Subject: [PATCH 5/9] harness: Add a new wallet-less monero-wallet-rpc server instance New monero-wallet-rpc server with no attached wallet. This is for programmatically creating/generating and using a new wallet. The wallet will be generated in "own" dir but can be named what- ever you need: "alice", "Bob", etc. --- dex/testing/xmr/harness.sh | 78 +++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/dex/testing/xmr/harness.sh b/dex/testing/xmr/harness.sh index 876fef3e2e..ee70a4ccf1 100755 --- a/dex/testing/xmr/harness.sh +++ b/dex/testing/xmr/harness.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash # Tmux script that sets up an XMR regtest harness with one node 'alpha' and 3 # wallets 'fred', 'bill' & 'charlie'. Charlie also has a View-Only sibling. +# +# There is now a new monero-wallet-rpc server with no attached wallet. This is +# for programmatically creating and using a new wallet. The wallet will be gen- +# erated in "own" directory but can be named whatever you need - maybe "alice", +# "Bob" or "carol" ################################################################################ # Development @@ -38,6 +43,7 @@ export FRED_WALLET_RPC_PORT="28084" export BILL_WALLET_RPC_PORT="28184" export CHARLIE_WALLET_RPC_PORT="28284" export CHARLIE_VIEW_WALLET_RPC_PORT="28384" +export OWN_WALLET_RPC_PORT="28484" # wallet seeds, passwords & primary addresses FRED_WALLET_SEED="vibrate fever timber cuffs hunter terminal dilute losing light because nabbing slower royal brunt gnaw vats fishing tipsy toxic vague oscar fudge mice nasty light" @@ -63,6 +69,7 @@ FRED_WALLET_DIR="${NODES_ROOT}/wallets/fred" BILL_WALLET_DIR="${NODES_ROOT}/wallets/bill" CHARLIE_WALLET_DIR="${NODES_ROOT}/wallets/charlie" CHARLIE_VIEW_WALLET_DIR="${NODES_ROOT}/wallets/charlie_view" +OWN_WALLET_DIR="${NODES_ROOT}/wallets/own" HARNESS_CTL_DIR="${NODES_ROOT}/harness-ctl" ALPHA_DATA_DIR="${NODES_ROOT}/alpha" ALPHA_REGTEST_CFG="${ALPHA_DATA_DIR}/alpha.conf" @@ -74,14 +81,15 @@ mkdir -p "${FRED_WALLET_DIR}" mkdir -p "${BILL_WALLET_DIR}" mkdir -p "${CHARLIE_WALLET_DIR}" mkdir -p "${CHARLIE_VIEW_WALLET_DIR}" +mkdir -p "${OWN_WALLET_DIR}" mkdir -p "${HARNESS_CTL_DIR}" mkdir -p "${ALPHA_DATA_DIR}" -touch "${ALPHA_REGTEST_CFG}" # currently empty +touch "${ALPHA_REGTEST_CFG}" -# make available from the harness-ctl dir -cp monero_functions ${HARNESS_CTL_DIR} +# make available from the harness-ctl dir +cp monero_functions ${HARNESS_CTL_DIR} -# Background watch mining in window ??? by default: +# Background watch mining in window 7 by default: # 'export NOMINER="1"' or uncomment this line to disable #NOMINER="1" @@ -111,7 +119,7 @@ EOF chmod +x "${HARNESS_CTL_DIR}/alpha_sendrawtransaction" # ----------------------------------------------------------------------------- -# Get one or more transactions from monerod +# Get one or more transaction details from monerod # inputs: # - txids as hex string - "hash1,hash2,hash3,..." # - decode_as_json - defaults to false @@ -372,6 +380,7 @@ cat > "${HARNESS_CTL_DIR}/wallets" < "${NODES_ROOT}/harness-ctl/quit" < "${ALPHA_REGTEST_CFG}" < Date: Wed, 7 Aug 2024 13:41:12 +0900 Subject: [PATCH 6/9] internal: Add basic simnet xmr swap. --- .gitignore | 1 + client/cmd/bisonw-desktop/go.mod | 2 +- go.mod | 4 +- go.sum | 4 + internal/adaptorsigs/dcr/dcr.go | 31 + internal/cmd/xmrswap/main.go | 1221 ++++++++++++++++++++++++++++++ 6 files changed, 1261 insertions(+), 2 deletions(-) create mode 100644 internal/adaptorsigs/dcr/dcr.go create mode 100644 internal/cmd/xmrswap/main.go diff --git a/.gitignore b/.gitignore index c3460f2bd2..f26121387e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ client/cmd/translationsreport/worksheets server/cmd/dexadm/dexadm server/cmd/geogame/geogame internal/libsecp256k1/secp256k1 +internal/cmd/xmrswap/xmrswap diff --git a/client/cmd/bisonw-desktop/go.mod b/client/cmd/bisonw-desktop/go.mod index e3310d95b6..70780d387b 100644 --- a/client/cmd/bisonw-desktop/go.mod +++ b/client/cmd/bisonw-desktop/go.mod @@ -1,6 +1,6 @@ module decred.org/dcrdex/client/cmd/bisonw-desktop -go 1.21 +go 1.21.10 replace decred.org/dcrdex => ../../.. diff --git a/go.mod b/go.mod index 5536b5c291..c4bab877fe 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( decred.org/dcrwallet/v4 v4.1.1 fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240625142744-cc26860b4026 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcutil v1.1.5 @@ -43,6 +44,7 @@ require ( github.com/decred/go-socks v1.1.0 github.com/decred/slog v1.2.0 github.com/decred/vspd/types/v2 v2.1.0 + github.com/dev-warrior777/go-monero v0.1.0 github.com/dgraph-io/badger v1.6.2 github.com/ethereum/go-ethereum v1.14.8 github.com/fatih/color v1.16.0 @@ -51,6 +53,7 @@ require ( github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000 github.com/go-chi/chi/v5 v5.0.1 github.com/gorilla/websocket v1.5.1 + github.com/haven-protocol-org/monero-go-utils v0.0.0-20211126154105-058b2666f217 github.com/huandu/skiplist v1.2.0 github.com/jessevdk/go-flags v1.5.0 github.com/jrick/logrotate v1.0.0 @@ -81,7 +84,6 @@ require ( github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.2 // indirect github.com/aead/siphash v1.0.1 // indirect - github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect diff --git a/go.sum b/go.sum index e532245662..084ee366e7 100644 --- a/go.sum +++ b/go.sum @@ -322,6 +322,8 @@ github.com/decred/vspd/types/v2 v2.1.0 h1:cUVlmHPeLVsksPRnr2WHsmC2t1Skl6g1WH0Hmp github.com/decred/vspd/types/v2 v2.1.0/go.mod h1:2xnNqedkt9GuL+pK8uIzDxqYxFlwLRflYFJH64b76n0= github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dev-warrior777/go-monero v0.1.0 h1:86JJRiYNk2muLJFWM6kgb7qZh1gE/ETjvjHSW+3Rn5c= +github.com/dev-warrior777/go-monero v0.1.0/go.mod h1:DSf/k695WklA8lTl1EzqNLhtKP3BE3x5KU6tsPrXp4k= github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= @@ -613,6 +615,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/haven-protocol-org/monero-go-utils v0.0.0-20211126154105-058b2666f217 h1:CflMOYZHhaBo+7up92oOYcesIG+qDCAKdJo+niKBFWM= +github.com/haven-protocol-org/monero-go-utils v0.0.0-20211126154105-058b2666f217/go.mod h1:vSMDRpw62HGWO1Fi9DQwfgs4e3JCbt475GWY/W5DQZI= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= diff --git a/internal/adaptorsigs/dcr/dcr.go b/internal/adaptorsigs/dcr/dcr.go new file mode 100644 index 0000000000..2471034a75 --- /dev/null +++ b/internal/adaptorsigs/dcr/dcr.go @@ -0,0 +1,31 @@ +package dcr + +import "github.com/decred/dcrd/txscript/v4" + +func LockRefundTxScript(kal, kaf []byte, locktime int64) ([]byte, error) { + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_IF). + AddOp(txscript.OP_2). + AddData(kal). + AddData(kaf). + AddOp(txscript.OP_2). + AddOp(txscript.OP_CHECKMULTISIG). + AddOp(txscript.OP_ELSE). + AddInt64(locktime). + AddOp(txscript.OP_CHECKSEQUENCEVERIFY). + AddOp(txscript.OP_DROP). + AddData(kaf). + AddOp(txscript.OP_CHECKSIG). + AddOp(txscript.OP_ENDIF). + Script() +} + +func LockTxScript(kal, kaf []byte) ([]byte, error) { + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_2). + AddData(kal). + AddData(kaf). + AddOp(txscript.OP_2). + AddOp(txscript.OP_CHECKMULTISIG). + Script() +} diff --git a/internal/cmd/xmrswap/main.go b/internal/cmd/xmrswap/main.go new file mode 100644 index 0000000000..bbe538bbf4 --- /dev/null +++ b/internal/cmd/xmrswap/main.go @@ -0,0 +1,1221 @@ +//go:build libsecp256k1 + +package main + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "fmt" + "math" + "math/big" + "net/http" + "os" + "path/filepath" + "time" + + "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/config" + dcradaptor "decred.org/dcrdex/internal/adaptorsigs/dcr" + "decred.org/dcrdex/internal/libsecp256k1" + "decred.org/dcrwallet/v4/rpc/client/dcrwallet" + dcrwalletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types" + "github.com/agl/ed25519/edwards25519" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrec" + "github.com/decred/dcrd/dcrec/edwards/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/rpcclient/v8" + "github.com/decred/dcrd/txscript/v4" + "github.com/decred/dcrd/txscript/v4/sign" + "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/dcrd/wire" + "github.com/decred/slog" + "github.com/dev-warrior777/go-monero/rpc" + "github.com/fatih/color" + "github.com/haven-protocol-org/monero-go-utils/base58" +) + +// TODO: Verification at all stages has not been implemented yet. + +// fieldIntSize is the size of a field element encoded +// as bytes. +const ( + fieldIntSize = 32 + dcrAmt = 7_000_000 // atoms + xmrAmt = 1_000 // 1e12 units + dumbFee = int64(6000) +) + +var ( + homeDir = os.Getenv("HOME") + dextestDir = filepath.Join(homeDir, "dextest") + bobDir = filepath.Join(dextestDir, "xmr", "wallets", "bob") + curve = edwards.Edwards() +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + } +} + +type walletClient = dcrwallet.Client + +type combinedClient struct { + *rpcclient.Client + *walletClient + chainParams *chaincfg.Params +} + +func newCombinedClient(nodeRPCClient *rpcclient.Client, chainParams *chaincfg.Params) *combinedClient { + return &combinedClient{ + nodeRPCClient, + dcrwallet.NewClient(dcrwallet.RawRequestCaller(nodeRPCClient), chainParams), + chainParams, + } +} + +type client struct { + xmr *rpc.Client + dcr *combinedClient + + kbvf, kbsf, kbvl, kbsl, vkbv *edwards.PrivateKey + pkbsf, pkbs *edwards.PublicKey + kaf, kal *secp256k1.PrivateKey + pkal, pkaf, pkasl, pkbsl *secp256k1.PublicKey + kbsfDleag, kbslDleag [libsecp256k1.ProofLen]byte + lockTxEsig [libsecp256k1.CTLen]byte + lockTx *wire.MsgTx + vIn int +} + +func newRPCWallet(settings map[string]string, logger dex.Logger, net dex.Network) (*combinedClient, error) { + certs, err := os.ReadFile(settings["rpccert"]) + if err != nil { + return nil, fmt.Errorf("TLS certificate read error: %w", err) + } + + cfg := &rpcclient.ConnConfig{ + Host: settings["rpclisten"], + Endpoint: "ws", + User: settings["rpcuser"], + Pass: settings["rpcpass"], + Certificates: certs, + DisableConnectOnNew: true, // don't start until Connect + } + if cfg.User == "" { + cfg.User = "user" + } + if cfg.Pass == "" { + cfg.Pass = "pass" + } + + nodeRPCClient, err := rpcclient.New(cfg, nil) + if err != nil { + return nil, fmt.Errorf("error setting up rpc client: %w", err) + } + + var params *chaincfg.Params + switch net { + case dex.Simnet: + params = chaincfg.SimNetParams() + case dex.Testnet: + params = chaincfg.TestNet3Params() + case dex.Mainnet: + params = chaincfg.MainNetParams() + default: + return nil, fmt.Errorf("unknown network ID: %d", uint8(net)) + } + + return newCombinedClient(nodeRPCClient, params), nil +} + +func newClient(ctx context.Context, xmrAddr, dcrNode string) (*client, error) { + xmr := rpc.New(rpc.Config{ + Address: xmrAddr, + Client: &http.Client{}, + }) + + settings, err := config.Parse(filepath.Join(dextestDir, "dcr", dcrNode, fmt.Sprintf("%s.conf", dcrNode))) + if err != nil { + return nil, err + } + settings["account"] = "default" + + dcr, err := newRPCWallet(settings, dex.StdOutLogger(dcrNode, slog.LevelTrace), dex.Simnet) + if err != nil { + return nil, err + } + + err = dcr.Connect(ctx, false) + if err != nil { + return nil, err + } + + return &client{ + xmr: xmr, + dcr: dcr, + }, nil +} + +// reverse reverses a byte string. +func reverse(s *[32]byte) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +// bigIntToEncodedBytes converts a big integer into its corresponding +// 32 byte little endian representation. +func bigIntToEncodedBytes(a *big.Int) *[32]byte { + s := new([32]byte) + if a == nil { + return s + } + // Caveat: a can be longer than 32 bytes. + aB := a.Bytes() + + // If we have a short byte string, expand + // it so that it's long enough. + aBLen := len(aB) + if aBLen < fieldIntSize { + diff := fieldIntSize - aBLen + for i := 0; i < diff; i++ { + aB = append([]byte{0x00}, aB...) + } + } + + for i := 0; i < fieldIntSize; i++ { + s[i] = aB[i] + } + + // Reverse the byte string --> little endian after + // encoding. + reverse(s) + + return s +} + +// encodedBytesToBigInt converts a 32 byte little endian representation of +// an integer into a big, big endian integer. +func encodedBytesToBigInt(s *[32]byte) *big.Int { + // Use a copy so we don't screw up our original + // memory. + sCopy := new([32]byte) + for i := 0; i < fieldIntSize; i++ { + sCopy[i] = s[i] + } + reverse(sCopy) + + bi := new(big.Int).SetBytes(sCopy[:]) + + return bi +} + +// scalarAdd adds two scalars. +func scalarAdd(a, b *big.Int) *big.Int { + feA := bigIntToFieldElement(a) + feB := bigIntToFieldElement(b) + sum := new(edwards25519.FieldElement) + + edwards25519.FeAdd(sum, feA, feB) + sumArray := new([32]byte) + edwards25519.FeToBytes(sumArray, sum) + + return encodedBytesToBigInt(sumArray) +} + +// bigIntToFieldElement converts a big little endian integer into its corresponding +// 40 byte field representation. +func bigIntToFieldElement(a *big.Int) *edwards25519.FieldElement { + aB := bigIntToEncodedBytes(a) + fe := new(edwards25519.FieldElement) + edwards25519.FeFromBytes(fe, aB) + return fe +} + +func sumPubKeys(pubA, pubB *edwards.PublicKey) *edwards.PublicKey { + pkSumX, pkSumY := curve.Add(pubA.GetX(), pubA.GetY(), pubB.GetX(), pubB.GetY()) + return edwards.NewPublicKey(pkSumX, pkSumY) +} + +// Convert the DCR value to atoms. +func toAtoms(v float64) uint64 { + return uint64(math.Round(v * 1e8)) +} + +// createNewXMRWallet uses the "own" wallet to create a new xmr wallet from keys +// and open it. Can only create one wallet at a time. +func createNewXMRWallet(ctx context.Context, genReq rpc.GenerateFromKeysRequest) (*rpc.Client, error) { + xmrChecker := rpc.New(rpc.Config{ + Address: "http://127.0.0.1:28484/json_rpc", + Client: &http.Client{}, + }) + + _, err := xmrChecker.GenerateFromKeys(ctx, &genReq) + if err != nil { + return nil, fmt.Errorf("unable to generate wallet: %v", err) + } + + openReq := rpc.OpenWalletRequest{ + Filename: genReq.Filename, + } + + err = xmrChecker.OpenWallet(ctx, &openReq) + if err != nil { + return nil, err + } + return xmrChecker, nil +} + +type prettyLogger struct { + c *color.Color +} + +func (cl prettyLogger) Write(p []byte) (n int, err error) { + return cl.c.Fprint(os.Stdout, string(p)) +} + +func run(ctx context.Context) error { + pl := prettyLogger{c: color.New(color.FgGreen)} + log := dex.NewLogger("T", dex.LevelInfo, pl) + + log.Info("Running success.") + if err := success(ctx); err != nil { + return err + } + log.Info("Success completed without error.") + log.Info("------------------") + log.Info("Running alice bails before xmr init.") + if err := aliceBailsBeforeXmrInit(ctx); err != nil { + return err + } + log.Info("Alice bails before xmr init completed without error.") + log.Info("------------------") + log.Info("Running refund.") + if err := refund(ctx); err != nil { + return err + } + log.Info("Refund completed without error.") + log.Info("------------------") + log.Info("Running bob bails after xmr init.") + if err := bobBailsAfterXmrInit(ctx); err != nil { + return err + } + log.Info("Bob bails after xmr init completed without error.") + return nil +} + +// generateDleag starts the trade by creating some keys. +func (c *client) generateDleag(ctx context.Context) (pkbsf *edwards.PublicKey, kbvf *edwards.PrivateKey, + pkaf *secp256k1.PublicKey, dleag [libsecp256k1.ProofLen]byte, err error) { + fail := func(err error) (*edwards.PublicKey, *edwards.PrivateKey, + *secp256k1.PublicKey, [libsecp256k1.ProofLen]byte, error) { + return nil, nil, nil, [libsecp256k1.ProofLen]byte{}, err + } + // This private key is shared with bob and becomes half of the view key. + c.kbvf, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + // Not shared. Becomes half the spend key. The pubkey is shared. + c.kbsf, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + c.pkbsf = c.kbsf.PubKey() + + // Not shared. This is used for all dcr signatures. Using a wallet + // address because funds may go here in the case of success. Any address + // would work for the spendTx though. + kafAddr, err := c.dcr.GetNewAddress(ctx, "default") + if err != nil { + return fail(err) + } + kafWIF, err := c.dcr.DumpPrivKey(ctx, kafAddr) + if err != nil { + return fail(err) + } + c.kaf = secp256k1.PrivKeyFromBytes(kafWIF.PrivKey()) + + // Share this pubkey with the other party. + c.pkaf = c.kaf.PubKey() + + c.kbsfDleag, err = libsecp256k1.Ed25519DleagProve(c.kbsf) + if err != nil { + return fail(err) + } + + c.pkasl, err = secp256k1.ParsePubKey(c.kbsfDleag[:33]) + if err != nil { + return fail(err) + } + + return c.pkbsf, c.kbvf, c.pkaf, c.kbsfDleag, nil +} + +// generateLockTxn creates even more keys and some transactions. +func (c *client) generateLockTxn(ctx context.Context, pkbsf *edwards.PublicKey, + kbvf *edwards.PrivateKey, pkaf *secp256k1.PublicKey, kbsfDleag [libsecp256k1.ProofLen]byte) (refundSig, + lockRefundTxScript, lockTxScript []byte, refundTx, spendRefundTx *wire.MsgTx, lockTxVout int, + pkbs *edwards.PublicKey, vkbv *edwards.PrivateKey, dleag [libsecp256k1.ProofLen]byte, err error) { + fail := func(err error) ([]byte, []byte, []byte, *wire.MsgTx, *wire.MsgTx, int, *edwards.PublicKey, *edwards.PrivateKey, [libsecp256k1.ProofLen]byte, error) { + return nil, nil, nil, nil, nil, 0, nil, nil, [libsecp256k1.ProofLen]byte{}, err + } + c.kbsfDleag = kbsfDleag + c.pkasl, err = secp256k1.ParsePubKey(c.kbsfDleag[:33]) + if err != nil { + return fail(err) + } + c.kbvf = kbvf + c.pkbsf = pkbsf + c.pkaf = pkaf + + // This becomes the other half of the view key. + c.kbvl, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + // This becomes the other half of the spend key and is shared. + c.kbsl, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + // This kept private. This is used for all dcr signatures. + c.kal, err = secp256k1.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + pkal := c.kal.PubKey() + + // This is the full xmr view key and is shared. Alice can also calculate + // it using kbvl. + vkbvBig := scalarAdd(c.kbvf.GetD(), c.kbvl.GetD()) + vkbvBig.Mod(vkbvBig, curve.N) + var vkbvBytes [32]byte + vkbvBig.FillBytes(vkbvBytes[:]) + c.vkbv, _, err = edwards.PrivKeyFromScalar(vkbvBytes[:]) + if err != nil { + return fail(fmt.Errorf("unable to create vkbv: %v", err)) + } + + // The public key for the xmr spend key. No party knows the full private + // key yet. + c.pkbs = sumPubKeys(c.kbsl.PubKey(), c.pkbsf) + + // The lock tx is the initial dcr transaction. + lockTxScript, err = dcradaptor.LockTxScript(pkal.SerializeCompressed(), c.pkaf.SerializeCompressed()) + if err != nil { + return fail(err) + } + + scriptAddr, err := stdaddr.NewAddressScriptHashV0(lockTxScript, c.dcr.chainParams) + if err != nil { + return fail(fmt.Errorf("error encoding script address: %w", err)) + } + p2shLockScriptVer, p2shLockScript := scriptAddr.PaymentScript() + // Add the transaction output. + txOut := &wire.TxOut{ + Value: dcrAmt, + Version: p2shLockScriptVer, + PkScript: p2shLockScript, + } + unfundedLockTx := wire.NewMsgTx() + unfundedLockTx.AddTxOut(txOut) + txBytes, err := unfundedLockTx.Bytes() + if err != nil { + return fail(err) + } + + fundRes, err := c.dcr.FundRawTransaction(ctx, hex.EncodeToString(txBytes), "default", dcrwalletjson.FundRawTransactionOptions{}) + if err != nil { + return fail(err) + } + + txBytes, err = hex.DecodeString(fundRes.Hex) + if err != nil { + return fail(err) + } + + c.lockTx = wire.NewMsgTx() + if err = c.lockTx.FromBytes(txBytes); err != nil { + return fail(err) + } + for i, out := range c.lockTx.TxOut { + if bytes.Equal(out.PkScript, p2shLockScript) { + c.vIn = i + break + } + } + + durationLocktime := int64(2) // blocks + // Unable to use time for tests as this is multiples of 512 seconds. + // durationLocktime := int64(10) // seconds * 512 + // durationLocktime |= wire.SequenceLockTimeIsSeconds + + // The refund tx does not outright refund but moves funds to the refund + // script's address. This is signed by both parties before the initial tx. + lockRefundTxScript, err = dcradaptor.LockRefundTxScript(pkal.SerializeCompressed(), c.pkaf.SerializeCompressed(), durationLocktime) + if err != nil { + return fail(err) + } + + scriptAddr, err = stdaddr.NewAddressScriptHashV0(lockRefundTxScript, c.dcr.chainParams) + if err != nil { + return fail(fmt.Errorf("error encoding script address: %w", err)) + } + p2shScriptVer, p2shScript := scriptAddr.PaymentScript() + txOut = &wire.TxOut{ + Value: dcrAmt - dumbFee, + Version: p2shScriptVer, + PkScript: p2shScript, + } + refundTx = wire.NewMsgTx() + refundTx.AddTxOut(txOut) + h := c.lockTx.TxHash() + op := wire.NewOutPoint(&h, uint32(c.vIn), 0) + txIn := wire.NewTxIn(op, dcrAmt, nil) + refundTx.AddTxIn(txIn) + + // This sig must be shared with Alice. + refundSig, err = sign.RawTxInSignature(refundTx, c.vIn, lockTxScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return fail(err) + } + + // SpendRefundTx is used in the final refund. Alice can sign it after a + // time and send wherever. Bob must use a signature that will reveal his + // half of the xmr key. + newAddr, err := c.dcr.GetNewAddress(ctx, "default") + if err != nil { + return fail(err) + } + p2AddrScriptVer, p2AddrScript := newAddr.PaymentScript() + txOut = &wire.TxOut{ + Value: dcrAmt - dumbFee - dumbFee, + Version: p2AddrScriptVer, + PkScript: p2AddrScript, + } + spendRefundTx = wire.NewMsgTx() + spendRefundTx.AddTxOut(txOut) + h = refundTx.TxHash() + op = wire.NewOutPoint(&h, 0, 0) + txIn = wire.NewTxIn(op, dcrAmt, nil) + txIn.Sequence = uint32(durationLocktime) + spendRefundTx.AddTxIn(txIn) + spendRefundTx.Version = wire.TxVersionTreasury + + c.kbslDleag, err = libsecp256k1.Ed25519DleagProve(c.kbsl) + if err != nil { + return fail(err) + } + c.pkbsl, err = secp256k1.ParsePubKey(c.kbslDleag[:33]) + if err != nil { + return fail(err) + } + + return refundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, c.vIn, c.pkbs, c.vkbv, c.kbslDleag, nil +} + +// generateRefundSigs signs the refund tx and shares the spendRefund esig that +// allows bob to spend the refund tx. +func (c *client) generateRefundSigs(refundTx, spendRefundTx *wire.MsgTx, vIn int, lockTxScript, lockRefundTxScript []byte, dleag [libsecp256k1.ProofLen]byte) (esig [libsecp256k1.CTLen]byte, refundSig []byte, err error) { + fail := func(err error) ([libsecp256k1.CTLen]byte, []byte, error) { + return [libsecp256k1.CTLen]byte{}, nil, err + } + c.kbslDleag = dleag + c.vIn = vIn + c.pkbsl, err = secp256k1.ParsePubKey(c.kbslDleag[:33]) + if err != nil { + return fail(err) + } + + hash, err := txscript.CalcSignatureHash(lockRefundTxScript, txscript.SigHashAll, spendRefundTx, 0, nil) + if err != nil { + return fail(err) + } + + var h chainhash.Hash + copy(h[:], hash) + esig, err = libsecp256k1.EcdsaotvesEncSign(c.kaf, c.pkbsl, h) + if err != nil { + return fail(err) + } + + // Share with bob. + refundSig, err = sign.RawTxInSignature(refundTx, c.vIn, lockTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return fail(err) + } + + return esig, refundSig, nil +} + +// initDcr is the first transaction to happen and creates a dcr transaction. +func (c *client) initDcr(ctx context.Context) (spendTx *wire.MsgTx, err error) { + fail := func(err error) (*wire.MsgTx, error) { + return nil, err + } + pkaslAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1(0, stdaddr.Hash160(c.pkaf.SerializeCompressed()), c.dcr.chainParams) + if err != nil { + return fail(err) + } + p2AddrScriptVer, p2AddrScript := pkaslAddr.PaymentScript() + + txOut := &wire.TxOut{ + Value: dcrAmt - dumbFee, + Version: p2AddrScriptVer, + PkScript: p2AddrScript, + } + spendTx = wire.NewMsgTx() + spendTx.AddTxOut(txOut) + h := c.lockTx.TxHash() + op := wire.NewOutPoint(&h, uint32(c.vIn), 0) + txIn := wire.NewTxIn(op, dcrAmt, nil) + spendTx.AddTxIn(txIn) + + tx, complete, err := c.dcr.SignRawTransaction(ctx, c.lockTx) + if err != nil { + return fail(err) + } + if !complete { + return fail(errors.New("lock tx sign not complete")) + } + + _, err = c.dcr.SendRawTransaction(ctx, tx, false) + if err != nil { + return fail(fmt.Errorf("unable to send lock tx: %v", err)) + } + return spendTx, nil +} + +// initXmr sends an xmr transaciton. Alice can only do this after confirming the +// dcr transaction. +func (c *client) initXmr(ctx context.Context, vkbv *edwards.PrivateKey, pkbs *edwards.PublicKey) error { + c.vkbv = vkbv + c.pkbs = pkbs + var fullPubKey []byte + fullPubKey = append(fullPubKey, c.pkbs.SerializeCompressed()...) + fullPubKey = append(fullPubKey, c.vkbv.PubKey().SerializeCompressed()...) + + sharedAddr := base58.EncodeAddr(18, fullPubKey) + + dest := rpc.Destination{ + Amount: xmrAmt, + Address: string(sharedAddr), + } + sendReq := rpc.TransferRequest{ + Destinations: []rpc.Destination{dest}, + } + + sendRes, err := c.xmr.Transfer(ctx, &sendReq) + if err != nil { + return err + } + fmt.Printf("xmr sent\n%+v\n", *sendRes) + return nil +} + +// sendLockTxSig allows Alice to redeem the dcr. If bob does not send this alice +// can eventually take his btc. Otherwise bob refunding will reveal his half of +// the xmr spend key allowing Alice to refund. +func (c *client) sendLockTxSig(lockTxScript []byte, spendTx *wire.MsgTx) (esig [libsecp256k1.CTLen]byte, err error) { + hash, err := txscript.CalcSignatureHash(lockTxScript, txscript.SigHashAll, spendTx, 0, nil) + if err != nil { + return [libsecp256k1.CTLen]byte{}, err + } + + var h chainhash.Hash + copy(h[:], hash) + + esig, err = libsecp256k1.EcdsaotvesEncSign(c.kal, c.pkasl, h) + if err != nil { + return [libsecp256k1.CTLen]byte{}, err + } + c.lockTxEsig = esig + return esig, nil +} + +// redeemDcr redeems the dcr, revealing a signature that reveals half of the xmr +// spend key. +func (c *client) redeemDcr(ctx context.Context, esig [libsecp256k1.CTLen]byte, lockTxScript []byte, spendTx *wire.MsgTx) (kalSig []byte, err error) { + kasl := secp256k1.PrivKeyFromBytes(c.kbsf.Serialize()) + kalSig, err = libsecp256k1.EcdsaotvesDecSig(kasl, esig) + if err != nil { + return nil, err + } + kalSig = append(kalSig, byte(txscript.SigHashAll)) + + kafSig, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return nil, err + } + + spendSig, err := txscript.NewScriptBuilder(). + AddData(kalSig). + AddData(kafSig). + AddData(lockTxScript). + Script() + + spendTx.TxIn[0].SignatureScript = spendSig + + tx, err := c.dcr.SendRawTransaction(ctx, spendTx, false) + if err != nil { + return nil, err + } + fmt.Println(tx) + + return kalSig, nil +} + +// redeemXmr redeems xmr by creating a new xmr wallet with the complete spend +// and view private keys. +func (c *client) redeemXmr(ctx context.Context, kalSig []byte) (*rpc.Client, error) { + kaslRecovered, err := libsecp256k1.EcdsaotvesRecEncKey(c.pkasl, c.lockTxEsig, kalSig[:len(kalSig)-1]) + if err != nil { + return nil, err + } + + kbsfRecovered, _, err := edwards.PrivKeyFromScalar(kaslRecovered.Serialize()) + if err != nil { + return nil, fmt.Errorf("unable to recover kbsf: %v", err) + } + vkbsBig := scalarAdd(c.kbsl.GetD(), kbsfRecovered.GetD()) + vkbsBig.Mod(vkbsBig, curve.N) + var vkbsBytes [32]byte + vkbsBig.FillBytes(vkbsBytes[:]) + vkbs, _, err := edwards.PrivKeyFromScalar(vkbsBytes[:]) + if err != nil { + return nil, fmt.Errorf("unable to create vkbs: %v", err) + } + + var fullPubKey []byte + fullPubKey = append(fullPubKey, vkbs.PubKey().Serialize()...) + fullPubKey = append(fullPubKey, c.vkbv.PubKey().Serialize()...) + walletAddr := base58.EncodeAddr(18, fullPubKey) + walletFileName := fmt.Sprintf("%s_spend", walletAddr) + + var vkbvBytes [32]byte + copy(vkbvBytes[:], c.vkbv.Serialize()) + + reverse(&vkbsBytes) + reverse(&vkbvBytes) + + genReq := rpc.GenerateFromKeysRequest{ + Filename: walletFileName, + Address: walletAddr, + SpendKey: hex.EncodeToString(vkbsBytes[:]), + ViewKey: hex.EncodeToString(vkbvBytes[:]), + } + + xmrChecker, err := createNewXMRWallet(ctx, genReq) + if err != nil { + return nil, err + } + + return xmrChecker, nil +} + +// startRefund starts the refund and can be done by either party. +func (c *client) startRefund(ctx context.Context, kalSig, kafSig, lockTxScript []byte, refundTx *wire.MsgTx) error { + refundSig, err := txscript.NewScriptBuilder(). + AddData(kalSig). + AddData(kafSig). + AddData(lockTxScript). + Script() + + refundTx.TxIn[0].SignatureScript = refundSig + + _, err = c.dcr.SendRawTransaction(ctx, refundTx, false) + if err != nil { + return err + } + return nil +} + +// refundDcr returns dcr to bob while revealing his half of the xmr spend key. +func (c *client) refundDcr(ctx context.Context, spendRefundTx *wire.MsgTx, esig [libsecp256k1.CTLen]byte, lockRefundTxScript []byte) (kafSig []byte, err error) { + kasf := secp256k1.PrivKeyFromBytes(c.kbsl.Serialize()) + kafSig, err = libsecp256k1.EcdsaotvesDecSig(kasf, esig) + if err != nil { + return nil, err + } + kafSig = append(kafSig, byte(txscript.SigHashAll)) + + kalSig, err := sign.RawTxInSignature(spendRefundTx, 0, lockRefundTxScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return nil, err + } + refundSig, err := txscript.NewScriptBuilder(). + AddData(kalSig). + AddData(kafSig). + AddOp(txscript.OP_TRUE). + AddData(lockRefundTxScript). + Script() + + spendRefundTx.TxIn[0].SignatureScript = refundSig + + _, err = c.dcr.SendRawTransaction(ctx, spendRefundTx, false) + if err != nil { + return nil, err + } + // TODO: Confirm refund happened. + return kafSig, nil +} + +// refundXmr refunds xmr but cannot happen without the dcr refund happening first. +func (c *client) refundXmr(ctx context.Context, kafSig []byte, esig [libsecp256k1.CTLen]byte) (*rpc.Client, error) { + kbslRecovered, err := libsecp256k1.EcdsaotvesRecEncKey(c.pkbsl, esig, kafSig[:len(kafSig)-1]) + if err != nil { + return nil, err + } + + kaslRecovered, _, err := edwards.PrivKeyFromScalar(kbslRecovered.Serialize()) + if err != nil { + return nil, fmt.Errorf("unable to recover kasl: %v", err) + } + vkbsBig := scalarAdd(c.kbsf.GetD(), kaslRecovered.GetD()) + vkbsBig.Mod(vkbsBig, curve.N) + var vkbsBytes [32]byte + vkbsBig.FillBytes(vkbsBytes[:]) + vkbs, _, err := edwards.PrivKeyFromScalar(vkbsBytes[:]) + if err != nil { + return nil, fmt.Errorf("unable to create vkbs: %v", err) + } + + var fullPubKey []byte + fullPubKey = append(fullPubKey, vkbs.PubKey().Serialize()...) + fullPubKey = append(fullPubKey, c.vkbv.PubKey().Serialize()...) + walletAddr := base58.EncodeAddr(18, fullPubKey) + walletFileName := fmt.Sprintf("%s_spend", walletAddr) + + var vkbvBytes [32]byte + copy(vkbvBytes[:], c.vkbv.Serialize()) + + reverse(&vkbsBytes) + reverse(&vkbvBytes) + + genReq := rpc.GenerateFromKeysRequest{ + Filename: walletFileName, + Address: walletAddr, + SpendKey: hex.EncodeToString(vkbsBytes[:]), + ViewKey: hex.EncodeToString(vkbvBytes[:]), + } + + xmrChecker, err := createNewXMRWallet(ctx, genReq) + if err != nil { + return nil, err + } + + return xmrChecker, nil +} + +// takeDcr is the punish if Bob takes too long. Alice gets the dcr while bob +// gets nothing. +func (c *client) takeDcr(ctx context.Context, lockRefundTxScript []byte, spendRefundTx *wire.MsgTx) (err error) { + newAddr, err := c.dcr.GetNewAddress(ctx, "default") + if err != nil { + return err + } + p2AddrScriptVer, p2AddrScript := newAddr.PaymentScript() + txOut := &wire.TxOut{ + Value: dcrAmt - dumbFee - dumbFee, + Version: p2AddrScriptVer, + PkScript: p2AddrScript, + } + spendRefundTx.TxOut[0] = txOut + + kafSig, err := sign.RawTxInSignature(spendRefundTx, 0, lockRefundTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return err + } + refundSig, err := txscript.NewScriptBuilder(). + AddData(kafSig). + AddOp(txscript.OP_FALSE). + AddData(lockRefundTxScript). + Script() + + spendRefundTx.TxIn[0].SignatureScript = refundSig + + _, err = c.dcr.SendRawTransaction(ctx, spendRefundTx, false) + if err != nil { + return err + } + // TODO: Confirm refund happened. + return nil +} + +// success is a successful trade. +func success(ctx context.Context) error { + alice, err := newClient(ctx, "http://127.0.0.1:28284/json_rpc", "trading1") + if err != nil { + return err + } + balReq := rpc.GetBalanceRequest{ + AccountIndex: 0, + } + xmrBal, err := alice.xmr.GetBalance(ctx, &balReq) + if err != nil { + return err + } + fmt.Printf("alice xmr balance\n%+v\n", *xmrBal) + + dcrBal, err := alice.dcr.GetBalance(ctx, "default") + if err != nil { + return err + } + dcrBeforeBal := toAtoms(dcrBal.Balances[0].Total) + fmt.Printf("alice dcr balance %v\n", dcrBeforeBal) + + bob, err := newClient(ctx, "http://127.0.0.1:28184/json_rpc", "trading2") + if err != nil { + return err + } + + // Alice generates dleag. + + pkbsf, kbvf, pkaf, aliceDleag, err := alice.generateDleag(ctx) + if err != nil { + return err + } + + // Bob generates transactions but does not send anything yet. + + _, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + if err != nil { + return fmt.Errorf("unalbe to generate lock transactions: %v", err) + } + + // Alice signs a refund script for Bob. + + _, _, err = alice.generateRefundSigs(refundTx, spendRefundTx, vIn, lockTxScript, lockRefundTxScript, bobDleag) + if err != nil { + return err + } + + // Bob initializes the swap with dcr being sent. + + spendTx, err := bob.initDcr(ctx) + if err != nil { + return err + } + + // Alice inits her monero side. + if err := alice.initXmr(ctx, vkbv, pkbs); err != nil { + return err + } + + time.Sleep(time.Second * 5) + + // Bob sends esig after confirming on chain xmr tx. + + bobEsig, err := bob.sendLockTxSig(lockTxScript, spendTx) + if err != nil { + return err + } + + // Alice redeems using the esig. + kalSig, err := alice.redeemDcr(ctx, bobEsig, lockTxScript, spendTx) + if err != nil { + return err + } + + // Prove that bob can't just sign the spend tx for the signature we need. + ks, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, bob.kal.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return err + } + if bytes.Equal(ks, kalSig) { + return errors.New("bob was able to get the correct sig without alice") + } + + // Bob redeems the xmr with the dcr signature. + xmrChecker, err := bob.redeemXmr(ctx, kalSig) + if err != nil { + return err + } + + // NOTE: This wallet must sync so may take a long time on mainnet. + // TODO: Wait for wallet sync rather than a dumb sleep. + time.Sleep(time.Second * 40) + + xmrBal, err = xmrChecker.GetBalance(ctx, &balReq) + if err != nil { + return err + } + if xmrBal.Balance != xmrAmt { + return fmt.Errorf("expected redeem xmr balance of %d but got %d", xmrAmt, xmrBal.Balance) + } + + dcrBal, err = alice.dcr.GetBalance(ctx, "default") + if err != nil { + return err + } + dcrAfterBal := toAtoms(dcrBal.Balances[0].Total) + wantBal := dcrBeforeBal + dcrAmt - uint64(dumbFee) + if wantBal != dcrAfterBal { + return fmt.Errorf("expected alice balance to be %d but got %d", wantBal, dcrAfterBal) + } + + return nil +} + +// aliceBailsBeforeXmrInit is a trade that fails because alice does nothing after +// Bob inits. +func aliceBailsBeforeXmrInit(ctx context.Context) error { + alice, err := newClient(ctx, "http://127.0.0.1:28284/json_rpc", "trading1") + if err != nil { + return err + } + + bob, err := newClient(ctx, "http://127.0.0.1:28184/json_rpc", "trading2") + if err != nil { + return err + } + + dcrBal, err := bob.dcr.GetBalance(ctx, "default") + if err != nil { + return err + } + dcrBeforeBal := toAtoms(dcrBal.Balances[0].Total) + + // Alice generates dleag. + + pkbsf, kbvf, pkaf, aliceDleag, err := alice.generateDleag(ctx) + if err != nil { + return err + } + + // Bob generates transactions but does not send anything yet. + + bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, _, _, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + if err != nil { + return fmt.Errorf("unalbe to generate lock transactions: %v", err) + } + + // Alice signs a refund script for Bob. + + spendRefundESig, aliceRefundSig, err := alice.generateRefundSigs(refundTx, spendRefundTx, vIn, lockTxScript, lockRefundTxScript, bobDleag) + if err != nil { + return err + } + + // Bob initializes the swap with dcr being sent. + + _, err = bob.initDcr(ctx) + if err != nil { + return err + } + + time.Sleep(time.Second * 5) + + // Bob starts the refund. + if err := bob.startRefund(ctx, bobRefundSig, aliceRefundSig, lockTxScript, refundTx); err != nil { + return err + } + + time.Sleep(time.Second * 5) + + // Bob refunds. + _, err = bob.refundDcr(ctx, spendRefundTx, spendRefundESig, lockRefundTxScript) + if err != nil { + return err + } + + time.Sleep(time.Second * 5) + + dcrBal, err = bob.dcr.GetBalance(ctx, "default") + if err != nil { + return err + } + + var initFee uint64 + for _, input := range bob.lockTx.TxIn { + initFee += uint64(input.ValueIn) + } + for _, output := range bob.lockTx.TxOut { + initFee -= uint64(output.Value) + } + + dcrAfterBal := toAtoms(dcrBal.Balances[0].Total) + wantBal := dcrBeforeBal - initFee - uint64(dumbFee)*2 + if wantBal != dcrAfterBal { + return fmt.Errorf("expected bob balance to be %d but got %d", wantBal, dcrAfterBal) + } + + return nil +} + +// refund is a failed trade where both parties have sent their initial funds and +// both get them back minus fees. +func refund(ctx context.Context) error { + alice, err := newClient(ctx, "http://127.0.0.1:28284/json_rpc", "trading1") + if err != nil { + return err + } + + bob, err := newClient(ctx, "http://127.0.0.1:28184/json_rpc", "trading2") + if err != nil { + return err + } + + // Alice generates dleag. + + pkbsf, kbvf, pkaf, aliceDleag, err := alice.generateDleag(ctx) + if err != nil { + return err + } + + // Bob generates transactions but does not send anything yet. + + bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + if err != nil { + return fmt.Errorf("unalbe to generate lock transactions: %v", err) + } + + // Alice signs a refund script for Bob. + + spendRefundESig, aliceRefundSig, err := alice.generateRefundSigs(refundTx, spendRefundTx, vIn, lockTxScript, lockRefundTxScript, bobDleag) + if err != nil { + return err + } + + // Bob initializes the swap with dcr being sent. + + _, err = bob.initDcr(ctx) + if err != nil { + return err + } + + // Alice inits her monero side. + if err := alice.initXmr(ctx, vkbv, pkbs); err != nil { + return err + } + + time.Sleep(time.Second * 5) + + // Bob starts the refund. + if err := bob.startRefund(ctx, bobRefundSig, aliceRefundSig, lockTxScript, refundTx); err != nil { + return err + } + + time.Sleep(time.Second * 5) + + // Bob refunds. + kafSig, err := bob.refundDcr(ctx, spendRefundTx, spendRefundESig, lockRefundTxScript) + if err != nil { + return err + } + + // Alice refunds. + xmrChecker, err := alice.refundXmr(ctx, kafSig, spendRefundESig) + if err != nil { + return err + } + + // NOTE: This wallet must sync so may take a long time on mainnet. + // TODO: Wait for wallet sync rather than a dumb sleep. + time.Sleep(time.Second * 40) + + balReq := rpc.GetBalanceRequest{} + bal, err := xmrChecker.GetBalance(ctx, &balReq) + if err != nil { + return err + } + if bal.Balance != xmrAmt { + return fmt.Errorf("expected refund xmr balance of %d but got %d", xmrAmt, bal.Balance) + } + fmt.Printf("new xmr wallet balance\n%+v\n", *bal) + + return nil +} + +// bobBailsAfterXmrInit is a failed trade where bob disappears after both parties +// init and alice takes all his dcr while losing her xmr. Bob gets nothing. +func bobBailsAfterXmrInit(ctx context.Context) error { + alice, err := newClient(ctx, "http://127.0.0.1:28284/json_rpc", "trading1") + if err != nil { + return err + } + + bob, err := newClient(ctx, "http://127.0.0.1:28184/json_rpc", "trading2") + if err != nil { + return err + } + + dcrBal, err := alice.dcr.GetBalance(ctx, "default") + if err != nil { + return err + } + dcrBeforeBal := toAtoms(dcrBal.Balances[0].Total) + + // Alice generates dleag. + + pkbsf, kbvf, pkaf, aliceDleag, err := alice.generateDleag(ctx) + if err != nil { + return err + } + + // Bob generates transactions but does not send anything yet. + + bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + if err != nil { + return fmt.Errorf("unalbe to generate lock transactions: %v", err) + } + + // Alice signs a refund script for Bob. + + _, aliceRefundSig, err := alice.generateRefundSigs(refundTx, spendRefundTx, vIn, lockTxScript, lockRefundTxScript, bobDleag) + if err != nil { + return err + } + + // Bob initializes the swap with dcr being sent. + + _, err = bob.initDcr(ctx) + if err != nil { + return err + } + + // Alice inits her monero side. + if err := alice.initXmr(ctx, vkbv, pkbs); err != nil { + return err + } + + time.Sleep(time.Second * 5) + + // Alice starts the refund. + if err := alice.startRefund(ctx, bobRefundSig, aliceRefundSig, lockTxScript, refundTx); err != nil { + return err + } + + // Lessen this sleep for failure. Two blocks must be mined for success. + time.Sleep(time.Second * 35) + + if err := alice.takeDcr(ctx, lockRefundTxScript, spendRefundTx); err != nil { + return err + } + + time.Sleep(time.Second * 5) + + dcrBal, err = alice.dcr.GetBalance(ctx, "default") + if err != nil { + return err + } + + dcrAfterBal := toAtoms(dcrBal.Balances[0].Total) + wantBal := dcrBeforeBal + dcrAmt - uint64(dumbFee)*2 + if wantBal != dcrAfterBal { + return fmt.Errorf("expected alice balance to be %d but got %d", wantBal, dcrAfterBal) + } + return nil +} From c0b6848ed80ba8a84b07d0ee0211b58150d1d927 Mon Sep 17 00:00:00 2001 From: martonp Date: Tue, 27 Aug 2024 16:09:52 +0200 Subject: [PATCH 7/9] xmrswap: Remove C deps for signature tweaking and dleq --- go.mod | 2 + go.sum | 4 + internal/adaptorsigs/adaptor.go | 489 +++++++++++++++++++++ internal/adaptorsigs/adaptor_test.go | 131 ++++++ internal/adaptorsigs/dcr/dcr.go | 13 +- internal/adaptorsigs/dleq.go | 113 +++++ internal/adaptorsigs/dleq_test.go | 60 +++ internal/cmd/xmrswap/main.go | 155 ++++--- internal/libsecp256k1/README.md | 10 - internal/libsecp256k1/build.sh | 10 - internal/libsecp256k1/libsecp256k1.go | 149 ------- internal/libsecp256k1/libsecp256k1_test.go | 215 --------- internal/libsecp256k1/secp256k1 | 1 - run_tests.sh | 4 - 14 files changed, 904 insertions(+), 452 deletions(-) create mode 100644 internal/adaptorsigs/adaptor.go create mode 100644 internal/adaptorsigs/adaptor_test.go create mode 100644 internal/adaptorsigs/dleq.go create mode 100644 internal/adaptorsigs/dleq_test.go delete mode 100644 internal/libsecp256k1/README.md delete mode 100755 internal/libsecp256k1/build.sh delete mode 100644 internal/libsecp256k1/libsecp256k1.go delete mode 100644 internal/libsecp256k1/libsecp256k1_test.go delete mode 160000 internal/libsecp256k1/secp256k1 diff --git a/go.mod b/go.mod index c4bab877fe..773061eef1 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.21 require ( decred.org/dcrwallet/v4 v4.1.1 + filippo.io/edwards25519 v1.0.0 fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 + github.com/athanorlabs/go-dleq v0.1.0 github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240625142744-cc26860b4026 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcutil v1.1.5 diff --git a/go.sum b/go.sum index 084ee366e7..f54e0947b4 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ decred.org/cspp/v2 v2.2.0/go.mod h1:9nO3bfvCheOPIFZw5f6sRQ42CjBFB5RKSaJ9Iq6G4MA= decred.org/dcrwallet/v4 v4.1.1 h1:imwPBboytp1PH6V8q7/JLTHiKgj/Scq9a3I1WmnJv0Y= decred.org/dcrwallet/v4 v4.1.1/go.mod h1:WxerkRcUGVreJsAI0ptCBPUujPUmWncbdYbme8Kl5r0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 h1:V2IC9t0Zj9Ur6qDbfhUuzVmIvXKFyxZXRJyigUvovs4= fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= @@ -100,6 +102,8 @@ github.com/ashanbrown/forbidigo v1.1.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBF github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= github.com/ashanbrown/makezero v0.0.0-20210308000810-4155955488a0/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= +github.com/athanorlabs/go-dleq v0.1.0 h1:0/llWZG8fz2uintMBKOiBC502zCsDA8nt8vxI73W9Qc= +github.com/athanorlabs/go-dleq v0.1.0/go.mod h1:DWry6jSD7A13MKmeZA0AX3/xBeQCXDoygX99VPwL3yU= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= diff --git a/internal/adaptorsigs/adaptor.go b/internal/adaptorsigs/adaptor.go new file mode 100644 index 0000000000..b268a231ce --- /dev/null +++ b/internal/adaptorsigs/adaptor.go @@ -0,0 +1,489 @@ +package adaptorsigs + +import ( + "errors" + "fmt" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" +) + +// AdaptorSignatureSize is the size of an encoded adaptor Schnorr signature. +const AdaptorSignatureSize = 129 + +// scalarSize is the size of an encoded big endian scalar. +const scalarSize = 32 + +var ( + // rfc6979ExtraDataV0 is the extra data to feed to RFC6979 when generating + // the deterministic nonce for the EC-Schnorr-DCRv0 scheme. This ensures + // the same nonce is not generated for the same message and key as for other + // signing algorithms such as ECDSA. + // + // It is equal to BLAKE-256([]byte("EC-Schnorr-DCRv0")). + rfc6979ExtraDataV0 = [32]byte{ + 0x0b, 0x75, 0xf9, 0x7b, 0x60, 0xe8, 0xa5, 0x76, + 0x28, 0x76, 0xc0, 0x04, 0x82, 0x9e, 0xe9, 0xb9, + 0x26, 0xfa, 0x6f, 0x0d, 0x2e, 0xea, 0xec, 0x3a, + 0x4f, 0xd1, 0x44, 0x6a, 0x76, 0x83, 0x31, 0xcb, + } +) + +// AdaptorSignature is a signature with auxillary data that commits to a hidden +// value. When an adaptor signature is combined with a corresponding signature, +// the hidden value is revealed. Alternatively, when combined with a hidden +// value, the adaptor reveals the signature. +// +// An adaptor signature is created by either doing a public or private key +// tweak of a valid schnorr signature. A private key tweak can only be done by +// a party who knows the hidden value, and a public key tweak can be done by +// a party that only knows the point on the secp256k1 curve derived by the +// multiplying the hidden value by the generator point. +// +// Generally the workflow of using adaptor signatures is the following: +// 1. Party A randomly selects a hidden value and creates a private key +// modified adaptor signature of something for which party B requires +// a valid signature. +// 2. The Party B sees the PublicTweak in the adaptor signature, and creates +// a public key tweaked adaptor signature for something that party A +// requires a valid signature. +// 3. Since party A knows the hidden value, they can use the hidden value to +// create a valid signature from the public key tweaked adaptor signature. +// 4. When the valid signature is revealed, by being posted to the blockchain, +// party B can recover the tweak and use it to decrypt the private key +// tweaked adaptor signature that party A originally sent them. +type AdaptorSignature struct { + r secp256k1.FieldVal + s secp256k1.ModNScalar + // t will always be in affine coordinates. + t secp256k1.JacobianPoint + pubKeyTweak bool +} + +// Serialize returns a serialized adaptor signature in the following format: +// +// sig[0:32] x coordinate of the point R, encoded as a big-endian uint256 +// sig[32:64] s, encoded also as big-endian uint256 +// sig[64:96] x coordinate of the point T, encoded as a big-endian uint256 +// sig[96:128] y coordinate of the point T, encoded as a big-endian uint256 +// sig[128] 1 if the adaptor was created with a public key tweak, 0 if it was +// created with a private key tweak. +func (sig *AdaptorSignature) Serialize() []byte { + var b [AdaptorSignatureSize]byte + sig.r.PutBytesUnchecked(b[0:32]) + sig.s.PutBytesUnchecked(b[32:64]) + sig.t.X.PutBytesUnchecked(b[64:96]) + sig.t.Y.PutBytesUnchecked(b[96:128]) + if sig.pubKeyTweak { + b[128] = 1 + } else { + b[128] = 0 + } + return b[:] +} + +func ParseAdaptorSignature(b []byte) (*AdaptorSignature, error) { + if len(b) != AdaptorSignatureSize { + str := fmt.Sprintf("malformed signature: wrong size: %d", len(b)) + return nil, errors.New(str) + } + + var r secp256k1.FieldVal + if overflow := r.SetBytes((*[32]byte)(b[0:32])); overflow > 0 { + str := "invalid signature: r >= field prime" + return nil, errors.New(str) + } + + var s secp256k1.ModNScalar + if overflow := s.SetBytes((*[32]byte)(b[32:64])); overflow > 0 { + str := "invalid signature: s >= group order" + return nil, errors.New(str) + } + + var t secp256k1.JacobianPoint + if overflow := t.X.SetBytes((*[32]byte)(b[64:96])); overflow > 0 { + str := "invalid signature: t.x >= field prime" + return nil, errors.New(str) + } + + if overflow := t.Y.SetBytes((*[32]byte)(b[96:128])); overflow > 0 { + str := "invalid signature: t.y >= field prime" + return nil, errors.New(str) + } + + t.Z.SetInt(1) + + pubKeyTweak := b[128] == byte(1) + + return &AdaptorSignature{ + r: r, + s: s, + t: t, + pubKeyTweak: pubKeyTweak, + }, nil +} + +// schnorrAdaptorVerify verifies that the adaptor signature will result in a +// valid signature when decrypted with the tweak. +func schnorrAdaptorVerify(sig *AdaptorSignature, hash []byte, pubKey *secp256k1.PublicKey) error { + // The algorithm for producing a EC-Schnorr-DCRv0 signature is as follows: + // This deviates from the original algorithm in step 7. + // + // 1. Fail if m is not 32 bytes + // 2. Fail if Q is not a point on the curve + // 3. Fail if r >= p + // 4. Fail if s >= n + // 5. e = BLAKE-256(r || m) (Ensure r is padded to 32 bytes) + // 6. Fail if e >= n + // 7. R = s*G + e*Q - T + // 8. Fail if R is the point at infinity + // 9. Fail if R.y is odd + // 10. Verified if R.x == r + + // Step 1. + // + // Fail if m is not 32 bytes + if len(hash) != scalarSize { + str := fmt.Sprintf("wrong size for message (got %v, want %v)", + len(hash), scalarSize) + return errors.New(str) + } + + // Step 2. + // + // Fail if Q is not a point on the curve + if !pubKey.IsOnCurve() { + str := "pubkey point is not on curve" + return errors.New(str) + } + + // Step 3. + // + // Fail if r >= p + // + // Note this is already handled by the fact r is a field element. + + // Step 4. + // + // Fail if s >= n + // + // Note this is already handled by the fact s is a mod n scalar. + + // Step 5. + // + // e = BLAKE-256(r || m) (Ensure r is padded to 32 bytes) + var commitmentInput [scalarSize * 2]byte + sig.r.PutBytesUnchecked(commitmentInput[0:scalarSize]) + copy(commitmentInput[scalarSize:], hash[:]) + commitment := blake256.Sum256(commitmentInput[:]) + + // Step 6. + // + // Fail if e >= n + var e secp256k1.ModNScalar + if overflow := e.SetBytes(&commitment); overflow != 0 { + str := "hash of (R || m) too big" + return errors.New(str) + } + + // Step 7. + // + // R = s*G + e*Q - T + var Q, R, sG, eQ, encryptedR secp256k1.JacobianPoint + pubKey.AsJacobian(&Q) + secp256k1.ScalarBaseMultNonConst(&sig.s, &sG) + secp256k1.ScalarMultNonConst(&e, &Q, &eQ) + secp256k1.AddNonConst(&sG, &eQ, &R) + tInv := sig.t + tInv.Y.Negate(1) + secp256k1.AddNonConst(&R, &tInv, &encryptedR) + + // Step 8. + // + // Fail if R is the point at infinity + if (encryptedR.X.IsZero() && encryptedR.Y.IsZero()) || encryptedR.Z.IsZero() { + str := "calculated R point is the point at infinity" + return errors.New(str) + } + + // Step 9. + // + // Fail if R.y is odd + // + // Note that R must be in affine coordinates for this check. + encryptedR.ToAffine() + if encryptedR.Y.IsOdd() { + str := "calculated encrypted R y-value is odd" + return errors.New(str) + } + + // Step 10. + // + // Verified if R.x == r + // + // Note that R must be in affine coordinates for this check. + if !sig.r.Equals(&encryptedR.X) { + str := "calculated R point was not given R" + return errors.New(str) + } + + return nil +} + +// Verify checks that the adaptor signature, when decrypted using the ke +func (sig *AdaptorSignature) Verify(hash []byte, pubKey *secp256k1.PublicKey) error { + if sig.pubKeyTweak { + return fmt.Errorf("only priv key tweaked adaptors can be verified") + } + + return schnorrAdaptorVerify(sig, hash, pubKey) +} + +// Decrypt returns a valid schnorr signature if the tweak is correct. +// This may not be a valid signature if the tweak is incorrect. The caller can +// use Verify to make sure it is a valid signature. +func (sig *AdaptorSignature) Decrypt(tweak *secp256k1.ModNScalar) (*schnorr.Signature, error) { + var expectedT secp256k1.JacobianPoint + secp256k1.ScalarBaseMultNonConst(tweak, &expectedT) + expectedT.ToAffine() + if !expectedT.X.Equals(&sig.t.X) { + return nil, fmt.Errorf("tweak X does not match expected") + } + if !expectedT.Y.Equals(&sig.t.Y) { + return nil, fmt.Errorf("tweak Y does not match expected") + } + + s := new(secp256k1.ModNScalar).Add(tweak) + if !sig.pubKeyTweak { + s.Negate() + } + s.Add(&sig.s) + + decryptedSig := schnorr.NewSignature(&sig.r, s) + return decryptedSig, nil +} + +// RecoverTweak recovers the tweak using the decrypted signature. +func (sig *AdaptorSignature) RecoverTweak(validSig *schnorr.Signature) (*secp256k1.ModNScalar, error) { + if !sig.pubKeyTweak { + return nil, fmt.Errorf("only pub key tweaked sigs can be recovered") + } + + _, s := parseSig(validSig) + + t := new(secp256k1.ModNScalar).NegateVal(&sig.s).Add(s) + + // Verify the recovered tweak + var expectedT secp256k1.JacobianPoint + secp256k1.ScalarBaseMultNonConst(t, &expectedT) + expectedT.ToAffine() + if !expectedT.X.Equals(&sig.t.X) { + return nil, fmt.Errorf("recovered tweak does not match expected") + } + if !expectedT.Y.Equals(&sig.t.Y) { + return nil, fmt.Errorf("recovered tweak does not match expected") + } + + return t, nil +} + +// PublicTweak returns the hidden value multiplied by the generator point. +func (sig *AdaptorSignature) PublicTweak() *secp256k1.JacobianPoint { + T := sig.t + return &T +} + +// schnorrEncryptedSign creates an adaptor signature by modifying the nonce in +// the commitment to be the sum of the nonce and the tweak. If the resulting +// signature is summed with the tweak, a valid signature is produced. +func schnorrEncryptedSign(privKey, nonce *secp256k1.ModNScalar, hash []byte, T *secp256k1.JacobianPoint) (*AdaptorSignature, error) { + // The algorithm for producing an encrypted EC-Schnorr-DCRv0 is as follows: + // The deviations from the original algorithm are in step 5 and 6. + // + // G = curve generator + // n = curve order + // d = private key + // m = message + // r, s = signature + // t = hidden value + // T = t * G + // + // 1. Fail if m is not 32 bytes + // 2. Fail if d = 0 or d >= n + // 3. Use RFC6979 to generate a deterministic nonce k in [1, n-1] + // parameterized by the private key, message being signed, extra data + // that identifies the scheme, and an iteration count + // 4. R = kG + // 5. Repeat from step 3 if R + T is odd + // 6. r = (R + T).x (R.x is the x coordinate of the point R + T) + // 7. e = BLAKE-256(r || m) (Ensure r is padded to 32 bytes) + // 8. Repeat from step 3 (with iteration + 1) if e >= n + // 9. s = k - e*d mod n + // 10. Return (r, s) + + // Step 4, + // + // R = kG + var R secp256k1.JacobianPoint + k := *nonce + secp256k1.ScalarBaseMultNonConst(&k, &R) + + // Step 5. + // + // Check if R + T is odd. If it is, we need to try again with a new nonce. + R.ToAffine() + var rPlusT secp256k1.JacobianPoint + secp256k1.AddNonConst(T, &R, &rPlusT) + rPlusT.ToAffine() + if rPlusT.Y.IsOdd() { + return nil, fmt.Errorf("need new nonce") + } + + // Step 6. + // + // r = (R + T).x (R.x is the x coordinate of the point R + T) + r := &rPlusT.X + + // Step 7. + // + // e = BLAKE-256(r + t || m) (Ensure r is padded to 32 bytes) + var commitmentInput [scalarSize * 2]byte + r.PutBytesUnchecked(commitmentInput[0:scalarSize]) + copy(commitmentInput[scalarSize:], hash[:]) + commitment := blake256.Sum256(commitmentInput[:]) + + // Step 8. + // + // Repeat from step 1 (with iteration + 1) if e >= N + var e secp256k1.ModNScalar + if overflow := e.SetBytes(&commitment); overflow != 0 { + k.Zero() + str := "hash of (R || m) too big" + return nil, errors.New(str) + } + + // Step 9. + // + // s = k - e*d mod n + s := new(secp256k1.ModNScalar).Mul2(&e, privKey).Negate().Add(&k) + k.Zero() + + // Step 10. + // + // Return (r, s, T) + affineT := new(secp256k1.JacobianPoint) + affineT.Set(T) + affineT.ToAffine() + return &AdaptorSignature{ + r: *r, + s: *s, + t: *affineT, + pubKeyTweak: true}, nil +} + +// zeroArray zeroes the memory of a scalar array. +func zeroArray(a *[scalarSize]byte) { + for i := 0; i < scalarSize; i++ { + a[i] = 0x00 + } +} + +// PublicKeyTweakedAdaptorSig creates a public key tweaked adaptor signature. +// This is created by a party which does not know the hidden value, but knows +// the point on the secp256k1 curve derived by multiplying the hidden value by +// the generator point. The party that knows the hidden value can use it to +// create a valid signature from the adaptor signature. Then, the valid +// signature can be combined with the adaptor signature to reveal the hidden +// value. +func PublicKeyTweakedAdaptorSig(privKey *secp256k1.PrivateKey, hash []byte, T *secp256k1.JacobianPoint) (*AdaptorSignature, error) { + // The algorithm for producing an encrypted EC-Schnorr-DCRv0 is as follows: + // The deviations from the original algorithm are in step 5 and 6. + // + // G = curve generator + // n = curve order + // d = private key + // m = message + // r, s = signature + // t = hidden value + // T = t * G + // + // 1. Fail if m is not 32 bytes + // 2. Fail if d = 0 or d >= n + // 3. Use RFC6979 to generate a deterministic nonce k in [1, n-1] + // parameterized by the private key, message being signed, extra data + // that identifies the scheme, and an iteration count + // 4. R = kG + // 5. Repeat from step 3 if R + T is odd + // 6. r = (R + T).x (R.x is the x coordinate of the point R + T) + // 7. e = BLAKE-256(r || m) (Ensure r is padded to 32 bytes) + // 8. Repeat from step 3 (with iteration + 1) if e >= n + // 9. s = k - e*d mod n + // 10. Return (r, s) + + // Step 1. + // + // Fail if m is not 32 bytes + if len(hash) != scalarSize { + str := fmt.Sprintf("wrong size for message hash (got %v, want %v)", + len(hash), scalarSize) + return nil, errors.New(str) + } + + // Step 2. + // + // Fail if d = 0 or d >= n + privKeyScalar := &privKey.Key + if privKeyScalar.IsZero() { + str := "private key is zero" + return nil, errors.New(str) + } + + var privKeyBytes [scalarSize]byte + privKeyScalar.PutBytes(&privKeyBytes) + defer zeroArray(&privKeyBytes) + for iteration := uint32(0); ; iteration++ { + // Step 3. + // + // Use RFC6979 to generate a deterministic nonce k in [1, n-1] + // parameterized by the private key, message being signed, extra data + // that identifies the scheme, and an iteration count + k := secp256k1.NonceRFC6979(privKeyBytes[:], hash, rfc6979ExtraDataV0[:], + nil, iteration) + + // Steps 4-10. + sig, err := schnorrEncryptedSign(privKeyScalar, k, hash, T) + k.Zero() + if err != nil { + // Try again with a new nonce. + continue + } + + return sig, nil + } +} + +func parseSig(sig *schnorr.Signature) (r *secp256k1.FieldVal, s *secp256k1.ModNScalar) { + sigB := sig.Serialize() + r, s = new(secp256k1.FieldVal), new(secp256k1.ModNScalar) + r.SetBytes((*[32]byte)(sigB[0:32])) + s.SetBytes((*[32]byte)(sigB[32:64])) + return r, s +} + +// PrivateKeyTweakedAdaptorSig creates a private key tweaked adaptor signature. +// This is created by a party which knows the hidden value. +func PrivateKeyTweakedAdaptorSig(sig *schnorr.Signature, pubKey *secp256k1.PublicKey, t *secp256k1.ModNScalar) *AdaptorSignature { + T := new(secp256k1.JacobianPoint) + secp256k1.ScalarBaseMultNonConst(t, T) + T.ToAffine() + + r, s := parseSig(sig) + tweakedS := new(secp256k1.ModNScalar).Add2(s, t) + + return &AdaptorSignature{ + r: *r, + s: *tweakedS, + t: *T, + } +} diff --git a/internal/adaptorsigs/adaptor_test.go b/internal/adaptorsigs/adaptor_test.go new file mode 100644 index 0000000000..23354f1b23 --- /dev/null +++ b/internal/adaptorsigs/adaptor_test.go @@ -0,0 +1,131 @@ +package adaptorsigs + +import ( + "math/rand" + "testing" + "time" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" +) + +func TestAdaptorSignatureRandom(t *testing.T) { + seed := time.Now().Unix() + rng := rand.New(rand.NewSource(seed)) + defer func(t *testing.T, seed int64) { + if t.Failed() { + t.Logf("random seed: %d", seed) + } + }(t, seed) + + for i := 0; i < 100; i++ { + // Generate two private keys + var pkBuf1, pkBuf2 [32]byte + if _, err := rng.Read(pkBuf1[:]); err != nil { + t.Fatalf("failed to read random private key: %v", err) + } + if _, err := rng.Read(pkBuf2[:]); err != nil { + t.Fatalf("failed to read random private key: %v", err) + } + var privKey1Scalar, privKey2Scalar secp256k1.ModNScalar + privKey1Scalar.SetBytes(&pkBuf1) + privKey2Scalar.SetBytes(&pkBuf2) + privKey1 := secp256k1.NewPrivateKey(&privKey1Scalar) + privKey2 := secp256k1.NewPrivateKey(&privKey2Scalar) + + // Generate random hashes to sign. + var hash1, hash2 [32]byte + if _, err := rng.Read(hash1[:]); err != nil { + t.Fatalf("failed to read random hash: %v", err) + } + if _, err := rng.Read(hash2[:]); err != nil { + t.Fatalf("failed to read random hash: %v", err) + } + + // Generate random signature tweak + var tBuf [32]byte + if _, err := rng.Read(tBuf[:]); err != nil { + t.Fatalf("failed to read random private key: %v", err) + } + var tweak secp256k1.ModNScalar + tweak.SetBytes(&tBuf) + + // Sign hash1 with private key 1 + sig, err := schnorr.Sign(privKey1, hash1[:]) + if err != nil { + t.Fatalf("Sign error: %v", err) + } + + // The owner of priv key 1 knows the tweak. Sends a priv key tweaked adaptor sig + // to the owner of priv key 2. + adaptorSigPrivKeyTweak := PrivateKeyTweakedAdaptorSig(sig, privKey1.PubKey(), &tweak) + err = adaptorSigPrivKeyTweak.Verify(hash1[:], privKey1.PubKey()) + if err != nil { + t.Fatalf("verify error: %v", err) + } + + // The owner of privKey2 creates a public key tweaked adaptor sig using + // tweak * G, and sends it to the owner of privKey1. + adaptorSigPubKeyTweak, err := PublicKeyTweakedAdaptorSig(privKey2, hash2[:], adaptorSigPrivKeyTweak.PublicTweak()) + if err != nil { + t.Fatalf("PublicKeyTweakedAdaptorSig error: %v", err) + } + + // The owner of privKey1 knows the tweak, so they can decrypt the + // public key tweaked adaptor sig. + decryptedSig, err := adaptorSigPubKeyTweak.Decrypt(&tweak) + if err != nil { + t.Fatal(err) + } + + // Using the decrypted version of their sig, which has been made public, + // the owner of privKey2 can recover the tweak. + recoveredTweak, err := adaptorSigPubKeyTweak.RecoverTweak(decryptedSig) + if err != nil { + t.Fatal(err) + } + if !recoveredTweak.Equals(&tweak) { + t.Fatalf("original tweak %v != recovered %v", tweak, recoveredTweak) + } + + // Using the recovered tweak, the original priv key tweaked adaptor sig + // can be decrypted. + decryptedOriginalSig, err := adaptorSigPrivKeyTweak.Decrypt(&tweak) + if err != nil { + t.Fatal(err) + } + if valid := decryptedOriginalSig.Verify(hash1[:], privKey1.PubKey()); !valid { + t.Fatal("decrypted original sig is invalid") + } + } +} + +func RandomBytes(len int) []byte { + bytes := make([]byte, len) + _, err := rand.Read(bytes) + if err != nil { + panic("error reading random bytes: " + err.Error()) + } + return bytes +} + +func TestAdaptorSigParsing(t *testing.T) { + adaptor := &AdaptorSignature{} + adaptor.r.SetByteSlice(RandomBytes(32)) + adaptor.s.SetByteSlice(RandomBytes(32)) + adaptor.pubKeyTweak = true + + var tweak secp256k1.JacobianPoint + secp256k1.ScalarBaseMultNonConst(&adaptor.s, &tweak) + + serialized := adaptor.Serialize() + + adaptor2, err := ParseAdaptorSignature(serialized) + if err != nil { + t.Fatal(err) + } + + if !adaptor2.r.Equals(&adaptor.r) { + t.Fatal("r mismatch") + } +} diff --git a/internal/adaptorsigs/dcr/dcr.go b/internal/adaptorsigs/dcr/dcr.go index 2471034a75..e7c2ff0f73 100644 --- a/internal/adaptorsigs/dcr/dcr.go +++ b/internal/adaptorsigs/dcr/dcr.go @@ -5,27 +5,30 @@ import "github.com/decred/dcrd/txscript/v4" func LockRefundTxScript(kal, kaf []byte, locktime int64) ([]byte, error) { return txscript.NewScriptBuilder(). AddOp(txscript.OP_IF). - AddOp(txscript.OP_2). AddData(kal). + AddOp(txscript.OP_2). + AddOp(txscript.OP_CHECKSIGALTVERIFY). AddData(kaf). AddOp(txscript.OP_2). - AddOp(txscript.OP_CHECKMULTISIG). + AddOp(txscript.OP_CHECKSIGALT). AddOp(txscript.OP_ELSE). AddInt64(locktime). AddOp(txscript.OP_CHECKSEQUENCEVERIFY). AddOp(txscript.OP_DROP). AddData(kaf). - AddOp(txscript.OP_CHECKSIG). + AddOp(txscript.OP_2). + AddOp(txscript.OP_CHECKSIGALT). AddOp(txscript.OP_ENDIF). Script() } func LockTxScript(kal, kaf []byte) ([]byte, error) { return txscript.NewScriptBuilder(). - AddOp(txscript.OP_2). AddData(kal). + AddOp(txscript.OP_2). + AddOp(txscript.OP_CHECKSIGALTVERIFY). AddData(kaf). AddOp(txscript.OP_2). - AddOp(txscript.OP_CHECKMULTISIG). + AddOp(txscript.OP_CHECKSIGALT). Script() } diff --git a/internal/adaptorsigs/dleq.go b/internal/adaptorsigs/dleq.go new file mode 100644 index 0000000000..6847deab08 --- /dev/null +++ b/internal/adaptorsigs/dleq.go @@ -0,0 +1,113 @@ +package adaptorsigs + +import ( + "bytes" + "fmt" + + "decred.org/dcrdex/dex/utils" + filipEdwards "filippo.io/edwards25519" + "filippo.io/edwards25519/field" + "github.com/athanorlabs/go-dleq" + "github.com/athanorlabs/go-dleq/ed25519" + dleqEdwards "github.com/athanorlabs/go-dleq/ed25519" + "github.com/athanorlabs/go-dleq/secp256k1" + dleqSecp "github.com/athanorlabs/go-dleq/secp256k1" + dcrEdwards "github.com/decred/dcrd/dcrec/edwards/v2" + dcrSecp "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +// ProveDLEQ generates a proof that the public keys generated from the provided +// secret on the secp256k1 and edwards25519 curves are derived from the same +// secret. +func ProveDLEQ(secret []byte) ([]byte, error) { + if len(secret) != 32 { + return nil, fmt.Errorf("secret must be 32 bytes") + } + + secretCopy := make([]byte, len(secret)) + copy(secretCopy, secret) + utils.ReverseSlice(secretCopy) + secretB := [32]byte{} + copy(secretB[:], secretCopy) + + proof, err := dleq.NewProof(ed25519.NewCurve(), secp256k1.NewCurve(), secretB) + if err != nil { + return nil, err + } + + return proof.Serialize(), nil +} + +// edwardsPointsEqual checks equality of edwards curve points in the dcrec +// and go-dleq libraries. +func edwardsPointsEqual(dcrPK *dcrEdwards.PublicKey, dleqPK *dleqEdwards.PointImpl) bool { + xB := dcrPK.GetX().Bytes() + yB := dcrPK.GetY().Bytes() + utils.ReverseSlice(xB) + utils.ReverseSlice(yB) + + x := new(field.Element) + y := new(field.Element) + z := new(field.Element) + t := new(field.Element) + + x.SetBytes(xB) + y.SetBytes(yB) + z.One() + t.Multiply(x, y) + + point := filipEdwards.NewIdentityPoint() + point.SetExtendedCoordinates(x, y, z, t) + + expDLEQ := dleqEdwards.NewPoint(point) + return dleqPK.Equals(expDLEQ) +} + +// VerifyDLEQ verifies a DLEQ proof that the public keys are generated from the +// same secret. +func VerifyDLEQ(spk *dcrSecp.PublicKey, epk *dcrEdwards.PublicKey, proofB []byte) error { + edwardsCurve := dleqEdwards.NewCurve() + secpCurve := dleqSecp.NewCurve() + + proof := new(dleq.Proof) + if err := proof.Deserialize(edwardsCurve, secpCurve, proofB); err != nil { + return err + } + if err := proof.Verify(edwardsCurve, secpCurve); err != nil { + return err + } + + edwardsPoint, ok := proof.CommitmentA.(*dleqEdwards.PointImpl) + if !ok { + return fmt.Errorf("expected ed25519.Point, got %T", proof.CommitmentA) + } + if !edwardsPointsEqual(epk, edwardsPoint) { + return fmt.Errorf("ed25519 points do not match") + } + + secpPoint, ok := proof.CommitmentB.(*dleqSecp.PointImpl) + if !ok { + return fmt.Errorf("expected secp256k1.Point, got %T", proof.CommitmentB) + } + if !bytes.Equal(secpPoint.Encode(), spk.SerializeCompressed()) { + return fmt.Errorf("secp256k1 points do not match") + } + + return nil +} + +// ExtractSecp256k1PubKeyFromProof extracts the secp256k1 public key from a +// DLEQ proof. +func ExtractSecp256k1PubKeyFromProof(proofB []byte) (*dcrSecp.PublicKey, error) { + proof := new(dleq.Proof) + if err := proof.Deserialize(dleqEdwards.NewCurve(), dleqSecp.NewCurve(), proofB); err != nil { + return nil, err + } + + secpPoint, ok := proof.CommitmentB.(*dleqSecp.PointImpl) + if !ok { + return nil, fmt.Errorf("expected secp256k1.Point, got %T", proof.CommitmentB) + } + + return dcrSecp.ParsePubKey(secpPoint.Encode()) +} diff --git a/internal/adaptorsigs/dleq_test.go b/internal/adaptorsigs/dleq_test.go new file mode 100644 index 0000000000..eecbdb29a7 --- /dev/null +++ b/internal/adaptorsigs/dleq_test.go @@ -0,0 +1,60 @@ +package adaptorsigs + +import ( + "testing" + + "decred.org/dcrdex/dex/encode" + "github.com/decred/dcrd/dcrec/edwards/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +func TestDleqProof(t *testing.T) { + pubKeysFromSecret := func(secret [32]byte) (*edwards.PublicKey, *secp256k1.PublicKey) { + epk, _, err := edwards.PrivKeyFromScalar(secret[:]) + if err != nil { + t.Fatalf("PrivKeyFromScalar error: %v", err) + } + + scalarSecret := new(secp256k1.ModNScalar) + overflow := scalarSecret.SetBytes(&secret) + if overflow > 0 { + t.Fatalf("overflow: %d", overflow) + } + spk := secp256k1.NewPrivateKey(scalarSecret) + + return epk.PubKey(), spk.PubKey() + } + + var secret [32]byte + copy(secret[1:], encode.RandomBytes(31)) + + epk, spk := pubKeysFromSecret(secret) + proof, err := ProveDLEQ(secret[:]) + if err != nil { + t.Fatalf("ProveDLEQ error: %v", err) + } + err = VerifyDLEQ(spk, epk, proof) + if err != nil { + t.Fatalf("VerifyDLEQ error: %v", err) + } + + secret[31] += 1 + badEpk, badSpk := pubKeysFromSecret(secret) + err = VerifyDLEQ(badSpk, epk, proof) + if err == nil { + t.Fatalf("badSpk should not verify") + } + err = VerifyDLEQ(spk, badEpk, proof) + if err == nil { + t.Fatalf("badEpk should not verify") + } + + extractedSecp, err := ExtractSecp256k1PubKeyFromProof(proof) + if err != nil { + t.Fatalf("ExtractSecp256k1PubKeyFromProof error: %v", err) + } + + if !extractedSecp.IsEqual(spk) { + t.Fatalf("extractedSecp != spk") + } +} diff --git a/internal/cmd/xmrswap/main.go b/internal/cmd/xmrswap/main.go index bbe538bbf4..4cb7c97dd8 100644 --- a/internal/cmd/xmrswap/main.go +++ b/internal/cmd/xmrswap/main.go @@ -1,5 +1,3 @@ -//go:build libsecp256k1 - package main import ( @@ -17,8 +15,8 @@ import ( "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/config" + "decred.org/dcrdex/internal/adaptorsigs" dcradaptor "decred.org/dcrdex/internal/adaptorsigs/dcr" - "decred.org/dcrdex/internal/libsecp256k1" "decred.org/dcrwallet/v4/rpc/client/dcrwallet" dcrwalletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types" "github.com/agl/ed25519/edwards25519" @@ -27,6 +25,7 @@ import ( "github.com/decred/dcrd/dcrec" "github.com/decred/dcrd/dcrec/edwards/v2" "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" "github.com/decred/dcrd/rpcclient/v8" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/txscript/v4/sign" @@ -88,8 +87,8 @@ type client struct { pkbsf, pkbs *edwards.PublicKey kaf, kal *secp256k1.PrivateKey pkal, pkaf, pkasl, pkbsl *secp256k1.PublicKey - kbsfDleag, kbslDleag [libsecp256k1.ProofLen]byte - lockTxEsig [libsecp256k1.CTLen]byte + kbsfDleag, kbslDleag []byte + lockTxEsig *adaptorsigs.AdaptorSignature lockTx *wire.MsgTx vIn int } @@ -313,10 +312,10 @@ func run(ctx context.Context) error { // generateDleag starts the trade by creating some keys. func (c *client) generateDleag(ctx context.Context) (pkbsf *edwards.PublicKey, kbvf *edwards.PrivateKey, - pkaf *secp256k1.PublicKey, dleag [libsecp256k1.ProofLen]byte, err error) { + pkaf *secp256k1.PublicKey, dleag []byte, err error) { fail := func(err error) (*edwards.PublicKey, *edwards.PrivateKey, - *secp256k1.PublicKey, [libsecp256k1.ProofLen]byte, error) { - return nil, nil, nil, [libsecp256k1.ProofLen]byte{}, err + *secp256k1.PublicKey, []byte, error) { + return nil, nil, nil, nil, err } // This private key is shared with bob and becomes half of the view key. c.kbvf, err = edwards.GeneratePrivateKey() @@ -347,12 +346,12 @@ func (c *client) generateDleag(ctx context.Context) (pkbsf *edwards.PublicKey, k // Share this pubkey with the other party. c.pkaf = c.kaf.PubKey() - c.kbsfDleag, err = libsecp256k1.Ed25519DleagProve(c.kbsf) + c.kbsfDleag, err = adaptorsigs.ProveDLEQ(c.kbsf.Serialize()) if err != nil { return fail(err) } - c.pkasl, err = secp256k1.ParsePubKey(c.kbsfDleag[:33]) + c.pkasl, err = adaptorsigs.ExtractSecp256k1PubKeyFromProof(c.kbsfDleag) if err != nil { return fail(err) } @@ -362,14 +361,15 @@ func (c *client) generateDleag(ctx context.Context) (pkbsf *edwards.PublicKey, k // generateLockTxn creates even more keys and some transactions. func (c *client) generateLockTxn(ctx context.Context, pkbsf *edwards.PublicKey, - kbvf *edwards.PrivateKey, pkaf *secp256k1.PublicKey, kbsfDleag [libsecp256k1.ProofLen]byte) (refundSig, + kbvf *edwards.PrivateKey, pkaf *secp256k1.PublicKey, kbsfDleag []byte) (refundSig, lockRefundTxScript, lockTxScript []byte, refundTx, spendRefundTx *wire.MsgTx, lockTxVout int, - pkbs *edwards.PublicKey, vkbv *edwards.PrivateKey, dleag [libsecp256k1.ProofLen]byte, err error) { - fail := func(err error) ([]byte, []byte, []byte, *wire.MsgTx, *wire.MsgTx, int, *edwards.PublicKey, *edwards.PrivateKey, [libsecp256k1.ProofLen]byte, error) { - return nil, nil, nil, nil, nil, 0, nil, nil, [libsecp256k1.ProofLen]byte{}, err + pkbs *edwards.PublicKey, vkbv *edwards.PrivateKey, dleag []byte, bdcrpk *secp256k1.PublicKey, err error) { + + fail := func(err error) ([]byte, []byte, []byte, *wire.MsgTx, *wire.MsgTx, int, *edwards.PublicKey, *edwards.PrivateKey, []byte, *secp256k1.PublicKey, error) { + return nil, nil, nil, nil, nil, 0, nil, nil, nil, nil, err } c.kbsfDleag = kbsfDleag - c.pkasl, err = secp256k1.ParsePubKey(c.kbsfDleag[:33]) + c.pkasl, err = adaptorsigs.ExtractSecp256k1PubKeyFromProof(c.kbsfDleag) if err != nil { return fail(err) } @@ -487,7 +487,7 @@ func (c *client) generateLockTxn(ctx context.Context, pkbsf *edwards.PublicKey, refundTx.AddTxIn(txIn) // This sig must be shared with Alice. - refundSig, err = sign.RawTxInSignature(refundTx, c.vIn, lockTxScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STEcdsaSecp256k1) + refundSig, err = sign.RawTxInSignature(refundTx, c.vIn, lockTxScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STSchnorrSecp256k1) if err != nil { return fail(err) } @@ -514,27 +514,27 @@ func (c *client) generateLockTxn(ctx context.Context, pkbsf *edwards.PublicKey, spendRefundTx.AddTxIn(txIn) spendRefundTx.Version = wire.TxVersionTreasury - c.kbslDleag, err = libsecp256k1.Ed25519DleagProve(c.kbsl) + c.kbslDleag, err = adaptorsigs.ProveDLEQ(c.kbsl.Serialize()) if err != nil { return fail(err) } - c.pkbsl, err = secp256k1.ParsePubKey(c.kbslDleag[:33]) + c.pkbsl, err = adaptorsigs.ExtractSecp256k1PubKeyFromProof(c.kbslDleag) if err != nil { return fail(err) } - return refundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, c.vIn, c.pkbs, c.vkbv, c.kbslDleag, nil + return refundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, c.vIn, c.pkbs, c.vkbv, c.kbslDleag, pkal, nil } // generateRefundSigs signs the refund tx and shares the spendRefund esig that // allows bob to spend the refund tx. -func (c *client) generateRefundSigs(refundTx, spendRefundTx *wire.MsgTx, vIn int, lockTxScript, lockRefundTxScript []byte, dleag [libsecp256k1.ProofLen]byte) (esig [libsecp256k1.CTLen]byte, refundSig []byte, err error) { - fail := func(err error) ([libsecp256k1.CTLen]byte, []byte, error) { - return [libsecp256k1.CTLen]byte{}, nil, err +func (c *client) generateRefundSigs(refundTx, spendRefundTx *wire.MsgTx, vIn int, lockTxScript, lockRefundTxScript []byte, dleag []byte) (esig *adaptorsigs.AdaptorSignature, refundSig []byte, err error) { + fail := func(err error) (*adaptorsigs.AdaptorSignature, []byte, error) { + return nil, nil, err } c.kbslDleag = dleag c.vIn = vIn - c.pkbsl, err = secp256k1.ParsePubKey(c.kbslDleag[:33]) + c.pkbsl, err = adaptorsigs.ExtractSecp256k1PubKeyFromProof(c.kbslDleag) if err != nil { return fail(err) } @@ -546,13 +546,16 @@ func (c *client) generateRefundSigs(refundTx, spendRefundTx *wire.MsgTx, vIn int var h chainhash.Hash copy(h[:], hash) - esig, err = libsecp256k1.EcdsaotvesEncSign(c.kaf, c.pkbsl, h) + + jacobianBobPubKey := new(secp256k1.JacobianPoint) + c.pkbsl.AsJacobian(jacobianBobPubKey) + esig, err = adaptorsigs.PublicKeyTweakedAdaptorSig(c.kaf, h[:], jacobianBobPubKey) if err != nil { return fail(err) } // Share with bob. - refundSig, err = sign.RawTxInSignature(refundTx, c.vIn, lockTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1) + refundSig, err = sign.RawTxInSignature(refundTx, c.vIn, lockTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STSchnorrSecp256k1) if err != nil { return fail(err) } @@ -595,6 +598,7 @@ func (c *client) initDcr(ctx context.Context) (spendTx *wire.MsgTx, err error) { if err != nil { return fail(fmt.Errorf("unable to send lock tx: %v", err)) } + return spendTx, nil } @@ -611,7 +615,7 @@ func (c *client) initXmr(ctx context.Context, vkbv *edwards.PrivateKey, pkbs *ed dest := rpc.Destination{ Amount: xmrAmt, - Address: string(sharedAddr), + Address: sharedAddr, } sendReq := rpc.TransferRequest{ Destinations: []rpc.Destination{dest}, @@ -628,43 +632,52 @@ func (c *client) initXmr(ctx context.Context, vkbv *edwards.PrivateKey, pkbs *ed // sendLockTxSig allows Alice to redeem the dcr. If bob does not send this alice // can eventually take his btc. Otherwise bob refunding will reveal his half of // the xmr spend key allowing Alice to refund. -func (c *client) sendLockTxSig(lockTxScript []byte, spendTx *wire.MsgTx) (esig [libsecp256k1.CTLen]byte, err error) { +func (c *client) sendLockTxSig(lockTxScript []byte, spendTx *wire.MsgTx) (esig *adaptorsigs.AdaptorSignature, err error) { hash, err := txscript.CalcSignatureHash(lockTxScript, txscript.SigHashAll, spendTx, 0, nil) if err != nil { - return [libsecp256k1.CTLen]byte{}, err + return nil, err } var h chainhash.Hash copy(h[:], hash) - esig, err = libsecp256k1.EcdsaotvesEncSign(c.kal, c.pkasl, h) + jacobianAlicePubKey := new(secp256k1.JacobianPoint) + c.pkasl.AsJacobian(jacobianAlicePubKey) + esig, err = adaptorsigs.PublicKeyTweakedAdaptorSig(c.kal, h[:], jacobianAlicePubKey) if err != nil { - return [libsecp256k1.CTLen]byte{}, err + return nil, err } + c.lockTxEsig = esig + return esig, nil } // redeemDcr redeems the dcr, revealing a signature that reveals half of the xmr // spend key. -func (c *client) redeemDcr(ctx context.Context, esig [libsecp256k1.CTLen]byte, lockTxScript []byte, spendTx *wire.MsgTx) (kalSig []byte, err error) { +func (c *client) redeemDcr(ctx context.Context, esig *adaptorsigs.AdaptorSignature, lockTxScript []byte, spendTx *wire.MsgTx, bobDCRPK *secp256k1.PublicKey) (kalSig []byte, err error) { kasl := secp256k1.PrivKeyFromBytes(c.kbsf.Serialize()) - kalSig, err = libsecp256k1.EcdsaotvesDecSig(kasl, esig) + + kalSigShnorr, err := esig.Decrypt(&kasl.Key) if err != nil { return nil, err } + kalSig = kalSigShnorr.Serialize() kalSig = append(kalSig, byte(txscript.SigHashAll)) - kafSig, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1) + kafSig, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STSchnorrSecp256k1) if err != nil { return nil, err } spendSig, err := txscript.NewScriptBuilder(). - AddData(kalSig). AddData(kafSig). + AddData(kalSig). AddData(lockTxScript). Script() + if err != nil { + return nil, err + } spendTx.TxIn[0].SignatureScript = spendSig @@ -672,7 +685,8 @@ func (c *client) redeemDcr(ctx context.Context, esig [libsecp256k1.CTLen]byte, l if err != nil { return nil, err } - fmt.Println(tx) + + fmt.Println("Redeem Tx -", tx) return kalSig, nil } @@ -680,10 +694,16 @@ func (c *client) redeemDcr(ctx context.Context, esig [libsecp256k1.CTLen]byte, l // redeemXmr redeems xmr by creating a new xmr wallet with the complete spend // and view private keys. func (c *client) redeemXmr(ctx context.Context, kalSig []byte) (*rpc.Client, error) { - kaslRecovered, err := libsecp256k1.EcdsaotvesRecEncKey(c.pkasl, c.lockTxEsig, kalSig[:len(kalSig)-1]) + kalSigParsed, err := schnorr.ParseSignature(kalSig[:len(kalSig)-1]) + if err != nil { + return nil, err + } + kaslRecoveredScalar, err := c.lockTxEsig.RecoverTweak(kalSigParsed) if err != nil { return nil, err } + kaslRecoveredBytes := kaslRecoveredScalar.Bytes() + kaslRecovered := secp256k1.PrivKeyFromBytes(kaslRecoveredBytes[:]) kbsfRecovered, _, err := edwards.PrivKeyFromScalar(kaslRecovered.Serialize()) if err != nil { @@ -728,57 +748,78 @@ func (c *client) redeemXmr(ctx context.Context, kalSig []byte) (*rpc.Client, err // startRefund starts the refund and can be done by either party. func (c *client) startRefund(ctx context.Context, kalSig, kafSig, lockTxScript []byte, refundTx *wire.MsgTx) error { refundSig, err := txscript.NewScriptBuilder(). - AddData(kalSig). AddData(kafSig). + AddData(kalSig). AddData(lockTxScript). Script() + if err != nil { + return err + } refundTx.TxIn[0].SignatureScript = refundSig - _, err = c.dcr.SendRawTransaction(ctx, refundTx, false) + tx, err := c.dcr.SendRawTransaction(ctx, refundTx, false) if err != nil { return err } + + fmt.Println("Cancel Tx -", tx) + return nil } // refundDcr returns dcr to bob while revealing his half of the xmr spend key. -func (c *client) refundDcr(ctx context.Context, spendRefundTx *wire.MsgTx, esig [libsecp256k1.CTLen]byte, lockRefundTxScript []byte) (kafSig []byte, err error) { +func (c *client) refundDcr(ctx context.Context, spendRefundTx *wire.MsgTx, esig *adaptorsigs.AdaptorSignature, lockRefundTxScript []byte) (kafSig []byte, err error) { kasf := secp256k1.PrivKeyFromBytes(c.kbsl.Serialize()) - kafSig, err = libsecp256k1.EcdsaotvesDecSig(kasf, esig) + decryptedSig, err := esig.Decrypt(&kasf.Key) if err != nil { return nil, err } + kafSig = decryptedSig.Serialize() kafSig = append(kafSig, byte(txscript.SigHashAll)) - kalSig, err := sign.RawTxInSignature(spendRefundTx, 0, lockRefundTxScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STEcdsaSecp256k1) + kalSig, err := sign.RawTxInSignature(spendRefundTx, 0, lockRefundTxScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STSchnorrSecp256k1) if err != nil { return nil, err } + refundSig, err := txscript.NewScriptBuilder(). - AddData(kalSig). AddData(kafSig). + AddData(kalSig). AddOp(txscript.OP_TRUE). AddData(lockRefundTxScript). Script() + if err != nil { + return nil, err + } spendRefundTx.TxIn[0].SignatureScript = refundSig - _, err = c.dcr.SendRawTransaction(ctx, spendRefundTx, false) + tx, err := c.dcr.SendRawTransaction(ctx, spendRefundTx, false) if err != nil { return nil, err } + + fmt.Println("Refund tx -", tx) + // TODO: Confirm refund happened. return kafSig, nil } // refundXmr refunds xmr but cannot happen without the dcr refund happening first. -func (c *client) refundXmr(ctx context.Context, kafSig []byte, esig [libsecp256k1.CTLen]byte) (*rpc.Client, error) { - kbslRecovered, err := libsecp256k1.EcdsaotvesRecEncKey(c.pkbsl, esig, kafSig[:len(kafSig)-1]) +func (c *client) refundXmr(ctx context.Context, kafSig []byte, esig *adaptorsigs.AdaptorSignature) (*rpc.Client, error) { + kafSigParsed, err := schnorr.ParseSignature(kafSig[:len(kafSig)-1]) + if err != nil { + return nil, err + } + kbslRecoveredScalar, err := esig.RecoverTweak(kafSigParsed) if err != nil { return nil, err } + kbslRecoveredBytes := kbslRecoveredScalar.Bytes() + kbslRecovered := secp256k1.PrivKeyFromBytes(kbslRecoveredBytes[:]) + kaslRecovered, _, err := edwards.PrivKeyFromScalar(kbslRecovered.Serialize()) if err != nil { return nil, fmt.Errorf("unable to recover kasl: %v", err) @@ -834,7 +875,7 @@ func (c *client) takeDcr(ctx context.Context, lockRefundTxScript []byte, spendRe } spendRefundTx.TxOut[0] = txOut - kafSig, err := sign.RawTxInSignature(spendRefundTx, 0, lockRefundTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1) + kafSig, err := sign.RawTxInSignature(spendRefundTx, 0, lockRefundTxScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STSchnorrSecp256k1) if err != nil { return err } @@ -843,6 +884,9 @@ func (c *client) takeDcr(ctx context.Context, lockRefundTxScript []byte, spendRe AddOp(txscript.OP_FALSE). AddData(lockRefundTxScript). Script() + if err != nil { + return err + } spendRefundTx.TxIn[0].SignatureScript = refundSig @@ -890,7 +934,7 @@ func success(ctx context.Context) error { // Bob generates transactions but does not send anything yet. - _, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + _, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, bDCRPK, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) if err != nil { return fmt.Errorf("unalbe to generate lock transactions: %v", err) } @@ -917,20 +961,19 @@ func success(ctx context.Context) error { time.Sleep(time.Second * 5) // Bob sends esig after confirming on chain xmr tx. - bobEsig, err := bob.sendLockTxSig(lockTxScript, spendTx) if err != nil { return err } // Alice redeems using the esig. - kalSig, err := alice.redeemDcr(ctx, bobEsig, lockTxScript, spendTx) + kalSig, err := alice.redeemDcr(ctx, bobEsig, lockTxScript, spendTx, bDCRPK) if err != nil { return err } // Prove that bob can't just sign the spend tx for the signature we need. - ks, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, bob.kal.Serialize(), dcrec.STEcdsaSecp256k1) + ks, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, bob.kal.Serialize(), dcrec.STSchnorrSecp256k1) if err != nil { return err } @@ -997,7 +1040,7 @@ func aliceBailsBeforeXmrInit(ctx context.Context) error { // Bob generates transactions but does not send anything yet. - bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, _, _, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, _, _, bobDleag, _, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) if err != nil { return fmt.Errorf("unalbe to generate lock transactions: %v", err) } @@ -1010,7 +1053,6 @@ func aliceBailsBeforeXmrInit(ctx context.Context) error { } // Bob initializes the swap with dcr being sent. - _, err = bob.initDcr(ctx) if err != nil { return err @@ -1069,28 +1111,24 @@ func refund(ctx context.Context) error { } // Alice generates dleag. - pkbsf, kbvf, pkaf, aliceDleag, err := alice.generateDleag(ctx) if err != nil { return err } // Bob generates transactions but does not send anything yet. - - bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, _, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) if err != nil { return fmt.Errorf("unalbe to generate lock transactions: %v", err) } // Alice signs a refund script for Bob. - spendRefundESig, aliceRefundSig, err := alice.generateRefundSigs(refundTx, spendRefundTx, vIn, lockTxScript, lockRefundTxScript, bobDleag) if err != nil { return err } // Bob initializes the swap with dcr being sent. - _, err = bob.initDcr(ctx) if err != nil { return err @@ -1134,6 +1172,7 @@ func refund(ctx context.Context) error { if bal.Balance != xmrAmt { return fmt.Errorf("expected refund xmr balance of %d but got %d", xmrAmt, bal.Balance) } + fmt.Printf("new xmr wallet balance\n%+v\n", *bal) return nil @@ -1167,7 +1206,7 @@ func bobBailsAfterXmrInit(ctx context.Context) error { // Bob generates transactions but does not send anything yet. - bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) + bobRefundSig, lockRefundTxScript, lockTxScript, refundTx, spendRefundTx, vIn, pkbs, vkbv, bobDleag, _, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, aliceDleag) if err != nil { return fmt.Errorf("unalbe to generate lock transactions: %v", err) } diff --git a/internal/libsecp256k1/README.md b/internal/libsecp256k1/README.md deleted file mode 100644 index 5112e5c0f6..0000000000 --- a/internal/libsecp256k1/README.md +++ /dev/null @@ -1,10 +0,0 @@ -### Package libsecp256k1 - -Package libsecp256k1 includes some primative cryptographic functions needed for -working with adaptor signatures that are not currently found in golang. This imports -code from https://github.com/tecnovert/secp256k1 and uses that with cgo. Both -that library and this package are in an experimental stage. - -### Usage - -Run the `build.sh` script. Currently untested on mac and will not work on Windows. diff --git a/internal/libsecp256k1/build.sh b/internal/libsecp256k1/build.sh deleted file mode 100755 index b19343f0c7..0000000000 --- a/internal/libsecp256k1/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -rm -fr secp256k1 -git clone https://github.com/tecnovert/secp256k1 -b anonswap_v0.2 - -cd secp256k1 -./autogen.sh -./configure --enable-module-dleag --enable-experimental --enable-module-generator --enable-module-ed25519 --enable-module-recovery --enable-module-ecdsaotves -make -cd .. diff --git a/internal/libsecp256k1/libsecp256k1.go b/internal/libsecp256k1/libsecp256k1.go deleted file mode 100644 index 637435cc79..0000000000 --- a/internal/libsecp256k1/libsecp256k1.go +++ /dev/null @@ -1,149 +0,0 @@ -// This code is available on the terms of the project LICENSE.md file, -// also available online at https://blueoakcouncil.org/license/1.0.0. - -package libsecp256k1 - -/* -#cgo CFLAGS: -g -Wall -#cgo LDFLAGS: -L. -l:secp256k1/.libs/libsecp256k1.a -#include "secp256k1/include/secp256k1_dleag.h" -#include "secp256k1/include/secp256k1_ecdsaotves.h" -#include - -secp256k1_context* _ctx() { - return secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); -} -*/ -import "C" -import ( - "errors" - "unsafe" - - "decred.org/dcrdex/dex/encode" - "github.com/decred/dcrd/dcrec/edwards/v2" - "github.com/decred/dcrd/dcrec/secp256k1/v4" -) - -const ( - ProofLen = 48893 - CTLen = 196 - maxSigLen = 72 // The actual size is variable. -) - -// Ed25519DleagProve creates a proof for checking a discrete logarithm is equal -// across the secp256k1 and ed25519 curves. -func Ed25519DleagProve(privKey *edwards.PrivateKey) (proof [ProofLen]byte, err error) { - secpCtx := C._ctx() - defer C.free(unsafe.Pointer(secpCtx)) - nonce := [32]byte{} - copy(nonce[:], encode.RandomBytes(32)) - key := [32]byte{} - copy(key[:], privKey.Serialize()) - n := (*C.uchar)(unsafe.Pointer(&nonce)) - k := (*C.uchar)(unsafe.Pointer(&key)) - nBits := uint64(252) - nb := (*C.ulong)(unsafe.Pointer(&nBits)) - plen := C.ulong(ProofLen) - p := (*C.uchar)(unsafe.Pointer(&proof)) - res := C.secp256k1_ed25519_dleag_prove(secpCtx, p, &plen, k, *nb, n) - if int(res) != 1 { - return [ProofLen]byte{}, errors.New("C.secp256k1_ed25519_dleag_prove exited with error") - } - return proof, nil -} - -// Ed25519DleagVerify verifies that a descrete logarithm is equal across the -// secp256k1 and ed25519 curves. -func Ed25519DleagVerify(proof [ProofLen]byte) bool { - secpCtx := C._ctx() - defer C.free(unsafe.Pointer(secpCtx)) - pl := C.ulong(ProofLen) - p := (*C.uchar)(unsafe.Pointer(&proof)) - res := C.secp256k1_ed25519_dleag_verify(secpCtx, p, pl) - return res == 1 -} - -// EcdsaotvesEncSign signs the hash and returns an encrypted signature. -func EcdsaotvesEncSign(signPriv *secp256k1.PrivateKey, encPub *secp256k1.PublicKey, hash [32]byte) (cyphertext [CTLen]byte, err error) { - secpCtx := C._ctx() - defer C.free(unsafe.Pointer(secpCtx)) - privBytes := [32]byte{} - copy(privBytes[:], signPriv.Serialize()) - priv := (*C.uchar)(unsafe.Pointer(&privBytes)) - pubBytes := [33]byte{} - copy(pubBytes[:], encPub.SerializeCompressed()) - pub := (*C.uchar)(unsafe.Pointer(&pubBytes)) - h := (*C.uchar)(unsafe.Pointer(&hash)) - s := (*C.uchar)(unsafe.Pointer(&cyphertext)) - res := C.ecdsaotves_enc_sign(secpCtx, s, priv, pub, h) - if int(res) != 1 { - return [CTLen]byte{}, errors.New("C.ecdsaotves_enc_sign exited with error") - } - return cyphertext, nil -} - -// EcdsaotvesEncVerify verifies the encrypted signature. -func EcdsaotvesEncVerify(signPub, encPub *secp256k1.PublicKey, hash [32]byte, cyphertext [CTLen]byte) bool { - secpCtx := C._ctx() - defer C.free(unsafe.Pointer(secpCtx)) - signBytes := [33]byte{} - copy(signBytes[:], signPub.SerializeCompressed()) - sp := (*C.uchar)(unsafe.Pointer(&signBytes)) - encBytes := [33]byte{} - copy(encBytes[:], encPub.SerializeCompressed()) - ep := (*C.uchar)(unsafe.Pointer(&encBytes)) - h := (*C.uchar)(unsafe.Pointer(&hash)) - c := (*C.uchar)(unsafe.Pointer(&cyphertext)) - res := C.ecdsaotves_enc_verify(secpCtx, sp, ep, h, c) - return res == 1 -} - -// EcdsaotvesDecSig retrieves the signature. -func EcdsaotvesDecSig(encPriv *secp256k1.PrivateKey, cyphertext [CTLen]byte) ([]byte, error) { - secpCtx := C._ctx() - defer C.free(unsafe.Pointer(secpCtx)) - encBytes := [32]byte{} - copy(encBytes[:], encPriv.Serialize()) - ep := (*C.uchar)(unsafe.Pointer(&encBytes)) - ct := (*C.uchar)(unsafe.Pointer(&cyphertext)) - var sig [maxSigLen]byte - s := (*C.uchar)(unsafe.Pointer(&sig)) - slen := C.ulong(maxSigLen) - res := C.ecdsaotves_dec_sig(secpCtx, s, &slen, ep, ct) - if int(res) != 1 { - return nil, errors.New("C.ecdsaotves_dec_sig exited with error") - } - sigCopy := make([]byte, maxSigLen) - copy(sigCopy, sig[:]) - // Remove trailing zeros. - for i := maxSigLen - 1; i >= 0; i-- { - if sigCopy[i] != 0 { - break - } - sigCopy = sigCopy[:i] - } - return sigCopy, nil -} - -// EcdsaotvesRecEncKey retrieves the encoded private key from signature and -// cyphertext. -func EcdsaotvesRecEncKey(encPub *secp256k1.PublicKey, cyphertext [CTLen]byte, sig []byte) (encPriv *secp256k1.PrivateKey, err error) { - secpCtx := C._ctx() - defer C.free(unsafe.Pointer(secpCtx)) - pubBytes := [33]byte{} - copy(pubBytes[:], encPub.SerializeCompressed()) - ep := (*C.uchar)(unsafe.Pointer(&pubBytes)) - ct := (*C.uchar)(unsafe.Pointer(&cyphertext)) - sigCopy := [maxSigLen]byte{} - copy(sigCopy[:], sig) - s := (*C.uchar)(unsafe.Pointer(&sigCopy)) - varSigLen := len(sig) - slen := C.ulong(varSigLen) - pkBytes := [32]byte{} - pk := (*C.uchar)(unsafe.Pointer(&pkBytes)) - res := C.ecdsaotves_rec_enc_key(secpCtx, pk, ep, ct, s, slen) - if int(res) != 1 { - return nil, errors.New("C.ecdsaotves_rec_enc_key exited with error") - } - return secp256k1.PrivKeyFromBytes(pkBytes[:]), nil -} diff --git a/internal/libsecp256k1/libsecp256k1_test.go b/internal/libsecp256k1/libsecp256k1_test.go deleted file mode 100644 index fd16d6e97f..0000000000 --- a/internal/libsecp256k1/libsecp256k1_test.go +++ /dev/null @@ -1,215 +0,0 @@ -//go:build libsecp256k1 - -package libsecp256k1 - -import ( - "bytes" - "math/rand" - "testing" - - "github.com/decred/dcrd/dcrec/edwards/v2" - "github.com/decred/dcrd/dcrec/secp256k1/v4" -) - -func randBytes(n int) []byte { - b := make([]byte, n) - rand.Read(b) - return b -} - -func TestEd25519DleagProve(t *testing.T) { - tests := []struct { - name string - }{{ - name: "ok", - }} - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - pk, err := edwards.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - sPk := secp256k1.PrivKeyFromBytes(pk.Serialize()) - proof, err := Ed25519DleagProve(pk) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(sPk.PubKey().SerializeCompressed(), proof[:33]) { - t.Fatal("first 33 bytes of proof not equal to secp256k1 pubkey") - } - }) - } -} - -func TestEd25519DleagVerify(t *testing.T) { - pk, err := edwards.GeneratePrivateKey() - if err != nil { - panic(err) - } - proof, err := Ed25519DleagProve(pk) - if err != nil { - panic(err) - } - tests := []struct { - name string - proof [ProofLen]byte - ok bool - }{{ - name: "ok", - proof: proof, - ok: true, - }, { - name: "bad proof", - proof: func() (p [ProofLen]byte) { - copy(p[:], proof[:]) - p[0] ^= p[0] - return p - }(), - }} - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ok := Ed25519DleagVerify(test.proof) - if ok != test.ok { - t.Fatalf("want %v but got %v", test.ok, ok) - } - }) - } -} - -func TestEcdsaotvesEncSign(t *testing.T) { - tests := []struct { - name string - }{{ - name: "ok", - }} - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - signPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - encPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - h := randBytes(32) - var hash [32]byte - copy(hash[:], h) - _, err = EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) - if err != nil { - t.Fatal(err) - } - }) - } -} - -func TestEcdsaotvesEncVerify(t *testing.T) { - signPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - encPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - h := randBytes(32) - var hash [32]byte - copy(hash[:], h) - ct, err := EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) - if err != nil { - t.Fatal(err) - } - tests := []struct { - name string - ok bool - ct [196]byte - }{{ - name: "ok", - ct: ct, - ok: true, - }, { - name: "bad sig", - ct: func() (c [CTLen]byte) { - copy(c[:], ct[:]) - c[0] ^= c[0] - return c - }(), - }} - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ok := EcdsaotvesEncVerify(signPk.PubKey(), encPk.PubKey(), hash, test.ct) - if ok != test.ok { - t.Fatalf("want %v but got %v", test.ok, ok) - } - }) - } -} - -func TestEcdsaotvesDecSig(t *testing.T) { - signPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - encPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - h := randBytes(32) - var hash [32]byte - copy(hash[:], h) - ct, err := EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) - if err != nil { - t.Fatal(err) - } - tests := []struct { - name string - }{{ - name: "ok", - }} - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, err := EcdsaotvesDecSig(encPk, ct) - if err != nil { - t.Fatal(err) - } - }) - } -} - -func TestEcdsaotvesRecEncKey(t *testing.T) { - signPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - encPk, err := secp256k1.GeneratePrivateKey() - if err != nil { - t.Fatal(err) - } - h := randBytes(32) - var hash [32]byte - copy(hash[:], h) - ct, err := EcdsaotvesEncSign(signPk, encPk.PubKey(), hash) - if err != nil { - t.Fatal(err) - } - sig, err := EcdsaotvesDecSig(encPk, ct) - if err != nil { - t.Fatal(err) - } - tests := []struct { - name string - }{{ - name: "ok", - }} - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - pk, err := EcdsaotvesRecEncKey(encPk.PubKey(), ct, sig) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(pk.Serialize(), encPk.Serialize()) { - t.Fatal("private keys not equal") - } - }) - } -} diff --git a/internal/libsecp256k1/secp256k1 b/internal/libsecp256k1/secp256k1 deleted file mode 160000 index e3ebcd782a..0000000000 --- a/internal/libsecp256k1/secp256k1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e3ebcd782a604f228784b10c50ffa099d9796720 diff --git a/run_tests.sh b/run_tests.sh index 1ec7227cab..ffd79def1c 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -9,10 +9,6 @@ echo "Go version: $GV" # Ensure html templates pass localization. go generate -x ./client/webserver/site # no -write -cd ./internal/libsecp256k1 -./build.sh -go test -race -tags libsecp256k1 - cd "$dir" # list of all modules to test From 04d1f1a26f6e61d9dc6f8657ea92969bcff881b8 Mon Sep 17 00:00:00 2001 From: martonp Date: Tue, 3 Sep 2024 09:34:49 +0200 Subject: [PATCH 8/9] Reivew fixes --- internal/adaptorsigs/adaptor.go | 90 +++++++++++++++++----------- internal/adaptorsigs/adaptor_test.go | 84 +++++++++++++++++++------- 2 files changed, 117 insertions(+), 57 deletions(-) diff --git a/internal/adaptorsigs/adaptor.go b/internal/adaptorsigs/adaptor.go index b268a231ce..c0fa30905e 100644 --- a/internal/adaptorsigs/adaptor.go +++ b/internal/adaptorsigs/adaptor.go @@ -10,7 +10,7 @@ import ( ) // AdaptorSignatureSize is the size of an encoded adaptor Schnorr signature. -const AdaptorSignatureSize = 129 +const AdaptorSignatureSize = 97 // scalarSize is the size of an encoded big endian scalar. const scalarSize = 32 @@ -30,6 +30,19 @@ var ( } ) +type affinePoint struct { + x secp256k1.FieldVal + y secp256k1.FieldVal +} + +func (p *affinePoint) asJacobian() *secp256k1.JacobianPoint { + var result secp256k1.JacobianPoint + result.X.Set(&p.x) + result.Y.Set(&p.y) + result.Z.SetInt(1) + return &result +} + // AdaptorSignature is a signature with auxillary data that commits to a hidden // value. When an adaptor signature is combined with a corresponding signature, // the hidden value is revealed. Alternatively, when combined with a hidden @@ -54,35 +67,32 @@ var ( // party B can recover the tweak and use it to decrypt the private key // tweaked adaptor signature that party A originally sent them. type AdaptorSignature struct { - r secp256k1.FieldVal - s secp256k1.ModNScalar - // t will always be in affine coordinates. - t secp256k1.JacobianPoint + r secp256k1.FieldVal + s secp256k1.ModNScalar + t affinePoint pubKeyTweak bool } // Serialize returns a serialized adaptor signature in the following format: // -// sig[0:32] x coordinate of the point R, encoded as a big-endian uint256 -// sig[32:64] s, encoded also as big-endian uint256 -// sig[64:96] x coordinate of the point T, encoded as a big-endian uint256 -// sig[96:128] y coordinate of the point T, encoded as a big-endian uint256 -// sig[128] 1 if the adaptor was created with a public key tweak, 0 if it was -// created with a private key tweak. +// sig[0:32] x coordinate of the point R, encoded as a big-endian uint256 +// sig[32:64] s, encoded also as big-endian uint256 +// sig[64:96] x coordinate of the point T, encoded as a big-endian uint256 +// sig[96] first bit is 1 if the signature is public key tweaked, second bit +// is 1 if the y coordinate of T is odd. func (sig *AdaptorSignature) Serialize() []byte { var b [AdaptorSignatureSize]byte sig.r.PutBytesUnchecked(b[0:32]) sig.s.PutBytesUnchecked(b[32:64]) - sig.t.X.PutBytesUnchecked(b[64:96]) - sig.t.Y.PutBytesUnchecked(b[96:128]) + sig.t.x.PutBytesUnchecked(b[64:96]) if sig.pubKeyTweak { - b[128] = 1 - } else { - b[128] = 0 + b[96] = 1 } + b[96] |= byte(sig.t.y.IsOddBit()) << 1 return b[:] } +// ParseAdaptorSignature parses an adaptor signature from a serialized format. func ParseAdaptorSignature(b []byte) (*AdaptorSignature, error) { if len(b) != AdaptorSignatureSize { str := fmt.Sprintf("malformed signature: wrong size: %d", len(b)) @@ -101,20 +111,20 @@ func ParseAdaptorSignature(b []byte) (*AdaptorSignature, error) { return nil, errors.New(str) } - var t secp256k1.JacobianPoint - if overflow := t.X.SetBytes((*[32]byte)(b[64:96])); overflow > 0 { + var t affinePoint + if overflow := t.x.SetBytes((*[32]byte)(b[64:96])); overflow > 0 { str := "invalid signature: t.x >= field prime" return nil, errors.New(str) } - if overflow := t.Y.SetBytes((*[32]byte)(b[96:128])); overflow > 0 { - str := "invalid signature: t.y >= field prime" + isOdd := (b[96]>>1)&1 == 1 + if valid := secp256k1.DecompressY(&t.x, isOdd, &t.y); !valid { + str := "invalid signature: not for a valid curve point" return nil, errors.New(str) } + t.y.Normalize() - t.Z.SetInt(1) - - pubKeyTweak := b[128] == byte(1) + pubKeyTweak := b[96]&1 == 1 return &AdaptorSignature{ r: r, @@ -124,6 +134,15 @@ func ParseAdaptorSignature(b []byte) (*AdaptorSignature, error) { }, nil } +// IsEqual returns true if the adaptor signature is equal to another. +func (sig *AdaptorSignature) IsEqual(otherSig *AdaptorSignature) bool { + return sig.r.Equals(&otherSig.r) && + sig.s.Equals(&otherSig.s) && + sig.t.x.Equals(&otherSig.t.x) && + sig.t.y.Equals(&otherSig.t.y) && + sig.pubKeyTweak == otherSig.pubKeyTweak +} + // schnorrAdaptorVerify verifies that the adaptor signature will result in a // valid signature when decrypted with the tweak. func schnorrAdaptorVerify(sig *AdaptorSignature, hash []byte, pubKey *secp256k1.PublicKey) error { @@ -195,9 +214,9 @@ func schnorrAdaptorVerify(sig *AdaptorSignature, hash []byte, pubKey *secp256k1. secp256k1.ScalarBaseMultNonConst(&sig.s, &sG) secp256k1.ScalarMultNonConst(&e, &Q, &eQ) secp256k1.AddNonConst(&sG, &eQ, &R) - tInv := sig.t + tInv := sig.t.asJacobian() tInv.Y.Negate(1) - secp256k1.AddNonConst(&R, &tInv, &encryptedR) + secp256k1.AddNonConst(&R, tInv, &encryptedR) // Step 8. // @@ -231,7 +250,8 @@ func schnorrAdaptorVerify(sig *AdaptorSignature, hash []byte, pubKey *secp256k1. return nil } -// Verify checks that the adaptor signature, when decrypted using the ke +// Verify checks that the adaptor signature, when decrypted using the tweak, +// will result in a valid schnorr signature for the given hash and public key. func (sig *AdaptorSignature) Verify(hash []byte, pubKey *secp256k1.PublicKey) error { if sig.pubKeyTweak { return fmt.Errorf("only priv key tweaked adaptors can be verified") @@ -247,14 +267,14 @@ func (sig *AdaptorSignature) Decrypt(tweak *secp256k1.ModNScalar) (*schnorr.Sign var expectedT secp256k1.JacobianPoint secp256k1.ScalarBaseMultNonConst(tweak, &expectedT) expectedT.ToAffine() - if !expectedT.X.Equals(&sig.t.X) { + if !expectedT.X.Equals(&sig.t.x) { return nil, fmt.Errorf("tweak X does not match expected") } - if !expectedT.Y.Equals(&sig.t.Y) { + if !expectedT.Y.Equals(&sig.t.y) { return nil, fmt.Errorf("tweak Y does not match expected") } - s := new(secp256k1.ModNScalar).Add(tweak) + s := new(secp256k1.ModNScalar).Set(tweak) if !sig.pubKeyTweak { s.Negate() } @@ -278,10 +298,10 @@ func (sig *AdaptorSignature) RecoverTweak(validSig *schnorr.Signature) (*secp256 var expectedT secp256k1.JacobianPoint secp256k1.ScalarBaseMultNonConst(t, &expectedT) expectedT.ToAffine() - if !expectedT.X.Equals(&sig.t.X) { + if !expectedT.X.Equals(&sig.t.x) { return nil, fmt.Errorf("recovered tweak does not match expected") } - if !expectedT.Y.Equals(&sig.t.Y) { + if !expectedT.Y.Equals(&sig.t.y) { return nil, fmt.Errorf("recovered tweak does not match expected") } @@ -290,8 +310,7 @@ func (sig *AdaptorSignature) RecoverTweak(validSig *schnorr.Signature) (*secp256 // PublicTweak returns the hidden value multiplied by the generator point. func (sig *AdaptorSignature) PublicTweak() *secp256k1.JacobianPoint { - T := sig.t - return &T + return sig.t.asJacobian() } // schnorrEncryptedSign creates an adaptor signature by modifying the nonce in @@ -375,10 +394,11 @@ func schnorrEncryptedSign(privKey, nonce *secp256k1.ModNScalar, hash []byte, T * affineT := new(secp256k1.JacobianPoint) affineT.Set(T) affineT.ToAffine() + t := affinePoint{x: affineT.X, y: affineT.Y} return &AdaptorSignature{ r: *r, s: *s, - t: *affineT, + t: t, pubKeyTweak: true}, nil } @@ -484,6 +504,6 @@ func PrivateKeyTweakedAdaptorSig(sig *schnorr.Signature, pubKey *secp256k1.Publi return &AdaptorSignature{ r: *r, s: *tweakedS, - t: *T, + t: affinePoint{x: T.X, y: T.Y}, } } diff --git a/internal/adaptorsigs/adaptor_test.go b/internal/adaptorsigs/adaptor_test.go index 23354f1b23..f33a8d42a7 100644 --- a/internal/adaptorsigs/adaptor_test.go +++ b/internal/adaptorsigs/adaptor_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "decred.org/dcrdex/dex/encode" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" ) @@ -19,19 +20,15 @@ func TestAdaptorSignatureRandom(t *testing.T) { }(t, seed) for i := 0; i < 100; i++ { - // Generate two private keys - var pkBuf1, pkBuf2 [32]byte - if _, err := rng.Read(pkBuf1[:]); err != nil { + // Generate two private keys. + privKey1, err := secp256k1.GeneratePrivateKeyFromRand(rng) + if err != nil { t.Fatalf("failed to read random private key: %v", err) } - if _, err := rng.Read(pkBuf2[:]); err != nil { + privKey2, err := secp256k1.GeneratePrivateKeyFromRand(rng) + if err != nil { t.Fatalf("failed to read random private key: %v", err) } - var privKey1Scalar, privKey2Scalar secp256k1.ModNScalar - privKey1Scalar.SetBytes(&pkBuf1) - privKey2Scalar.SetBytes(&pkBuf2) - privKey1 := secp256k1.NewPrivateKey(&privKey1Scalar) - privKey2 := secp256k1.NewPrivateKey(&privKey2Scalar) // Generate random hashes to sign. var hash1, hash2 [32]byte @@ -77,6 +74,9 @@ func TestAdaptorSignatureRandom(t *testing.T) { if err != nil { t.Fatal(err) } + if !decryptedSig.Verify(hash2[:], privKey2.PubKey()) { + t.Fatal("failed to verify decrypted signature") + } // Using the decrypted version of their sig, which has been made public, // the owner of privKey2 can recover the tweak. @@ -109,23 +109,63 @@ func RandomBytes(len int) []byte { return bytes } -func TestAdaptorSigParsing(t *testing.T) { - adaptor := &AdaptorSignature{} - adaptor.r.SetByteSlice(RandomBytes(32)) - adaptor.s.SetByteSlice(RandomBytes(32)) - adaptor.pubKeyTweak = true +func TestPublicKeyTweakParsing(t *testing.T) { + for i := 0; i < 100; i++ { + privKey, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + hash := encode.RandomBytes(32) + tweak, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + var T secp256k1.JacobianPoint + tweak.PubKey().AsJacobian(&T) - var tweak secp256k1.JacobianPoint - secp256k1.ScalarBaseMultNonConst(&adaptor.s, &tweak) + adaptorSig, err := PublicKeyTweakedAdaptorSig(privKey, hash, &T) + if err != nil { + t.Fatal(err) + } - serialized := adaptor.Serialize() + serialized := adaptorSig.Serialize() + parsed, err := ParseAdaptorSignature(serialized) + if err != nil { + t.Fatal(err) + } - adaptor2, err := ParseAdaptorSignature(serialized) - if err != nil { - t.Fatal(err) + if !adaptorSig.IsEqual(parsed) { + t.Fatalf("parsed sig does not equal original") + } } +} + +func TestPrivateKeyTweakParsing(t *testing.T) { + for i := 0; i < 100; i++ { + privKey, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + hash := encode.RandomBytes(32) + tweak, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } - if !adaptor2.r.Equals(&adaptor.r) { - t.Fatal("r mismatch") + sig, err := schnorr.Sign(privKey, hash) + if err != nil { + t.Fatal(err) + } + + adaptorSig := PrivateKeyTweakedAdaptorSig(sig, privKey.PubKey(), &tweak.Key) + serialized := adaptorSig.Serialize() + parsed, err := ParseAdaptorSignature(serialized) + if err != nil { + t.Fatal(err) + } + + if !adaptorSig.IsEqual(parsed) { + t.Fatalf("parsed sig does not equal original") + } } } From 4fdfbb84d77f99f0fcc2ffa893f0d12d44c66069 Mon Sep 17 00:00:00 2001 From: martonp Date: Fri, 20 Sep 2024 10:55:39 +0200 Subject: [PATCH 9/9] Use updated secp256k1 library --- client/cmd/bisonw-desktop/go.mod | 2 +- client/cmd/bisonw-desktop/go.sum | 4 ++-- dex/testing/loadbot/go.mod | 2 +- dex/testing/loadbot/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- internal/adaptorsigs/adaptor.go | 38 ++++++++------------------------ 7 files changed, 18 insertions(+), 38 deletions(-) diff --git a/client/cmd/bisonw-desktop/go.mod b/client/cmd/bisonw-desktop/go.mod index 70780d387b..2f2ea0e8d3 100644 --- a/client/cmd/bisonw-desktop/go.mod +++ b/client/cmd/bisonw-desktop/go.mod @@ -92,7 +92,7 @@ require ( github.com/decred/dcrd/database/v3 v3.0.2 // indirect github.com/decred/dcrd/dcrec v1.0.1 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b // indirect github.com/decred/dcrd/dcrjson/v4 v4.1.0 // indirect github.com/decred/dcrd/dcrutil/v4 v4.0.2 // indirect github.com/decred/dcrd/gcs/v4 v4.1.0 // indirect diff --git a/client/cmd/bisonw-desktop/go.sum b/client/cmd/bisonw-desktop/go.sum index 0fd6112ca3..b978b5ab83 100644 --- a/client/cmd/bisonw-desktop/go.sum +++ b/client/cmd/bisonw-desktop/go.sum @@ -293,8 +293,8 @@ github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b h1:HIjTBv19sQRjUiifEKPy6BLFKkOtV44uCnDThmYQ37s= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrjson/v4 v4.1.0 h1:WJVogRnYnNxB5hWoGHODvP4fNTG1JycTuHHKt/XucHk= github.com/decred/dcrd/dcrjson/v4 v4.1.0/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc= github.com/decred/dcrd/dcrutil/v4 v4.0.2 h1:eIl3E6gGln54qE8nk5o5lLtjh2/9C2Rz63OpD662h+8= diff --git a/dex/testing/loadbot/go.mod b/dex/testing/loadbot/go.mod index 9a42a71111..8b656096ff 100644 --- a/dex/testing/loadbot/go.mod +++ b/dex/testing/loadbot/go.mod @@ -74,7 +74,7 @@ require ( github.com/decred/dcrd/database/v3 v3.0.2 // indirect github.com/decred/dcrd/dcrec v1.0.1 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b // indirect github.com/decred/dcrd/dcrjson/v4 v4.1.0 // indirect github.com/decred/dcrd/dcrutil/v4 v4.0.2 // indirect github.com/decred/dcrd/gcs/v4 v4.1.0 // indirect diff --git a/dex/testing/loadbot/go.sum b/dex/testing/loadbot/go.sum index bc1b87e569..98db339237 100644 --- a/dex/testing/loadbot/go.sum +++ b/dex/testing/loadbot/go.sum @@ -289,8 +289,8 @@ github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b h1:HIjTBv19sQRjUiifEKPy6BLFKkOtV44uCnDThmYQ37s= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrjson/v4 v4.1.0 h1:WJVogRnYnNxB5hWoGHODvP4fNTG1JycTuHHKt/XucHk= github.com/decred/dcrd/dcrjson/v4 v4.1.0/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc= github.com/decred/dcrd/dcrutil/v4 v4.0.2 h1:eIl3E6gGln54qE8nk5o5lLtjh2/9C2Rz63OpD662h+8= diff --git a/go.mod b/go.mod index 773061eef1..04f19121b0 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/decred/dcrd/crypto/blake256 v1.0.1 github.com/decred/dcrd/dcrec v1.0.1 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b github.com/decred/dcrd/dcrjson/v4 v4.1.0 github.com/decred/dcrd/dcrutil/v4 v4.0.2 github.com/decred/dcrd/gcs/v4 v4.1.0 diff --git a/go.sum b/go.sum index f54e0947b4..1caa75ab77 100644 --- a/go.sum +++ b/go.sum @@ -293,8 +293,8 @@ github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b h1:HIjTBv19sQRjUiifEKPy6BLFKkOtV44uCnDThmYQ37s= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.1-0.20240912225101-7333f805850b/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrjson/v4 v4.1.0 h1:WJVogRnYnNxB5hWoGHODvP4fNTG1JycTuHHKt/XucHk= github.com/decred/dcrd/dcrjson/v4 v4.1.0/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc= github.com/decred/dcrd/dcrutil/v4 v4.0.2 h1:eIl3E6gGln54qE8nk5o5lLtjh2/9C2Rz63OpD662h+8= diff --git a/internal/adaptorsigs/adaptor.go b/internal/adaptorsigs/adaptor.go index c0fa30905e..5cb0a2e1f1 100644 --- a/internal/adaptorsigs/adaptor.go +++ b/internal/adaptorsigs/adaptor.go @@ -122,7 +122,6 @@ func ParseAdaptorSignature(b []byte) (*AdaptorSignature, error) { str := "invalid signature: not for a valid curve point" return nil, errors.New(str) } - t.y.Normalize() pubKeyTweak := b[96]&1 == 1 @@ -266,12 +265,8 @@ func (sig *AdaptorSignature) Verify(hash []byte, pubKey *secp256k1.PublicKey) er func (sig *AdaptorSignature) Decrypt(tweak *secp256k1.ModNScalar) (*schnorr.Signature, error) { var expectedT secp256k1.JacobianPoint secp256k1.ScalarBaseMultNonConst(tweak, &expectedT) - expectedT.ToAffine() - if !expectedT.X.Equals(&sig.t.x) { - return nil, fmt.Errorf("tweak X does not match expected") - } - if !expectedT.Y.Equals(&sig.t.y) { - return nil, fmt.Errorf("tweak Y does not match expected") + if !expectedT.EquivalentNonConst(sig.t.asJacobian()) { + return nil, fmt.Errorf("tweak does not match expected value") } s := new(secp256k1.ModNScalar).Set(tweak) @@ -290,19 +285,14 @@ func (sig *AdaptorSignature) RecoverTweak(validSig *schnorr.Signature) (*secp256 return nil, fmt.Errorf("only pub key tweaked sigs can be recovered") } - _, s := parseSig(validSig) - - t := new(secp256k1.ModNScalar).NegateVal(&sig.s).Add(s) + s := validSig.S() + t := new(secp256k1.ModNScalar).NegateVal(&sig.s).Add(&s) // Verify the recovered tweak var expectedT secp256k1.JacobianPoint secp256k1.ScalarBaseMultNonConst(t, &expectedT) - expectedT.ToAffine() - if !expectedT.X.Equals(&sig.t.x) { - return nil, fmt.Errorf("recovered tweak does not match expected") - } - if !expectedT.Y.Equals(&sig.t.y) { - return nil, fmt.Errorf("recovered tweak does not match expected") + if !expectedT.EquivalentNonConst(sig.t.asJacobian()) { + return nil, fmt.Errorf("tweak does not match expected value") } return t, nil @@ -483,14 +473,6 @@ func PublicKeyTweakedAdaptorSig(privKey *secp256k1.PrivateKey, hash []byte, T *s } } -func parseSig(sig *schnorr.Signature) (r *secp256k1.FieldVal, s *secp256k1.ModNScalar) { - sigB := sig.Serialize() - r, s = new(secp256k1.FieldVal), new(secp256k1.ModNScalar) - r.SetBytes((*[32]byte)(sigB[0:32])) - s.SetBytes((*[32]byte)(sigB[32:64])) - return r, s -} - // PrivateKeyTweakedAdaptorSig creates a private key tweaked adaptor signature. // This is created by a party which knows the hidden value. func PrivateKeyTweakedAdaptorSig(sig *schnorr.Signature, pubKey *secp256k1.PublicKey, t *secp256k1.ModNScalar) *AdaptorSignature { @@ -498,12 +480,10 @@ func PrivateKeyTweakedAdaptorSig(sig *schnorr.Signature, pubKey *secp256k1.Publi secp256k1.ScalarBaseMultNonConst(t, T) T.ToAffine() - r, s := parseSig(sig) - tweakedS := new(secp256k1.ModNScalar).Add2(s, t) - + s := sig.S() return &AdaptorSignature{ - r: *r, - s: *tweakedS, + r: sig.R(), + s: *new(secp256k1.ModNScalar).Add2(&s, t), t: affinePoint{x: T.X, y: T.Y}, } }