Skip to content

Commit

Permalink
multi: Add FindBond to Bonder.
Browse files Browse the repository at this point in the history
When the server tells us of an unknown v0 bond attempt to find and
recreate it so that it can be refunded later. Useful when restoring
from seed.
  • Loading branch information
JoeGruffins committed Nov 27, 2023
1 parent 6afd126 commit 57e4e98
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 5 deletions.
131 changes: 131 additions & 0 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5015,6 +5015,137 @@ func (btc *baseWallet) RefundBond(ctx context.Context, ver uint16, coinID, scrip
return NewOutput(txHash, 0, uint64(msgTx.TxOut[0].Value)), nil
}

func (btc *baseWallet) decodeV0BondTx(msgTx *wire.MsgTx, txHash *chainhash.Hash, coinID []byte) (*asset.BondDetails, error) {
if len(msgTx.TxOut) < 2 {
return nil, fmt.Errorf("tx %s is not a v0 bond transaction: too few outputs", txHash)
}
acct, lockTime, pkh, err := dexbtc.ExtractBondCommitDataV0(0, msgTx.TxOut[1].PkScript)
if err != nil {
return nil, fmt.Errorf("unable to extract bond commitment details from output 1 of %s: %v", txHash, err)
}
// Sanity check.
bondScript, err := dexbtc.MakeBondScript(0, lockTime, pkh[:])
if err != nil {
return nil, fmt.Errorf("failed to build bond output redeem script: %w", err)
}
pkScript, err := btc.scriptHashScript(bondScript)
if err != nil {
return nil, fmt.Errorf("error constructing p2sh script: %v", err)
}
if !bytes.Equal(pkScript, msgTx.TxOut[0].PkScript) {
return nil, fmt.Errorf("bond script does not match commit data for %s: %x != %x",
txHash, bondScript, msgTx.TxOut[0].PkScript)
}
return &asset.BondDetails{
Bond: &asset.Bond{
Version: 0,
AssetID: BipID,
Amount: uint64(msgTx.TxOut[0].Value),
CoinID: coinID,
Data: bondScript,
//
// SignedTx and UnsignedTx not populated because this is
// an already posted bond and these fields are no longer used.
// SignedTx, UnsignedTx []byte
//
// RedeemTx cannot be populated because we do not have
// the private key that only core knows. Core will need
// the BondPKH to determine what the private key was.
// RedeemTx []byte
},
LockTime: time.Unix(int64(lockTime), 0),
AcctID: acct,
BondPKH: pkh[:],
PKHFn: func(bondKey *secp256k1.PrivateKey) []byte {
pk := bondKey.PubKey().SerializeCompressed()
return btcutil.Hash160(pk)
},
}, nil
}

// FindBond finds the bond with coinID and returns the values used to create it.
func (btc *baseWallet) FindBond(_ context.Context, coinID []byte) (bond *asset.BondDetails, err error) {
txHash, vout, err := decodeCoinID(coinID)
if err != nil {
return nil, err
}

// If the bond was funded by this wallet or had a change output paying
// to this wallet, it should be found here.
tx, err := btc.node.getWalletTransaction(txHash)
if err != nil {
return nil, fmt.Errorf("did not find the bond output %v:%d", txHash, vout)
}
msgTx, err := btc.deserializeTx(tx.Bytes)
if err != nil {
return nil, fmt.Errorf("invalid hex for tx %s: %v", txHash, err)
}
return btc.decodeV0BondTx(msgTx, txHash, coinID)
}

