diff --git a/.gitignore b/.gitignore index 04c9850784..87563dd17c 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ client/cmd/translationsreport/translationsreport client/cmd/translationsreport/worksheets server/cmd/dexadm/dexadm internal/libsecp256k1/secp256k1 +internal/cmd/xmrswap/xmrswap diff --git a/dex/testing/xmr/harness.sh b/dex/testing/xmr/harness.sh index 876fef3e2e..53a7e66e84 100755 --- a/dex/testing/xmr/harness.sh +++ b/dex/testing/xmr/harness.sh @@ -38,6 +38,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 BOB_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 +64,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" +BOB_WALLET_DIR="${NODES_ROOT}/wallets/bob" HARNESS_CTL_DIR="${NODES_ROOT}/harness-ctl" ALPHA_DATA_DIR="${NODES_ROOT}/alpha" ALPHA_REGTEST_CFG="${ALPHA_DATA_DIR}/alpha.conf" @@ -74,6 +76,7 @@ mkdir -p "${FRED_WALLET_DIR}" mkdir -p "${BILL_WALLET_DIR}" mkdir -p "${CHARLIE_WALLET_DIR}" mkdir -p "${CHARLIE_VIEW_WALLET_DIR}" +mkdir -p "${BOB_WALLET_DIR}" mkdir -p "${HARNESS_CTL_DIR}" mkdir -p "${ALPHA_DATA_DIR}" touch "${ALPHA_REGTEST_CFG}" # currently empty @@ -381,9 +384,11 @@ cat > "${NODES_ROOT}/harness-ctl/quit" < OP_PUSH33 +// +// outputs_value = 0 +// for txo in tx.vout: +// outputs_value += txo.nValue +// fee_paid = inputs_value - outputs_value +// assert (fee_paid > 0) +// +// vsize = self.getTxVSize(tx, add_bytes, add_witness_bytes) +// fee_rate_paid = fee_paid * 1000 // vsize +// +// self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid) +// +// if not self.compareFeeRates(fee_rate_paid, feerate): +// self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate) +// # TODO: Display warning to user +// +// return txid, locked_n +// +// def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out, +// prevout_id, prevout_n, prevout_seq, prevout_script, +// Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None): +// # Verify: +// # Must have only one input with correct prevout and sequence +// # Must have only one output to the p2wsh of the lock refund script +// # Output value must be locked_coin - lock tx fee +// +// tx = self.loadTx(tx_bytes) +// txid = self.getTxid(tx) +// self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid))) +// +// ensure(tx.nVersion == self.txVersion(), 'Bad version') +// ensure(tx.nLockTime == 0, 'nLockTime not 0') +// ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') +// +// ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence') +// ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script), 'Input scriptsig mismatch') +// ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch') +// +// ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') +// +// script_pk = self.getScriptDest(script_out) +// locked_n = findOutput(tx, script_pk) +// ensure(locked_n is not None, 'Output not found in tx') +// locked_coin = tx.vout[locked_n].nValue +// +// # Check script and values +// A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out) +// ensure(A == Kal, 'Bad script pubkey') +// ensure(B == Kaf, 'Bad script pubkey') +// ensure(csv_val == csv_val_expect, 'Bad script csv value') +// ensure(C == Kaf, 'Bad script pubkey') +// +// fee_paid = swap_value - locked_coin +// assert (fee_paid > 0) +// +// dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) +// witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) +// vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) +// fee_rate_paid = fee_paid * 1000 // vsize +// +// self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid) +// +// if not self.compareFeeRates(fee_rate_paid, feerate): +// raise ValueError('Bad fee rate, expected: {}'.format(feerate)) +// +// return txid, locked_coin, locked_n +// +// def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes, +// lock_refund_tx_id, prevout_script, +// Kal, +// prevout_n, prevout_value, feerate, vkbv=None): +// # Verify: +// # Must have only one input with correct prevout (n is always 0) and sequence +// # Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr +// tx = self.loadTx(tx_bytes) +// txid = self.getTxid(tx) +// self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid))) +// +// ensure(tx.nVersion == self.txVersion(), 'Bad version') +// ensure(tx.nLockTime == 0, 'nLockTime not 0') +// ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') +// +// ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence') +// ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script), 'Input scriptsig mismatch') +// ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch') +// +// ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') +// +// # Destination doesn't matter to the follower +// ''' +// p2wpkh = CScript([OP_0, hash160(Kal)]) +// locked_n = findOutput(tx, p2wpkh) +// ensure(locked_n is not None, 'Output not found in lock refund spend tx') +// ''' +// tx_value = tx.vout[0].nValue +// +// fee_paid = prevout_value - tx_value +// assert (fee_paid > 0) +// +// dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script) +// witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) +// vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) +// fee_rate_paid = fee_paid * 1000 // vsize +// +// self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx_value, vsize, fee_rate_paid) +// +// if not self.compareFeeRates(fee_rate_paid, feerate): +// raise ValueError('Bad fee rate, expected: {}'.format(feerate)) +// +// return True +// +// def verifySCLockSpendTx(self, tx_bytes, +// lock_tx_bytes, lock_tx_script, +// a_pkhash_f, feerate, vkbv=None): +// # Verify: +// # Must have only one input with correct prevout (n is always 0) and sequence +// # Must have only one output with destination and amount +// +// tx = self.loadTx(tx_bytes) +// txid = self.getTxid(tx) +// self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid))) +// +// ensure(tx.nVersion == self.txVersion(), 'Bad version') +// ensure(tx.nLockTime == 0, 'nLockTime not 0') +// ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') +// +// lock_tx = self.loadTx(lock_tx_bytes) +// lock_tx_id = self.getTxid(lock_tx) +// +// output_script = self.getScriptDest(lock_tx_script) +// locked_n = findOutput(lock_tx, output_script) +// ensure(locked_n is not None, 'Output not found in tx') +// locked_coin = lock_tx.vout[locked_n].nValue +// +// ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence') +// ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(lock_tx_script), 'Input scriptsig mismatch') +// ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch') +// +// ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') +// p2wpkh = self.getScriptForPubkeyHash(a_pkhash_f) +// ensure(tx.vout[0].scriptPubKey == p2wpkh, 'Bad output destination') +// +// # The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount +// fee_paid = locked_coin - tx.vout[0].nValue +// assert (fee_paid > 0) +// +// dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) +// witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) +// vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) +// fee_rate_paid = fee_paid * 1000 // vsize +// +// self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_rate_paid) +// +// if not self.compareFeeRates(fee_rate_paid, feerate): +// raise ValueError('Bad fee rate, expected: {}'.format(feerate)) +// +// return True diff --git a/internal/cmd/xmrswap/main.go b/internal/cmd/xmrswap/main.go new file mode 100644 index 0000000000..ba121290b6 --- /dev/null +++ b/internal/cmd/xmrswap/main.go @@ -0,0 +1,585 @@ +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/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/haven-protocol-org/monero-go-utils/base58" +) + +// fieldIntSize is the size of a field element encoded +// as bytes. +const ( + fieldIntSize = 32 + dcrAmt = 7_000_000 // atoms + xmrAmt = 1_000 // 1e12 units +) + +var ( + homeDir = os.Getenv("HOME") + dextestDir = filepath.Join(homeDir, "dextest") +) + +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 +} + +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 and returns the sum mod N. +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 { + curve := edwards.Edwards() + 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)) +} + +func run(ctx context.Context) error { + alice, err := newClient(ctx, "http://127.0.0.1:28284/json_rpc", "alpha") + if err != nil { + return err + } + balReq := rpc.GetBalanceRequest{ + AccountIndex: 0, + } + balRes, err := alice.xmr.GetBalance(ctx, &balReq) + if err != nil { + return err + } + fmt.Printf("alice balance\n%+v\n", *balRes) + + bob, err := newClient(ctx, "http://127.0.0.1:28184/json_rpc", "trading1") + if err != nil { + return err + } + + // Alice does stuff. + + // This private key is shared with bob. + kbvf, err := edwards.GeneratePrivateKey() + if err != nil { + return err + } + + // This private key becomes the proof. Not shared. + kbsf, err := edwards.GeneratePrivateKey() + if err != nil { + return err + } + + kaf, err := secp256k1.GeneratePrivateKey() + if err != nil { + return err + } + + // Share this pubkey with the other party. + pkaf := kaf.PubKey() + + // Share dleag with bob. + dleag, err := libsecp256k1.Ed25519DleagProve(kbsf) + if err != nil { + return err + } + + pkasl, err := secp256k1.ParsePubKey(dleag[:33]) + if err != nil { + return err + } + + // Bob does stuff. + + kbvl, err := edwards.GeneratePrivateKey() + if err != nil { + return err + } + + kbsl, err := edwards.GeneratePrivateKey() + if err != nil { + return err + } + + kal, err := secp256k1.GeneratePrivateKey() + if err != nil { + return err + } + + pkal := kal.PubKey() + + vkbvBig := scalarAdd(new(big.Int).SetBytes(kbvf.Serialize()), new(big.Int).SetBytes(kbvl.Serialize())) + vkbvBytes := [32]byte{} + vkbvBig.FillBytes(vkbvBytes[:]) + vkbv, _ := edwards.PrivKeyFromSecret(vkbvBytes[:]) + + pkbv := sumPubKeys(kbvl.PubKey(), kbvf.PubKey()) + pkbs := sumPubKeys(kbsl.PubKey(), kbsf.PubKey()) + + lockTxScript, err := dcradaptor.LockTxScript(pkal.SerializeCompressed(), pkaf.SerializeCompressed()) + if err != nil { + return err + } + + scriptAddr, err := stdaddr.NewAddressScriptHashV0(lockTxScript, bob.dcr.chainParams) + if err != nil { + return 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, + } + lockTx := wire.NewMsgTx() + lockTx.AddTxOut(txOut) + txBytes, err := lockTx.Bytes() + if err != nil { + return err + } + + fundRes, err := bob.dcr.FundRawTransaction(ctx, hex.EncodeToString(txBytes), "default", dcrwalletjson.FundRawTransactionOptions{}) + if err != nil { + return err + } + + txBytes, err = hex.DecodeString(fundRes.Hex) + if err != nil { + return err + } + + lockTx = wire.NewMsgTx() + if err = lockTx.FromBytes(txBytes); err != nil { + return err + } + vIn := 0 + for i, out := range lockTx.TxOut { + if bytes.Equal(out.PkScript, p2shLockScript) { + vIn = i + break + } + } + + durationLocktime := int64(30) // seconds + durationLocktime &= wire.SequenceLockTimeIsSeconds + lockRefundTxScript, err := dcradaptor.LockRefundTxScript(pkal.SerializeCompressed(), pkaf.SerializeCompressed(), durationLocktime) + if err != nil { + return err + } + + scriptAddr, err = stdaddr.NewAddressScriptHashV0(lockRefundTxScript, bob.dcr.chainParams) + if err != nil { + return fmt.Errorf("error encoding script address: %w", err) + } + p2shScriptVer, p2shScript := scriptAddr.PaymentScript() + // Add the transaction output. + dumbFee := int64(6000) + txOut = &wire.TxOut{ + Value: dcrAmt - dumbFee, + Version: p2shScriptVer, + PkScript: p2shScript, + } + refundTx := wire.NewMsgTx() + refundTx.AddTxOut(txOut) + h := lockTx.TxHash() + op := wire.NewOutPoint(&h, uint32(vIn), 0) + txIn := wire.NewTxIn(op, dcrAmt, nil) + txIn.Sequence = uint32(durationLocktime) + refundTx.AddTxIn(txIn) + + // This sig must be shared with Alice. + refundSigBob, err := sign.SignatureScript(refundTx, vIn, p2shLockScript, txscript.SigHashAll, kal.Serialize(), dcrec.STEcdsaSecp256k1, true) + if err != nil { + return err + } + + newAddr, err := bob.dcr.GetNewAddress(ctx, "default") + if err != nil { + return err + } + p2AddrScriptVer, p2AddrScript := newAddr.PaymentScript() + // Add the transaction output. + txOut = &wire.TxOut{ + Value: dcrAmt - 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) + spendRefundTx.AddTxIn(txIn) + + // Alice does more stuff. + + aliceEsig, err := libsecp256k1.EcdsaotvesEncSign(kaf, pkasl, h) + if err != nil { + return err + } + + // Share with bob. + refundSigAlice, err := sign.SignatureScript(refundTx, vIn, p2shLockScript, txscript.SigHashAll, kaf.Serialize(), dcrec.STEcdsaSecp256k1, true) + if err != nil { + return err + } + + // Bob does stuff. + + pkaslAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1(0, pkasl, bob.dcr.chainParams) + if err != nil { + return err + } + p2AddrScriptVer, p2AddrScript = pkaslAddr.PaymentScript() + + txOut = &wire.TxOut{ + Value: dcrAmt - dumbFee, + Version: p2AddrScriptVer, + PkScript: p2AddrScript, + } + spendTx := wire.NewMsgTx() + spendTx.AddTxOut(txOut) + h = lockTx.TxHash() + op = wire.NewOutPoint(&h, uint32(vIn), 0) + txIn = wire.NewTxIn(op, dcrAmt, nil) + spendTx.AddTxIn(txIn) + + hash, err := txscript.CalcSignatureHash(lockTxScript, txscript.SigHashAll, spendTx, 0, nil) + if err != nil { + return err + } + + tx, complete, err := bob.dcr.SignRawTransaction(ctx, lockTx) + if err != nil { + return err + } + if !complete { + return errors.New("lock tx sign not complete") + } + + _, err = bob.dcr.SendRawTransaction(ctx, tx, false) + if err != nil { + return fmt.Errorf("unalbe to send lock tx: %v") + } + + // Alice inits. + var fullPubKey []byte + fullPubKey = append(fullPubKey, pkbv.SerializeCompressed()...) + fullPubKey = append(fullPubKey, pkbs.SerializeCompressed()...) + + sharedAddr := base58.EncodeAddr(18, fullPubKey) + + dest := rpc.Destination{ + Amount: xmrAmt, + Address: string(sharedAddr), + } + sendReq := rpc.TransferRequest{ + Destinations: []rpc.Destination{dest}, + } + + sendRes, err := alice.xmr.Transfer(ctx, &sendReq) + if err != nil { + return err + } + fmt.Printf("xmr sent\n%+v\n", *sendRes) + + // Bob sends esig. + + copy(h[:], hash) + + bobEsig, err := libsecp256k1.EcdsaotvesEncSign(kal, pkasl, h) + if err != nil { + return err + } + + // Alice redeems. + + kasl := secp256k1.PrivKeyFromBytes(kbsf.Serialize()) + kalSig, err := libsecp256k1.EcdsaotvesDecSig(kasl, bobEsig) + if err != nil { + return err + } + kalSig = append(kalSig, byte(txscript.SigHashAll)) + + kafSig, err := sign.RawTxInSignature(spendTx, 0, lockTxScript, txscript.SigHashAll, kaf.Serialize(), dcrec.STEcdsaSecp256k1) + if err != nil { + return err + } + + spendSig, err := txscript.NewScriptBuilder(). + AddData(kalSig). + AddData(kafSig). + AddData(lockTxScript). + Script() + + spendTx.TxIn[0].SignatureScript = spendSig + + time.Sleep(time.Second * 5) + + _, err = alice.dcr.SendRawTransaction(ctx, spendTx, false) + if err != nil { + return err + } + + // Bob gets the sig and encoding key. + kaslRecovered, err := libsecp256k1.EcdsaotvesRecEncKey(pkasl, bobEsig, kalSig[:len(kalSig)-1]) + if err != nil { + return err + } + + kbsfRecovered, _ := edwards.PrivKeyFromSecret(kaslRecovered.Serialize()) + vkbsBig := scalarAdd(new(big.Int).SetBytes(kbsfRecovered.Serialize()), new(big.Int).SetBytes(kbsl.Serialize())) + vkbsBytes := [32]byte{} + vkbsBig.FillBytes(vkbsBytes[:]) + vkbs, _ := edwards.PrivKeyFromSecret(vkbsBytes[:]) + + fullPubKey = []byte{} + fullPubKey = append(fullPubKey, vkbv.PubKey().Serialize()...) + fullPubKey = append(fullPubKey, vkbs.PubKey().Serialize()...) + + walletAddr := base58.EncodeAddr(18, fullPubKey) + walletFileName := fmt.Sprintf("%s_spend", walletAddr) + reverse(&vkbsBytes) + reverse(&vkbvBytes) + + xmrChecker := rpc.New(rpc.Config{ + Address: "http://127.0.0.1:28484/json_rpc", + Client: &http.Client{}, + }) + + err = xmrChecker.CloseWallet(ctx) + if err != nil { + fmt.Printf("cant close wallet: %v\n", err) + } + + genReq := rpc.GenerateFromKeysRequest{ + Filename: walletFileName, + Address: walletAddr, + SpendKey: hex.EncodeToString(vkbsBytes[:]), + ViewKey: hex.EncodeToString(vkbvBytes[:]), + // Password: "pass", + } + + genRes, err := xmrChecker.GenerateFromKeys(ctx, &genReq) + if err != nil { + return err + } + fmt.Printf("gen from keys\n%+v\n", *genRes) + + openReq := rpc.OpenWalletRequest{ + Filename: walletAddr, + // Password: "pass", + } + + err = xmrChecker.OpenWallet(ctx, &openReq) + if err != nil { + return err + } + + balReq = rpc.GetBalanceRequest{} + balRes, err = xmrChecker.GetBalance(ctx, &balReq) + if err != nil { + return err + } + fmt.Printf("new wallet balance\n%+v\n", *balRes) + + _, _, _ = refundSigBob, aliceEsig, refundSigAlice + + return nil +}