Skip to content

Commit

Permalink
multi: no trades while blockchain syncing
Browse files Browse the repository at this point in the history
Client waits for blockchain sync to complete.
Server waits to start markets receiving new orders, and warns if fee estimation isn't ready.
  • Loading branch information
buck54321 authored and chappjc committed Nov 3, 2020
1 parent d463439 commit 2cac73a
Show file tree
Hide file tree
Showing 28 changed files with 725 additions and 141 deletions.
39 changes: 39 additions & 0 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

"decred.org/dcrdex/client/asset"
Expand All @@ -39,6 +40,7 @@ const (
// rpcclient.Client's GetBlockVerboseTx appears to be busted.
methodGetBlockVerboseTx = "getblock"
methodGetNetworkInfo = "getnetworkinfo"
methodGetBlockchainInfo = "getblockchaininfo"
// BipID is the BIP-0044 asset ID.
BipID = 0

Expand Down Expand Up @@ -348,6 +350,7 @@ type ExchangeWallet struct {
log dex.Logger
symbol string
tipChange func(error)
tipAtConnect int64
minNetworkVersion uint64
fallbackFeeRate uint64
redeemConfTarget uint64
Expand Down Expand Up @@ -519,6 +522,7 @@ func (btc *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error)
if err != nil {
return nil, fmt.Errorf("error initializing best block for %s: %v", btc.symbol, err)
}
atomic.StoreInt64(&btc.tipAtConnect, btc.currentTip.height)
var wg sync.WaitGroup
wg.Add(1)
go func() {
Expand All @@ -541,6 +545,41 @@ func (btc *ExchangeWallet) shutdown() {
btc.findRedemptionMtx.Unlock()
}

// getBlockchainInfoResult models the data returned from the getblockchaininfo
// command.
type getBlockchainInfoResult struct {
Blocks int64 `json:"blocks"`
Headers int64 `json:"headers"`
BestBlockHash string `json:"bestblockhash"`
InitialBlockDownload bool `json:"initialblockdownload"`
}

// getBlockchainInfo sends the getblockchaininfo request and returns the result.
func (btc *ExchangeWallet) getBlockchainInfo() (*getBlockchainInfoResult, error) {
chainInfo := new(getBlockchainInfoResult)
err := btc.wallet.call(methodGetBlockchainInfo, nil, chainInfo)
if err != nil {
return nil, err
}
return chainInfo, nil
}

// SyncStatus is information about the blockchain sync status.
func (btc *ExchangeWallet) SyncStatus() (bool, float32, error) {
chainInfo, err := btc.getBlockchainInfo()
if err != nil {
return false, 0, fmt.Errorf("getblockchaininfo error: %w", err)
}
toGo := chainInfo.Headers - chainInfo.Blocks
if chainInfo.InitialBlockDownload || toGo > 1 {
ogTip := atomic.LoadInt64(&btc.tipAtConnect)
totalToSync := chainInfo.Headers - ogTip
progress := 1 - (float32(toGo) / float32(totalToSync))
return false, progress, nil
}
return true, 1, nil
}

// Balance returns the total available funds in the wallet. Part of the
// asset.Wallet interface.
func (btc *ExchangeWallet) Balance() (*asset.Balance, error) {
Expand Down
43 changes: 43 additions & 0 deletions client/asset/btc/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2077,3 +2077,46 @@ func testSendEdges(t *testing.T, segwit bool) {
}
}
}

func TestSyncStatus(t *testing.T) {
wallet, node, shutdown := tNewWallet(false)
defer shutdown()
node.rawRes[methodGetBlockchainInfo] = mustMarshal(t, &getBlockchainInfoResult{
Headers: 100,
Blocks: 99,
})

synced, progress, err := wallet.SyncStatus()
if err != nil {
t.Fatalf("SyncStatus error (synced expected): %v", err)
}
if !synced {
t.Fatalf("synced = false for 1 block to go")
}
if progress < 1 {
t.Fatalf("progress not complete when loading last block")
}

node.rawErr[methodGetBlockchainInfo] = tErr
_, _, err = wallet.SyncStatus()
if err == nil {
t.Fatalf("SyncStatus error not propagated")
}
node.rawErr[methodGetBlockchainInfo] = nil

wallet.tipAtConnect = 100
node.rawRes[methodGetBlockchainInfo] = mustMarshal(t, &getBlockchainInfoResult{
Headers: 200,
Blocks: 150,
})
synced, progress, err = wallet.SyncStatus()
if err != nil {
t.Fatalf("SyncStatus error (half-synced): %v", err)
}
if synced {
t.Fatalf("synced = true for 50 blocks to go")
}
if progress > 0.500001 || progress < 0.4999999 {
t.Fatalf("progress out of range. Expected 0.5, got %.2f", progress)
}
}
20 changes: 20 additions & 0 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

"decred.org/dcrdex/client/asset"
Expand Down Expand Up @@ -131,6 +132,7 @@ var (
// rpcClient is an rpcclient.Client, or a stub for testing.
type rpcClient interface {
EstimateSmartFee(confirmations int64, mode chainjson.EstimateSmartFeeMode) (float64, error)
GetBlockChainInfo() (*chainjson.GetBlockChainInfoResult, error)
SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error)
GetTxOut(txHash *chainhash.Hash, index uint32, mempool bool) (*chainjson.GetTxOutResult, error)
GetBalanceMinConf(account string, minConfirms int) (*walletjson.GetBalanceResult, error)
Expand Down Expand Up @@ -352,6 +354,7 @@ type ExchangeWallet struct {
log dex.Logger
acct string
tipChange func(error)
tipAtConnect int64
fallbackFeeRate uint64
redeemConfTarget uint64
useSplitTx bool
Expand Down Expand Up @@ -521,6 +524,7 @@ func (dcr *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error)
if err != nil {
return nil, fmt.Errorf("error initializing best block for DCR: %v", err)
}
atomic.StoreInt64(&dcr.tipAtConnect, dcr.currentTip.height)

dcr.log.Infof("Connected to dcrwallet (JSON-RPC API v%s) proxying dcrd (JSON-RPC API v%s) on %v",
walletSemver, nodeSemver, curnet)
Expand Down Expand Up @@ -1847,6 +1851,22 @@ func (dcr *ExchangeWallet) shutdown() {
}
}

// SyncStatus is information about the blockchain sync status.
func (dcr *ExchangeWallet) SyncStatus() (bool, float32, error) {
chainInfo, err := dcr.node.GetBlockChainInfo()
if err != nil {
return false, 0, fmt.Errorf("getblockchaininfo error: %w", err)
}
toGo := chainInfo.Headers - chainInfo.Blocks
if chainInfo.InitialBlockDownload || toGo > 1 {
ogTip := atomic.LoadInt64(&dcr.tipAtConnect)
totalToSync := chainInfo.Headers - ogTip
progress := 1 - (float32(toGo) / float32(totalToSync))
return false, progress, nil
}
return true, 1, nil
}

// Combines the RPC type with the spending input information.
type compositeUTXO struct {
rpc walletjson.ListUnspentResult
Expand Down
119 changes: 84 additions & 35 deletions client/asset/dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,41 +173,43 @@ func signFunc(msgTx *wire.MsgTx, scriptSize int) (*wire.MsgTx, bool, error) {
}

type tRPCClient struct {
sendRawHash *chainhash.Hash
sendRawErr error
sentRawTx *wire.MsgTx
txOutRes map[outPoint]*chainjson.GetTxOutResult
txOutErr error
bestBlockErr error
mempool []*chainhash.Hash
mempoolErr error
rawTx *chainjson.TxRawResult
rawTxErr error
unspent []walletjson.ListUnspentResult
unspentErr error
balanceResult *walletjson.GetBalanceResult
balanceErr error
lockUnspentErr error
changeAddr dcrutil.Address
changeAddrErr error
newAddr dcrutil.Address
newAddrErr error
signFunc func(tx *wire.MsgTx) (*wire.MsgTx, bool, error)
privWIF *dcrutil.WIF
privWIFErr error
walletTx *walletjson.GetTransactionResult
walletTxErr error
lockErr error
passErr error
disconnected bool
rawRes map[string]json.RawMessage
rawErr map[string]error
blockchainMtx sync.RWMutex
verboseBlocks map[string]*chainjson.GetBlockVerboseResult
mainchain map[int64]*chainhash.Hash
lluCoins []walletjson.ListUnspentResult // Returned from ListLockUnspent
lockedCoins []*wire.OutPoint // Last submitted to LockUnspent
listLockedErr error
sendRawHash *chainhash.Hash
sendRawErr error
sentRawTx *wire.MsgTx
txOutRes map[outPoint]*chainjson.GetTxOutResult
txOutErr error
bestBlockErr error
mempool []*chainhash.Hash
mempoolErr error
rawTx *chainjson.TxRawResult
rawTxErr error
unspent []walletjson.ListUnspentResult
unspentErr error
balanceResult *walletjson.GetBalanceResult
balanceErr error
lockUnspentErr error
changeAddr dcrutil.Address
changeAddrErr error
newAddr dcrutil.Address
newAddrErr error
signFunc func(tx *wire.MsgTx) (*wire.MsgTx, bool, error)
privWIF *dcrutil.WIF
privWIFErr error
walletTx *walletjson.GetTransactionResult
walletTxErr error
lockErr error
passErr error
disconnected bool
rawRes map[string]json.RawMessage
rawErr map[string]error
blockchainMtx sync.RWMutex
verboseBlocks map[string]*chainjson.GetBlockVerboseResult
mainchain map[int64]*chainhash.Hash
lluCoins []walletjson.ListUnspentResult // Returned from ListLockUnspent
lockedCoins []*wire.OutPoint // Last submitted to LockUnspent
listLockedErr error
blockchainInfo *chainjson.GetBlockChainInfoResult
blockchainInfoErr error
}

func defaultSignFunc(tx *wire.MsgTx) (*wire.MsgTx, bool, error) { return tx, true, nil }
Expand Down Expand Up @@ -236,6 +238,10 @@ func (c *tRPCClient) EstimateSmartFee(confirmations int64, mode chainjson.Estima
return optimalRate, nil // optimalFeeRate: 22 atoms/byte = 0.00022 DCR/KB * 1e8 atoms/DCR * 1e-3 KB/Byte
}

func (c *tRPCClient) GetBlockChainInfo() (*chainjson.GetBlockChainInfoResult, error) {
return c.blockchainInfo, c.blockchainInfoErr
}

func (c *tRPCClient) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) {
c.sentRawTx = tx
if c.sendRawErr == nil && c.sendRawHash == nil {
Expand Down Expand Up @@ -1828,3 +1834,46 @@ func TestSendEdges(t *testing.T) {
}
}
}

func TestSyncStatus(t *testing.T) {
wallet, node, shutdown := tNewWallet()
defer shutdown()
node.blockchainInfo = &chainjson.GetBlockChainInfoResult{
Headers: 100,
Blocks: 99,
}

synced, progress, err := wallet.SyncStatus()
if err != nil {
t.Fatalf("SyncStatus error (synced expected): %v", err)
}
if !synced {
t.Fatalf("synced = false for 1 block to go")
}
if progress < 1 {
t.Fatalf("progress not complete when loading last block")
}

node.blockchainInfoErr = tErr
_, _, err = wallet.SyncStatus()
if err == nil {
t.Fatalf("SyncStatus error not propagated")
}
node.blockchainInfoErr = nil

wallet.tipAtConnect = 100
node.blockchainInfo = &chainjson.GetBlockChainInfoResult{
Headers: 200,
Blocks: 150,
}
synced, progress, err = wallet.SyncStatus()
if err != nil {
t.Fatalf("SyncStatus error (half-synced): %v", err)
}
if synced {
t.Fatalf("synced = true for 50 blocks to go")
}
if progress > 0.500001 || progress < 0.4999999 {
t.Fatalf("progress out of range. Expected 0.5, got %.2f", progress)
}
}
2 changes: 2 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ type Wallet interface {
Withdraw(address string, value uint64) (Coin, error)
// ValidateSecret checks that the secret hashes to the secret hash.
ValidateSecret(secret, secretHash []byte) bool
// SyncStatus is information about the blockchain sync status.
SyncStatus() (synced bool, progress float32, err error)
}

// Balance is categorized information about a wallet's balance.
Expand Down
Loading

0 comments on commit 2cac73a

Please sign in to comment.