// FindBond finds the bond with coinID and returns the values used to create it.
// The intermediate wallet is able to brute force finding blocks.
func (btc *intermediaryWallet) FindBond(_ context.Context, coinID []byte) (bond *asset.BondDetails, err error) {
txHash, vout, err := decodeCoinID(coinID)
if err != nil {
return nil, err
}

// If the bond was funded by this wallet or had a change output paying
// to this wallet, it should be found here.
tx, err := btc.node.getWalletTransaction(txHash)
if err == nil {
msgTx, err := btc.deserializeTx(tx.Bytes)
if err != nil {
return nil, fmt.Errorf("invalid hex for tx %s: %v", txHash, err)
}
return btc.decodeV0BondTx(msgTx, txHash, coinID)
}
if !errors.Is(err, asset.CoinNotFoundError) {
btc.log.Warnf("Unexpected error looking up bond output %v:%d", txHash, vout)
}

// The bond was not funded by this wallet or had no change output when
// restored from seed. This is not a problem. However, we are unable to
// use filters because we don't know any output scripts. Brute force
// finding the transaction.
bestBlockHdr, err := btc.node.getBestBlockHeader()
if err != nil {
return nil, fmt.Errorf("unable to get best hash: %v", err)
}
blockHash, err := chainhash.NewHashFromStr(bestBlockHdr.Hash)
if err != nil {
return nil, fmt.Errorf("invalid best block hash from %s node: %v", btc.symbol, err)
}
var (
blk *wire.MsgBlock
msgTx *wire.MsgTx
searchUntil = time.Now().Add(-365 * 24 * time.Hour) // long!
)
out:
for {
blk, err = btc.tipRedeemer.getBlock(*blockHash)
if err != nil {
return nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err)
}
if blk.Header.Timestamp.Before(searchUntil) {
return nil, fmt.Errorf("searched blocks until %v but did not find the bond tx %s", searchUntil, txHash)
}
for _, tx := range blk.Transactions {
if tx.TxHash() == *txHash {
btc.log.Debugf("Found mined tx %s in block %s.", txHash, blk.BlockHash())
msgTx = tx
break out
}
}
blockHash = &blk.Header.PrevBlock
if blockHash == nil {
return nil, fmt.Errorf("did not find the bond output %v:%d", txHash, vout)
}
}
return btc.decodeV0BondTx(msgTx, txHash, coinID)
}

// BondsFeeBuffer suggests how much extra may be required for the transaction
// fees part of required bond reserves when bond rotation is enabled. The
// provided fee rate may be zero, in which case the wallet will use it's own
Expand Down
107 changes: 107 additions & 0 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4255,6 +4255,113 @@ func (dcr *ExchangeWallet) RefundBond(ctx context.Context, ver uint16, coinID, s
*/
}

// FindBond finds the bond with coinID and returns the values used to create it.
func (dcr *ExchangeWallet) FindBond(ctx context.Context, coinID []byte) (bond *asset.BondDetails, err error) {
txHash, vout, err := decodeCoinID(coinID)
if err != nil {
return nil, err
}

decodeV0BondTx := func(msgTx *wire.MsgTx) (*asset.BondDetails, error) {
if len(msgTx.TxOut) < 2 {
return nil, fmt.Errorf("tx %s is not a v0 bond transaction: too few outputs", txHash)
}
acct, lockTime, pkh, err := dexdcr.ExtractBondCommitDataV0(0, msgTx.TxOut[1].PkScript)
if err != nil {
return nil, fmt.Errorf("unable to extract bond commitment details from output 1 of %s: %v", txHash, err)
}
// Sanity check.
bondScript, err := dexdcr.MakeBondScript(0, lockTime, pkh[:])
if err != nil {
return nil, fmt.Errorf("failed to build bond output redeem script: %w", err)
}
bondAddr, err := stdaddr.NewAddressScriptHash(0, bondScript, dcr.chainParams)
if err != nil {
return nil, fmt.Errorf("failed to build bond output payment script: %w", err)
}
_, bondScriptWOpcodes := bondAddr.PaymentScript()
if !bytes.Equal(bondScriptWOpcodes, msgTx.TxOut[0].PkScript) {
return nil, fmt.Errorf("bond script does not match commit data for %s: %x != %x",
txHash, bondScript, msgTx.TxOut[0].PkScript)
}
return &asset.BondDetails{
Bond: &asset.Bond{
Version: 0,
AssetID: BipID,
Amount: uint64(msgTx.TxOut[0].Value),
CoinID: coinID,
Data: bondScript,
//
// SignedTx and UnsignedTx not populated because this is
// an already posted bond and these fields are no longer used.
// SignedTx, UnsignedTx []byte
//
// RedeemTx cannot be populated because we do not have
// the private key that only core knows. Core will need
// the BondPKH to determine what the private key was.
// RedeemTx []byte
},
LockTime: time.Unix(int64(lockTime), 0),
AcctID: acct,
BondPKH: pkh[:],
PKHFn: func(bondKey *secp256k1.PrivateKey) []byte {
pk := bondKey.PubKey().SerializeCompressed()
return stdaddr.Hash160(pk)
},
}, nil
}

// If the bond was funded by this wallet or had a change output paying
// to this wallet, it should be found here.
tx, err := dcr.wallet.GetTransaction(ctx, txHash)
if err == nil {
msgTx, err := msgTxFromHex(tx.Hex)
if err != nil {
return nil, fmt.Errorf("invalid hex for tx %s: %v", txHash, err)
}
return decodeV0BondTx(msgTx)
}
if !errors.Is(err, asset.CoinNotFoundError) {
dcr.log.Warnf("Unexpected error looking up bond output %v:%d", txHash, vout)
}

// The bond was not funded by this wallet or had no change output when
// restored from seed. This is not a problem. However, we are unable to
// use filters because we don't know any output scripts. Brute force
// finding the transaction.
blockHash, _, err := dcr.wallet.GetBestBlock(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get best hash: %v", err)
}
var (
blk *wire.MsgBlock
msgTx *wire.MsgTx
searchUntil = time.Now().Add(-365 * 24 * time.Hour) // long!
)
out:
for {
blk, err = dcr.wallet.GetBlock(ctx, blockHash)
if err != nil {
return nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err)
}
if blk.Header.Timestamp.Before(searchUntil) {
return nil, fmt.Errorf("searched blocks until %v but did not find the bond tx %s", searchUntil, txHash)
}
for _, tx := range blk.Transactions {
if tx.TxHash() == *txHash {
dcr.log.Debugf("Found mined tx %s in block %s.", txHash, blk.BlockHash())
msgTx = tx
break out
}
}
blockHash = &blk.Header.PrevBlock
if blockHash == nil {
return nil, fmt.Errorf("did not find the bond tx %s", txHash)
}
}
return decodeV0BondTx(msgTx)
}

