Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/core/btc/ui: Accelerate Orders using CPFP #1555

Merged
merged 13 commits into from May 20, 2022
624 changes: 620 additions & 4 deletions client/asset/btc/btc.go

Large diffs are not rendered by default.

1,085 changes: 1,062 additions & 23 deletions client/asset/btc/btc_test.go

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions client/asset/btc/spv.go
Expand Up @@ -1799,23 +1799,22 @@ func (w *spvWallet) getTransaction(txHash *chainhash.Hash) (*GetTransactionResul

// TODO: The serialized transaction is already in the DB, so
// reserializing can be avoided here.
var txBuf bytes.Buffer
txBuf.Grow(details.MsgTx.SerializeSize())
err = details.MsgTx.Serialize(&txBuf)
txRaw, err := serializeMsgTx(&details.MsgTx)
if err != nil {
return nil, err
}

ret := &GetTransactionResult{
TxID: txHash.String(),
Hex: txBuf.Bytes(), // 'Hex' field name is a lie, kinda
Hex: txRaw, // 'Hex' field name is a lie, kinda
Time: uint64(details.Received.Unix()),
TimeReceived: uint64(details.Received.Unix()),
}

if details.Block.Height != -1 {
ret.BlockHash = details.Block.Hash.String()
ret.BlockTime = uint64(details.Block.Time.Unix())
ret.BlockHeight = uint64(details.Block.Height)
ret.Confirmations = uint64(confirms(details.Block.Height, syncBlock.Height))
}

Expand Down
72 changes: 54 additions & 18 deletions client/asset/btc/spv_test.go
Expand Up @@ -204,46 +204,81 @@ func (c *tBtcWallet) walletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetail
if c.getTransactionErr != nil {
return nil, c.getTransactionErr
}
if c.testData.getTransaction == nil {
var txData *GetTransactionResult
if c.getTransactionMap != nil {
if txData = c.getTransactionMap["any"]; txData == nil {
txData = c.getTransactionMap[txHash.String()]
}
}
if txData == nil {
return nil, WalletTransactionNotFound
}

txData := c.testData.getTransaction
tx, _ := msgTxFromBytes(txData.Hex)

blockHash, _ := chainhash.NewHashFromStr(txData.BlockHash)

blk := c.getBlock(txData.BlockHash)
var blockHeight int32
if blk != nil {
blockHeight = int32(blk.height)
} else {
blockHeight = -1
}

credits := make([]wtxmgr.CreditRecord, 0, len(tx.TxIn))
for i := range tx.TxIn {
debits := make([]wtxmgr.DebitRecord, 0, len(tx.TxIn))
for i, in := range tx.TxIn {
credits = append(credits, wtxmgr.CreditRecord{
// Amount:,
Index: uint32(i),
Spent: c.walletTxSpent,
// Change: ,
})
}

var debitAmount int64
// The sources of transaction inputs all need to be added to getTransactionMap
// in order to get accurate Fees and Amounts when calling GetWalletTransaction
// when using the SPV wallet.
if gtr := c.getTransactionMap[in.PreviousOutPoint.Hash.String()]; gtr != nil {
tx, _ := msgTxFromBytes(gtr.Hex)
debitAmount = tx.TxOut[in.PreviousOutPoint.Index].Value
}

debits = append(debits, wtxmgr.DebitRecord{
Amount: btcutil.Amount(debitAmount),
})

}
return &wtxmgr.TxDetails{
TxRecord: wtxmgr.TxRecord{
MsgTx: *tx,
MsgTx: *tx,
Received: time.Unix(int64(txData.Time), 0),
},
Block: wtxmgr.BlockMeta{
Block: wtxmgr.Block{
Hash: *blockHash,
Height: int32(blk.height),
Height: blockHeight,
},
},
Credits: credits,
Debits: debits,
}, nil
}

func (c *tBtcWallet) getTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) {
if c.getTransactionErr != nil {
return nil, c.getTransactionErr
}
return c.testData.getTransaction, nil
var txData *GetTransactionResult
if c.getTransactionMap != nil {
if txData = c.getTransactionMap["any"]; txData == nil {
txData = c.getTransactionMap[txHash.String()]
}
}
if txData == nil {
return nil, WalletTransactionNotFound
}
return txData, nil
}

