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/go.mod b/go.mod index eb66645961..e2f9e8dee8 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module decred.org/dcrdex -go 1.19 +go 1.21.10 + +toolchain go1.22.4 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.23.4 github.com/btcsuite/btcd/btcec/v2 v2.2.2 github.com/btcsuite/btcd/btcutil v1.1.3 @@ -43,6 +46,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.0.0-20240607223259-5c9a0b540b3d github.com/dgraph-io/badger v1.6.2 github.com/ethereum/go-ethereum v1.11.5 github.com/fatih/color v1.11.0 @@ -51,6 +55,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 @@ -80,7 +85,6 @@ require ( github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/VictoriaMetrics/fastcache v1.6.0 // 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/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect diff --git a/go.sum b/go.sum index 113b2f0fa8..618a163119 100644 --- a/go.sum +++ b/go.sum @@ -178,6 +178,7 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -262,6 +263,7 @@ github.com/decred/dcrd/blockchain/stake/v5 v5.0.1/go.mod h1:y1tMD1TssTlPmKDYbSrF github.com/decred/dcrd/blockchain/standalone/v2 v2.2.1 h1:zeI9CHkLM9be4QOBmIAtoPfs6NCgJM1lpmRUYE61I8o= github.com/decred/dcrd/blockchain/standalone/v2 v2.2.1/go.mod h1:yXZz/EgWdGw5nqMEvyKj/iXZ9I2VSyO95xKj6mRUMIM= github.com/decred/dcrd/blockchain/v5 v5.0.1 h1:IGr8rJsgBVKDBI8STzeuGF6Mej0xbIX4gVVBA9yEMRU= +github.com/decred/dcrd/blockchain/v5 v5.0.1/go.mod h1:LtSV1+u8aBQzlExAQcl4HIJ6Bfi5f6Rvws/9euH4mDA= github.com/decred/dcrd/certgen v1.1.3 h1:MYENpBWVSP6FkkLBSSnaBGEOWobPcgYBLDDo88szi9c= github.com/decred/dcrd/certgen v1.1.3/go.mod h1:Od5y39J+r2ZlvrizyWu2cylcYu0+emTTVm3eix4W8bw= github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU= @@ -315,6 +317,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.0.0-20240607223259-5c9a0b540b3d h1:+PB0b05axI9P6XBXcc2MfbxDScrLOW62WlieddYbaXQ= +github.com/dev-warrior777/go-monero v0.0.0-20240607223259-5c9a0b540b3d/go.mod h1:oP7WZQhC9BsCDFmyfpAofVNe7fEOaa0LrjAU05I5TlY= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= 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= @@ -325,6 +329,7 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/docker v1.6.2 h1:HlFGsy+9/xrgMmhmN+NGhCc5SHGJ7I+kHosRR1xc/aI= +github.com/docker/docker v1.6.2/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -385,8 +390,11 @@ github.com/gcash/bchutil v0.0.0-20200506001747-c2894cd54b33/go.mod h1:wB++2ZcHUv github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000 h1:vVi7Ym3I9T4ZKhQy0/XLKzS3xAqX4K+/cSAmnvMR+HM= github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000/go.mod h1:H2USFGwtiu6CNMxiVQPqZkDzsoVSt9BLNqTfBBqGXRo= github.com/gcash/bchwallet v0.8.3-0.20210524112536-14ca25bc6549 h1:hC4Qxxvq0SN/1Jhqzh6ETZoBOXBG5PMBTYkTQqBcPKQ= +github.com/gcash/bchwallet v0.8.3-0.20210524112536-14ca25bc6549/go.mod h1:4hGEkghrAx5yIOdvakkRP5ZT8K/ZMtJhrXXr1Mc6lz4= github.com/gcash/bchwallet/walletdb v0.0.0-20210524044131-61bcca2ae6f9 h1:lryOGJ7VBv4zFpLtFEsH8ik68OnTwN3A71cisqXgN6w= +github.com/gcash/bchwallet/walletdb v0.0.0-20210524044131-61bcca2ae6f9/go.mod h1:KNPI56t8uMlItrGyu3tS4NCd3jGfA6KjdUg4bsk+QJg= github.com/gcash/neutrino v0.0.0-20210524114821-3b1878290cf9 h1:V5UNzi/5pZxE5s6kfCe59VjJRmfkyI+npZizMcAvEdI= +github.com/gcash/neutrino v0.0.0-20210524114821-3b1878290cf9/go.mod h1:MshBO/Xf8SCndZFetZ8yg79db/JghnOiMmPiY1Eatlw= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= @@ -399,6 +407,7 @@ github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL github.com/go-critic/go-critic v0.5.6/go.mod h1:cVjj0DfqewQVIlIAGexPCaGaZDAqGE29PYDDADIVNEo= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -529,6 +538,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -617,6 +627,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/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= @@ -1078,6 +1090,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -1089,6 +1102,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -1681,6 +1695,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/internal/adaptorsigs/dcr/dcr.go b/internal/adaptorsigs/dcr/dcr.go new file mode 100644 index 0000000000..8c9a00de4e --- /dev/null +++ b/internal/adaptorsigs/dcr/dcr.go @@ -0,0 +1,239 @@ +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() +} + +// def verifySCLockTx(self, tx_bytes, script_out, +// swap_value, +// Kal, Kaf, +// feerate, +// check_lock_tx_inputs, vkbv=None): +// # Verify: +// # +// +// # Not necessary to check the lock txn is mineable, as protocol will wait for it to confirm +// # However by checking early we can avoid wasting time processing unmineable txns +// # Check fee is reasonable +// +// tx = self.loadTx(tx_bytes) +// txid = self.getTxid(tx) +// self._log.info('Verifying lock tx: {}.'.format(b2h(txid))) +// +// ensure(tx.nVersion == self.txVersion(), 'Bad version') +// ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores +// +// 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 value +// ensure(locked_coin == swap_value, 'Bad locked value') +// +// # Check script +// A, B = self.extractScriptLockScriptValues(script_out) +// ensure(A == Kal, 'Bad script pubkey') +// ensure(B == Kaf, 'Bad script pubkey') +// +// if check_lock_tx_inputs: +// # TODO: Check that inputs are unspent +// # Verify fee rate +// inputs_value = 0 +// add_bytes = 0 +// add_witness_bytes = getCompactSizeLen(len(tx.vin)) +// for pi in tx.vin: +// ptx = self.rpc('getrawtransaction', [i2h(pi.prevout.hash), True]) +// prevout = ptx['vout'][pi.prevout.n] +// inputs_value += make_int(prevout['value']) +// +// prevout_type = prevout['scriptPubKey']['type'] +// if prevout_type == 'witness_v0_keyhash': +// add_witness_bytes += 107 # sig 72, pk 33 and 2 size bytes +// add_witness_bytes += getCompactSizeLen(107) +// else: +// # Assume P2PKH, TODO more types +// add_bytes += 107 # OP_PUSH72 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..cee26672c6 --- /dev/null +++ b/internal/cmd/xmrswap/main.go @@ -0,0 +1,708 @@ +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/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 + 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, pkbv, pkbs *edwards.PublicKey + kaf, kal *secp256k1.PrivateKey + pkal, pkaf, pkasl *secp256k1.PublicKey + dleag [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 +} +func run(ctx context.Context) error { + return runSuccess(ctx) +} + +func (c *client) generateDleag() (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. + c.kbvf, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + // This private key becomes the proof. Not shared. + c.kbsf, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + c.pkbsf = c.kbsf.PubKey() + + c.kaf, err = secp256k1.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + // Share this pubkey with the other party. + c.pkaf = c.kaf.PubKey() + + // Share dleag with bob. + c.dleag, err = libsecp256k1.Ed25519DleagProve(c.kbsf) + if err != nil { + return fail(err) + } + + c.pkasl, err = secp256k1.ParsePubKey(c.dleag[:33]) + if err != nil { + return fail(err) + } + + return c.pkbsf, c.kbvf, c.pkaf, c.dleag, nil +} + +func (c *client) generateLockTxn(ctx context.Context, pkbsf *edwards.PublicKey, kbvf *edwards.PrivateKey, pkaf *secp256k1.PublicKey, dleag [libsecp256k1.ProofLen]byte) (refundSig, p2shLockScript, lockTxScript []byte, refundTx *wire.MsgTx, lockTxVout int, pkbv, pkbs *edwards.PublicKey, err error) { + fail := func(err error) ([]byte, []byte, []byte, *wire.MsgTx, int, *edwards.PublicKey, *edwards.PublicKey, error) { + return nil, nil, nil, nil, 0, nil, nil, err + } + c.dleag = dleag + c.pkasl, err = secp256k1.ParsePubKey(c.dleag[:33]) + if err != nil { + return fail(err) + } + c.kbvf = kbvf + c.pkbsf = pkbsf + c.pkaf = pkaf + c.kbvl, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + c.kbsl, err = edwards.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + c.kal, err = secp256k1.GeneratePrivateKey() + if err != nil { + return fail(err) + } + + pkal := c.kal.PubKey() + + 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)) + } + + c.pkbv = sumPubKeys(c.kbvl.PubKey(), c.kbvf.PubKey()) + c.pkbs = sumPubKeys(c.kbsl.PubKey(), c.pkbsf) + + 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, + } + c.lockTx = wire.NewMsgTx() + c.lockTx.AddTxOut(txOut) + txBytes, err := c.lockTx.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(30) // seconds + durationLocktime &= wire.SequenceLockTimeIsSeconds + 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() + // Add the transaction output. + 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) + txIn.Sequence = uint32(durationLocktime) + refundTx.AddTxIn(txIn) + + // This sig must be shared with Alice. + refundSigBob, err := sign.SignatureScript(refundTx, c.vIn, p2shLockScript, txscript.SigHashAll, c.kal.Serialize(), dcrec.STEcdsaSecp256k1, true) + if err != nil { + return fail(err) + } + + newAddr, err := c.dcr.GetNewAddress(ctx, "default") + if err != nil { + return fail(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) + return refundSigBob, p2shLockScript, lockTxScript, refundTx, c.vIn, c.pkbv, c.pkbs, nil +} + +func (c *client) generateRefundSigs(refundTx *wire.MsgTx, vIn int, p2shLockScript []byte) (esig [libsecp256k1.CTLen]byte, refundSig []byte, err error) { + c.vIn = vIn + fail := func(err error) ([libsecp256k1.CTLen]byte, []byte, error) { + return [libsecp256k1.CTLen]byte{}, nil, err + } + refundTxHash := refundTx.TxHash() + + esig, err = libsecp256k1.EcdsaotvesEncSign(c.kaf, c.pkasl, refundTxHash) + if err != nil { + return fail(err) + } + + // Share with bob. + refundSig, err = sign.SignatureScript(refundTx, c.vIn, p2shLockScript, txscript.SigHashAll, c.kaf.Serialize(), dcrec.STEcdsaSecp256k1, true) + if err != nil { + return fail(err) + } + return esig, refundSig, nil +} + +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.NewAddressPubKeyEcdsaSecp256k1(0, c.pkasl, 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("unalbe to send lock tx: %v")) + } + return spendTx, nil +} + +func (c *client) initXmr(ctx context.Context, pkbv, pkbs *edwards.PublicKey) error { + c.pkbv = pkbv + c.pkbs = pkbs + var fullPubKey []byte + fullPubKey = append(fullPubKey, c.pkbs.SerializeCompressed()...) + fullPubKey = append(fullPubKey, c.pkbv.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 +} + +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 +} + +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 + + time.Sleep(time.Second * 5) + + _, err = c.dcr.SendRawTransaction(ctx, spendTx, false) + if err != nil { + return nil, err + } + // TODO: Check balance changed as expected. + + return kalSig, nil +} + +func (c *client) redeemXmr(ctx context.Context, kalSig []byte) error { + kaslRecovered, err := libsecp256k1.EcdsaotvesRecEncKey(c.pkasl, c.lockTxEsig, kalSig[:len(kalSig)-1]) + if err != nil { + return err + } + + kbsfRecovered, _, err := edwards.PrivKeyFromScalar(kaslRecovered.Serialize()) + if err != nil { + return fmt.Errorf("unable to recover kbsf: %v", err) + } + vkbsBig := scalarAdd(kbsfRecovered.GetD(), c.kbsl.GetD()) + vkbsBig.Mod(vkbsBig, curve.N) + var vkbsBytes [32]byte + vkbsBig.FillBytes(vkbsBytes[:]) + vkbs, _, err := edwards.PrivKeyFromScalar(vkbsBytes[:]) + if err != nil { + return 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 err + } + + time.Sleep(15 * time.Second) + + balReq := rpc.GetBalanceRequest{} + balRes, err := xmrChecker.GetBalance(ctx, &balReq) + if err != nil { + return err + } + if balRes.Balance != xmrAmt { + return fmt.Errorf("expected redeem xmr balance of %d but got %d", xmrAmt, balRes.Balance) + } + fmt.Printf("new xmr wallet balance\n%+v\n", *balRes) + return nil +} + +func runSuccess(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 xmr balance\n%+v\n", *balRes) + + bob, err := newClient(ctx, "http://127.0.0.1:28184/json_rpc", "trading1") + if err != nil { + return err + } + + // Alice generates dleag. + + pkbsf, kbvf, pkaf, dleag, err := alice.generateDleag() + if err != nil { + return err + } + + // Bob generates transactions but does not send anything yet. + + _, p2shLockScript, lockTxScript, refundTx, vIn, pkbv, pkbs, err := bob.generateLockTxn(ctx, pkbsf, kbvf, pkaf, dleag) + if err != nil { + return fmt.Errorf("unalbe to generate lock transactions: %v", err) + } + + // Alice signs a refund script for Bob. + + _, _, err = alice.generateRefundSigs(refundTx, vIn, p2shLockScript) + 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, pkbv, pkbs); err != nil { + return err + } + + // 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 + } + + // Bob redeems the xmr with the dcr signature. + if err := bob.redeemXmr(ctx, kalSig); err != nil { + return err + } + return nil +}