Skip to content

Commit

Permalink
zec: fix zcash multisplit (#2931)
Browse files Browse the repository at this point in the history
* fix zcash multisplit

* bump rpc version

* don't use btc.GetTransactionResult
  • Loading branch information
buck54321 committed Aug 26, 2024
1 parent bd2ee25 commit 436d31f
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 346 deletions.
9 changes: 4 additions & 5 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2626,7 +2626,7 @@ func (btc *baseWallet) submitMultiSplitTx(fundingCoins asset.Coins, spents []*Ou
Address: outputAddresses[i].String(),
}
}
btc.cm.LockOutputs(locks)
btc.cm.LockUTXOs(locks)
btc.node.lockUnspent(false, ops)

var totalOut uint64
Expand Down Expand Up @@ -2760,8 +2760,7 @@ func (btc *baseWallet) fundMultiWithSplit(keep, maxLock uint64, values []*asset.
}
}

btc.cm.LockOutputs(locks)

btc.cm.LockUTXOs(locks)
btc.node.lockUnspent(false, spents)

return coins, redeemScripts, splitFees, nil
Expand Down Expand Up @@ -3360,7 +3359,7 @@ func accelerateOrder(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes,

// Log it as a fundingCoin, since it is expected that this will be
// chained into further matches.
btc.cm.LockOutputs([]*UTxO{{
btc.cm.LockUTXOs([]*UTxO{{
TxHash: newChange.txHash(),
Vout: newChange.vout(),
Address: newChange.String(),
Expand Down Expand Up @@ -3880,7 +3879,7 @@ func (btc *baseWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, ui
})
}

btc.cm.LockOutputs(locks)
btc.cm.LockUTXOs(locks)
btc.cm.UnlockOutPoints(pts)

return receipts, changeCoin, fees, nil
Expand Down
6 changes: 4 additions & 2 deletions client/asset/btc/coinmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,10 @@ func (c *CoinManager) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
return coins, nil
}

// LockOutputs locks the specified utxos.
func (c *CoinManager) LockOutputs(utxos []*UTxO) {
// LockUTXOs locks the specified utxos.
// TODO: Move lockUnspent calls into this method instead of the caller doing it
// at every callsite, and because that's what we do with unlocking.
func (c *CoinManager) LockUTXOs(utxos []*UTxO) {
c.mtx.Lock()
for _, utxo := range utxos {
c.lockedOutputs[NewOutPoint(utxo.TxHash, utxo.Vout)] = utxo
Expand Down
246 changes: 246 additions & 0 deletions client/asset/zec/regnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"testing"
"time"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/client/asset/btc"
"decred.org/dcrdex/client/asset/btc/livetest"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/config"
dexbtc "decred.org/dcrdex/dex/networks/btc"
dexzec "decred.org/dcrdex/dex/networks/zec"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/rpcclient/v8"
)

Expand Down Expand Up @@ -152,3 +160,241 @@ func testDeserializeBlocks(t *testing.T, port string, upgradeHeights ...int64) {
ver--
}
}

func TestMultiSplit(t *testing.T) {
log := dex.StdOutLogger("T", dex.LevelTrace)
c := make(chan asset.WalletNotification, 16)
tmpDir, _ := os.MkdirTemp("", "")
defer os.RemoveAll(tmpDir)
walletCfg := &asset.WalletConfig{
Type: walletTypeRPC,
Settings: map[string]string{
"txsplit": "true",
"rpcuser": "user",
"rpcpassword": "pass",
"regtest": "1",
"rpcport": "33770",
},
Emit: asset.NewWalletEmitter(c, BipID, log),
PeersChange: func(u uint32, err error) {
log.Info("peers changed", u, err)
},
DataDir: tmpDir,
}
wi, err := NewWallet(walletCfg, log, dex.Simnet)
if err != nil {
t.Fatalf("Error making new wallet: %v", err)
}
w := wi.(*zecWallet)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
for {
select {
case n := <-c:
log.Infof("wallet note emitted: %+v", n)
case <-ctx.Done():
return
}
}
}()

cm := dex.NewConnectionMaster(w)
if err := cm.ConnectOnce(ctx); err != nil {
t.Fatalf("Error connecting wallet: %v", err)
}

// Unlock all transparent outputs.
if ops, err := listLockUnspent(w, log); err != nil {
t.Fatalf("Error listing unspent outputs: %v", err)
} else if len(ops) > 0 {
coins := make([]*btc.Output, len(ops))
for i, op := range ops {
txHash, _ := chainhash.NewHashFromStr(op.TxID)
coins[i] = btc.NewOutput(txHash, op.Vout, 0)
}
if err := lockUnspent(w, true, coins); err != nil {
t.Fatalf("Error unlocking coins")
}
log.Info("Unlocked %d transparent outputs", len(ops))
}

bals, err := w.balances()
if err != nil {
t.Fatalf("Error getting wallet balance: %v", err)
}

var v0, v1 uint64 = 1e8, 2e8
orderReq0, orderReq1 := dexzec.RequiredOrderFunds(v0, 1, dexbtc.RedeemP2PKHInputSize, 1), dexzec.RequiredOrderFunds(v1, 1, dexbtc.RedeemP2PKHInputSize, 2)

tAddr := func() string {
addr, err := transparentAddressString(w)
if err != nil {
t.Fatalf("Error getting transparent address: %v", err)
}
return addr
}

// Send everything to a transparent address.
unspents, err := listUnspent(w)
if err != nil {
t.Fatalf("listUnspent error: %v", err)
}
fees := dexzec.TxFeesZIP317(1+(dexbtc.RedeemP2PKHInputSize*uint64(len(unspents))), 1+(3*dexbtc.P2PKHOutputSize), 0, 0, 0, uint64(bals.orchard.noteCount))
netBal := bals.available() - fees
changeVal := netBal - orderReq0 - orderReq1

recips := []*zSendManyRecipient{
{Address: tAddr(), Amount: btcutil.Amount(orderReq0).ToBTC()},
{Address: tAddr(), Amount: btcutil.Amount(orderReq1).ToBTC()},
{Address: tAddr(), Amount: btcutil.Amount(changeVal).ToBTC()},
}

txHash, err := w.sendManyShielded(recips)
if err != nil {
t.Fatalf("sendManyShielded error: %v", err)
}

log.Infof("z_sendmany successful. txid = %s", txHash)

// Could be orchard notes. Mature them.
if err := mineAlpha(ctx); err != nil {
t.Fatalf("Error mining a block: %v", err)
}

// All funds should be transparent now.
multiFund := &asset.MultiOrder{
Version: version,
Values: []*asset.MultiOrderValue{
{Value: v0, MaxSwapCount: 1},
{Value: v1, MaxSwapCount: 2},
},
Options: map[string]string{"multisplit": "true"},
}

checkFundMulti := func(expSplit bool) {
t.Helper()
coinSets, _, fundingFees, err := w.FundMultiOrder(multiFund, 0)
if err != nil {
t.Fatalf("FundMultiOrder error: %v", err)
}

if len(coinSets) != 2 || len(coinSets[0]) != 1 || len(coinSets[1]) != 1 {
t.Fatalf("Expected 2 coin sets of len 1 each, got %+v", coinSets)
}

coin0, coin1 := coinSets[0][0], coinSets[1][0]

if err := w.cm.ReturnCoins(asset.Coins{coin0, coin1}); err != nil {
t.Fatalf("ReturnCoins error: %v", err)
}

if coin0.Value() != orderReq0 {
t.Fatalf("coin 0 had insufficient value: %d < %d", coin0.Value(), orderReq0)
}

if coin1.Value() < orderReq1 {
t.Fatalf("coin 1 had insufficient value: %d < %d", coin1.Value(), orderReq1)
}

// Should be no split tx.
split := fundingFees > 0
if split != expSplit {
t.Fatalf("Expected split %t, got %t", expSplit, split)
}

log.Infof("Coin 0: %s", coin0)
log.Infof("Coin 1: %s", coin1)
log.Infof("Funding fees: %d", fundingFees)
}

checkFundMulti(false) // no split

// Could be orchard notes. Mature them.
if err := mineAlpha(ctx); err != nil {
t.Fatalf("Error mining a block: %v", err)
}

// Send everything to a single transparent address to test for a
// fully-transparent split tx.
splitFees := dexzec.TxFeesZIP317(1+(3*dexbtc.RedeemP2PKHInputSize), 1+dexbtc.P2PKHOutputSize, 0, 0, 0, 0)
netBal -= splitFees
txHash, err = w.sendOneShielded(ctx, tAddr(), netBal, NoPrivacy)
if err != nil {
t.Fatalf("sendOneShielded(transparent) error: %v", err)
}
log.Infof("Sent all to transparent with tx %s", txHash)

// Could be orchard notes. Mature them.
if err := mineAlpha(ctx); err != nil {
t.Fatalf("Error mining a block: %v", err)
}

checkFundMulti(true) // fully-transparent split

// Could be orchard notes. Mature them.
if err := mineAlpha(ctx); err != nil {
t.Fatalf("Error mining a block: %v", err)
}

// Send everything to a shielded address.
addrRes, err := zGetAddressForAccount(w, shieldedAcctNumber, []string{transparentAddressType, orchardAddressType})
if err != nil {
t.Fatalf("zGetAddressForAccount error: %v", err)
}
receivers, err := zGetUnifiedReceivers(w, addrRes.Address)
if err != nil {
t.Fatalf("zGetUnifiedReceivers error: %v", err)
}
orchardAddr := receivers.Orchard

bals, err = w.balances()
if err != nil {
t.Fatalf("Error getting wallet balance: %v", err)
}
unspents, err = listUnspent(w)
if err != nil {
t.Fatalf("listUnspent error: %v", err)
}

splitFees = dexzec.TxFeesZIP317(1+(dexbtc.RedeemP2PKHInputSize*uint64(len(unspents))), 1, 0, 0, 0, uint64(bals.orchard.noteCount))
netBal = bals.available() - splitFees

txHash, err = w.sendOneShielded(ctx, orchardAddr, netBal, NoPrivacy)
if err != nil {
t.Fatalf("sendManyShielded error: %v", err)
}
log.Infof("sendOneShielded(shielded) successful. txid = %s", txHash)

// Could be orchard notes. Mature them.
if err := mineAlpha(ctx); err != nil {
t.Fatalf("Error mining a block: %v", err)
}

checkFundMulti(true) // shielded split

cancel()
cm.Wait()
}

func mineAlpha(ctx context.Context) error {
// Wait for txs to propagate
select {
case <-time.After(time.Second * 5):
case <-ctx.Done():
return ctx.Err()
}
// Mine
if err := exec.Command("tmux", "send-keys", "-t", "zec-harness:4", "./mine-alpha 1", "C-m").Run(); err != nil {
return err
}
// Wait for blocks to propagate
select {
case <-time.After(time.Second * 5):
case <-ctx.Done():
return ctx.Err()
}
return nil
}
4 changes: 2 additions & 2 deletions client/asset/zec/shielded_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ const (

// z_sendmany "fromaddress" [{"address":... ,"amount":...},...] ( minconf ) ( fee ) ( privacyPolicy )
func zSendMany(c rpcCaller, fromAddress string, recips []*zSendManyRecipient, priv privacyPolicy) (operationID string, err error) {
const minConf, fee = 1, 0.00001
return operationID, c.CallRPC(methodZSendMany, []any{fromAddress, recips, minConf, fee, priv}, &operationID)
const minConf = 1
return operationID, c.CallRPC(methodZSendMany, []any{fromAddress, recips, minConf, nil, priv}, &operationID)
}

type opResult struct {
Expand Down
17 changes: 14 additions & 3 deletions client/asset/zec/transparent_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,19 @@ type zTx struct {
blockHash *chainhash.Hash
}

type GetTransactionResult struct {
Confirmations int64 `json:"confirmations"`
BlockHash string `json:"blockhash"`
// BlockIndex int64 `json:"blockindex"` // unused, consider commenting
BlockTime uint64 `json:"blocktime"`
TxID string `json:"txid"`
Time uint64 `json:"time"`
TimeReceived uint64 `json:"timereceived"`
Bytes dex.Bytes `json:"hex"`
}

func getTransaction(c rpcCaller, txHash *chainhash.Hash) (*zTx, error) {
var tx btc.GetTransactionResult
var tx GetTransactionResult
if err := c.CallRPC("gettransaction", []any{txHash.String()}, &tx); err != nil {
return nil, err
}
Expand Down Expand Up @@ -245,8 +256,8 @@ func getRPCBlockHeader(c rpcCaller, blockHash *chainhash.Hash) (*btc.BlockHeader
return blkHeader, nil
}

func getWalletTransaction(c rpcCaller, txHash *chainhash.Hash) (*btc.GetTransactionResult, error) {
var tx btc.GetTransactionResult
func getWalletTransaction(c rpcCaller, txHash *chainhash.Hash) (*GetTransactionResult, error) {
var tx GetTransactionResult
err := c.CallRPC("gettransaction", []any{txHash.String()}, &tx)
if err != nil {
if btc.IsTxNotFoundErr(err) {
Expand Down
Loading

0 comments on commit 436d31f

Please sign in to comment.