// SendTransaction broadcasts a valid fully-signed transaction.
func (dcr *ExchangeWallet) SendTransaction(rawTx []byte) ([]byte, error) {
msgTx, err := msgTxFromBytes(rawTx)
Expand Down
15 changes: 15 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"decred.org/dcrdex/dex"
"decred.org/dcrdex/server/account"
dcrwalletjson "decred.org/dcrwallet/v3/rpc/jsonrpc/types"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
Expand Down Expand Up @@ -581,6 +582,15 @@ type Broadcaster interface {
SendTransaction(rawTx []byte) ([]byte, error)
}

// BondDetails is the return from Bonder.FindBond.
type BondDetails struct {
*Bond
LockTime time.Time
AcctID account.AccountID
BondPKH []byte
PKHFn func(priv *secp256k1.PrivateKey) []byte
}

// Bonder is a wallet capable of creating and redeeming time-locked fidelity
// bond transaction outputs.
type Bonder interface {
Expand Down Expand Up @@ -611,6 +621,11 @@ type Bonder interface {
// private key to spend it. The bond is broadcasted.
RefundBond(ctx context.Context, ver uint16, coinID, script []byte, amt uint64, privKey *secp256k1.PrivateKey) (Coin, error)

// FindBond finds the bond with coinID and returns the values used to
// create it. The output should be unspent with the lockTime set to
// some time in the future.
FindBond(ctx context.Context, coinID []byte) (bondDetails *BondDetails, err error)

// A RefundBondByCoinID may be created in the future to attempt to refund a
// bond by locating it on chain, i.e. without providing the amount or
// script, while also verifying the bond output is unspent. However, it's
Expand Down
4 changes: 2 additions & 2 deletions client/core/bond.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func (c *Core) refundExpiredBonds(ctx context.Context, acct *dexAccount, cfg *de
var refundCoinStr string
var refundVal uint64
var bondAlreadySpent bool
if bond.KeyIndex == math.MaxUint32 { // invalid/unknown key index fallback (v0 db.Bond, which was never released), also will skirt reserves :/
if bond.KeyIndex == math.MaxUint32 { // invalid/unknown key index fallback (v0 db.Bond, which was never released, or unknown bond from server), also will skirt reserves :/
if len(bond.RefundTx) > 0 {
refundCoinID, err := wallet.SendTransaction(bond.RefundTx)
if err != nil {
Expand All @@ -530,7 +530,7 @@ func (c *Core) refundExpiredBonds(ctx context.Context, acct *dexAccount, cfg *de
} else { // else "Unknown bond reported by server", see result.ActiveBonds in authDEX
bondAlreadySpent = true
}
} else { // expected case -- TODO: remove the math.MaxUint32 sometime after 0.6 release
} else { // expected case -- TODO: remove the math.MaxUint32 sometime after bonds V1
priv, err := c.bondKeyIdx(bond.AssetID, bond.KeyIndex)
if err != nil {
c.log.Errorf("Failed to derive bond private key: %v", err)
Expand Down
Loading

0 comments on commit 57e4e98

Please sign in to comment.