func (c *tBtcWallet) syncedTo() waddrmgr.BlockStamp {
Expand Down Expand Up @@ -419,14 +454,15 @@ func TestSwapConfirmations(t *testing.T) {
node.confs = 10
node.confsSpent = true
txB, _ := serializeMsgTx(swapTx)
node.getTransaction = &GetTransactionResult{
BlockHash: swapBlockHash.String(),
BlockIndex: swapHeight,
Hex: txB,
}
node.getTransactionMap = map[string]*GetTransactionResult{
"any": &GetTransactionResult{
BlockHash: swapBlockHash.String(),
BlockIndex: swapHeight,
Hex: txB,
}}
node.walletTxSpent = true
checkSuccess("confirmations", swapConfs, true)
node.getTransaction = nil
node.getTransactionMap = nil
node.walletTxSpent = false
node.confsErr = WalletTransactionNotFound

Expand Down Expand Up @@ -555,10 +591,10 @@ func TestGetTxOut(t *testing.T) {

// Wallet transaction found
node.getTransactionErr = nil
node.getTransaction = &GetTransactionResult{
node.getTransactionMap = map[string]*GetTransactionResult{"any": &GetTransactionResult{
BlockHash: blockHash.String(),
Hex: txB,
}
}}

_, confs, err := spv.getTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
if err != nil {
Expand All @@ -570,7 +606,7 @@ func TestGetTxOut(t *testing.T) {

// No wallet transaction, but we have a spend recorded.
node.getTransactionErr = WalletTransactionNotFound
node.getTransaction = nil
node.getTransactionMap = nil
node.checkpoints[outPt] = &scanCheckpoint{res: &filterScanResult{
blockHash: blockHash,
spend: &spendingInput{},
Expand Down Expand Up @@ -633,7 +669,7 @@ func TestSendWithSubtract(t *testing.T) {

const availableFunds = 5e8
const feeRate = 100
const inputSize = dexbtc.RedeemP2WPKHInputSize + ((dexbtc.RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4)
const inputSize = dexbtc.RedeemP2WPKHInputTotalSize
const feesWithChange = (dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize + inputSize) * feeRate
const feesWithoutChange = (dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize + inputSize) * feeRate

Expand Down
1 change: 1 addition & 0 deletions client/asset/btc/wallettypes.go
Expand Up @@ -63,6 +63,7 @@ type GetTransactionResult struct {
BlockHash string `json:"blockhash"`
BlockIndex int64 `json:"blockindex"`
BlockTime uint64 `json:"blocktime"`
BlockHeight uint64 `json:"blockheight"`
TxID string `json:"txid"`
Time uint64 `json:"time"`
TimeReceived uint64 `json:"timereceived"`
Expand Down
57 changes: 57 additions & 0 deletions client/asset/interface.go
Expand Up @@ -19,6 +19,7 @@ const (
WalletTraitNewAddresser // The Wallet can generate new addresses on demand with NewAddress.
WalletTraitLogFiler // The Wallet allows for downloading of a log file.
WalletTraitFeeRater // Wallet can provide a fee rate for non-critical transactions
WalletTraitAccelerator // This wallet can accelerate transactions using the CPFP technique
)

// IsRescanner tests if the WalletTrait has the WalletTraitRescanner bit set.
Expand All @@ -45,6 +46,12 @@ func (wt WalletTrait) IsFeeRater() bool {
return wt&WalletTraitFeeRater != 0
}

// IsAccelerator tests if the WalletTrait has the WalletTraitAccelerator bit set,
// which indicates the presence of an Accelerate method.
func (wt WalletTrait) IsAccelerator() bool {
return wt&WalletTraitAccelerator != 0
}

// DetermineWalletTraits returns the WalletTrait bitset for the provided Wallet.
func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(Rescanner); is {
Expand All @@ -59,6 +66,9 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(FeeRater); is {
t |= WalletTraitFeeRater
}
if _, is := w.(Accelerator); is {
t |= WalletTraitAccelerator
}
return t
}

Expand Down Expand Up @@ -360,6 +370,53 @@ type FeeRater interface {
FeeRate() uint64
}

// EarlyAcceleration is returned from the PreAccelerate function to inform the
// user that either their last acceleration or oldest swap transaction happened
// very recently, and that they should double check that they really want to do
// an acceleration.
type EarlyAcceleration struct {
// TimePast is the amount of seconds that has past since either the previous
// acceleration, or the oldest unmined swap transaction was submitted to
// the blockchain.
TimePast uint64 `json:"timePast"`
// WasAccelerated is true if the action that took place TimePast seconds
// ago was an acceleration. If false, the oldest unmined swap transaction
// in the order was submitted TimePast seconds ago.
WasAccelerated bool `json:"wasAccelerated"`
}

// Accelerator is implemented by wallets which support acceleration of the
// mining of swap transactions.
type Accelerator interface {
// AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a
// chain of swap transactions and previous accelerations. It broadcasts a new
// transaction with a fee high enough so that the average fee of all the
// unconfirmed transactions in the chain and the new transaction will have
// an average fee rate of newFeeRate. The changeCoin argument is the latest
// chhange in the order. It must be the input in the acceleration transaction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chhange -> change

// in order for the order to be accelerated. requiredForRemainingSwaps is the
// amount of funds required to complete the rest of the swaps in the order.
// The change output of the acceleration transaction will have at least
// this amount.
//
// The returned change coin may be nil, and should be checked before use.
AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (Coin, string, error)
// AccelerationEstimate takes the same parameters as AccelerateOrder, but
// instead of broadcasting the acceleration transaction, it just returns
// the amount of funds that will need to be spent in order to increase the
// average fee rate to the desired amount.
AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error)
// PreAccelerate returns the current average fee rate of the unmined swap
// initiation and acceleration transactions, and also returns a suggested
// range that the fee rate should be increased to in order to expedite mining.
// The feeSuggestion argument is the current prevailing network rate. It is
// used to help determine the suggestedRange, which is a range meant to give
// the user a good amount of flexibility in determining the post acceleration
// effective fee rate, but still not allowing them to pick something
// outrageously high.
PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *XYRange, *EarlyAcceleration, error)
}

// TokenMaster is implemented by assets which support degenerate tokens.
type TokenMaster interface {
// CreateTokenWallet creates a wallet for the specified token asset. The
Expand Down