diff --git a/.gitignore b/.gitignore index 0e82613dfd..f26121387e 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ client/cmd/translationsreport/translationsreport client/cmd/translationsreport/worksheets server/cmd/dexadm/dexadm server/cmd/geogame/geogame +internal/libsecp256k1/secp256k1 +internal/cmd/xmrswap/xmrswap diff --git a/dex/testing/xmr/README.md b/dex/testing/xmr/README.md new file mode 100644 index 0000000000..30df8ab7cd --- /dev/null +++ b/dex/testing/xmr/README.md @@ -0,0 +1,244 @@ +# 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 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,... + +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..ee70a4ccf1 --- /dev/null +++ b/dex/testing/xmr/harness.sh @@ -0,0 +1,638 @@ +#!/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 +################################################################################ + +# export PATH=$PATH:~/monero-x86_64-linux-gnu-v0.18.3.3 + +################################################################################ +# Monero RPC functions +################################################################################ + +source monero_functions + +################################################################################ +# 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" +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" +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" +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" + +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 "${OWN_WALLET_DIR}" +mkdir -p "${HARNESS_CTL_DIR}" +mkdir -p "${ALPHA_DATA_DIR}" +touch "${ALPHA_REGTEST_CFG}" + +# make available from the harness-ctl dir +cp monero_functions ${HARNESS_CTL_DIR} + +# Background watch mining in window 7 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" +# ----------------------------------------------------------------------------- + +# 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" </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" < "${ALPHA_REGTEST_CFG}" < 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 +} 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"