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: bitcoin spv #1230

Merged
merged 22 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ var (
spvWalletDefinition,
rpcWalletDefinition,
},
LegacyWalletIndex: 1,
}
)

Expand Down
58 changes: 43 additions & 15 deletions client/asset/btc/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ func logNeutrino(netDir string) error {
rotatorMtx.Lock()
defer rotatorMtx.Unlock()
if loggerCount > 0 {
loggerCount++
return nil
Copy link
Member

Choose a reason for hiding this comment

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

I think you still want to increment here.

if loggerCount > 0 {
	loggerCount++
	return nil
}

seems like the only place not to increment is with a non-nil return setting up the logger.

Copy link
Member

@chappjc chappjc Oct 21, 2021

Choose a reason for hiding this comment

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

I confirmed that the remaining log races go away with this, but then realized that in createSPVWallet (the other place where logNeutrino is called), we need a defer unlogNeutrino() too otherwise it never decrements to zero to close the log rotator.

Sadly, this reveals a problem: https://github.com/lightninglabs/neutrino/blob/51686db787e01a3709b1583af3e20e916811d585/neutrino.go#L1136-L1137

The time drain on this logger is unfortunate, and it's looking like only ever calling UseLogger once (somehow), even if we are synchronizing the best we can, is the only actual solution. I'm not even sure a dumb Sleep would do as a workaround, but perhaps it's worth doing that just to save ourselves further wasted time.

}

Expand Down Expand Up @@ -477,10 +478,19 @@ func (w *spvWallet) syncStatus() (*syncStatus, error) {
return nil, err
}

target := w.syncHeight()
currentHeight := blk.Height
synced := w.wallet.ChainSynced()
// Sometimes the wallet doesn't report the chain as synced right away.
// Seems to be a bug.
if !synced && target > 0 && target == currentHeight {
synced = true
}

return &syncStatus{
Target: w.syncHeight(),
Height: blk.Height,
Syncing: !w.wallet.ChainSynced(),
Target: target,
Height: currentHeight,
Syncing: !synced,
}, nil
}

Expand Down Expand Up @@ -677,7 +687,8 @@ func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtrac
}

func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*chainhash.Hash, error) {
const unfundedTxSize = dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize
txOutSize := dexbtc.TxOutOverhead + uint64(len(pkScript)) // send-to address
var unfundedTxSize uint64 = dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize /* change */ + txOutSize

unspents, err := w.listUnspent()
if err != nil {
Expand All @@ -689,7 +700,9 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c
return nil, fmt.Errorf("error converting unspent outputs: %w", err)
}

enough := func(inputsSize, inputsVal uint64) bool {
// With sendWithSubtract, fees are subtracted from the sent amount, so we
// target an input sum, not an output value. Makes the math easy.
enough := func(_, inputsVal uint64) bool {
return inputsVal >= value
}

Expand All @@ -699,8 +712,16 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c
}

fees := (unfundedTxSize + uint64(inputsSize)) * feeRate
if fees > sum {
send := value - fees
extra := sum - send

switch {
case fees > sum:
return nil, fmt.Errorf("fees > sum")
case fees > value:
return nil, fmt.Errorf("fees > value")
case send > sum:
return nil, fmt.Errorf("send > sum")
}

tx := wire.NewMsgTx(wire.TxVersion)
Expand All @@ -710,9 +731,6 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c
tx.AddTxIn(txIn)
}

send := value - fees
extra := sum - send

change := extra - fees
changeAddr, err := w.changeAddress()
if err != nil {
Expand Down Expand Up @@ -918,6 +936,11 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error {
// *Rescan supplied with a QuitChan-type RescanOption.
// Actually, should use btcwallet.Wallet.NtfnServer ?

// notes := make(<-chan interface{})
// if w.chainClient != nil {
// notes = w.chainClient.Notifications()
// }

// Nanny for the caches checkpoints and txBlocks caches.
wg.Add(1)
go func() {
Expand All @@ -926,6 +949,7 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error {
defer w.stop()

ticker := time.NewTicker(time.Minute * 20)
defer ticker.Stop()
expiration := time.Hour * 2
for {
select {
Expand All @@ -945,6 +969,8 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error {
}
}
w.checkpointMtx.Unlock()
// case note := <-notes:
// fmt.Printf("--Notification received: %T: %+v \n", note, note)
case <-ctx.Done():
return
}
Expand Down Expand Up @@ -1290,14 +1316,10 @@ func (w *spvWallet) getTxOut(txHash *chainhash.Hash, vout uint32, pkScript []byt
return nil, 0, err
}

if utxo == nil || utxo.spend != nil {
if utxo == nil || utxo.spend != nil || utxo.blockHash == nil {
return nil, 0, nil
}

if utxo.blockHash == nil {
return nil, 0, fmt.Errorf("output %s:%v not found", txHash, vout)
}

tip, err := w.cl.BestBlock()
if err != nil {
return nil, 0, fmt.Errorf("BestBlock error: %v", err)
Expand Down Expand Up @@ -1335,11 +1357,12 @@ search:
if err != nil {
return nil, fmt.Errorf("error getting block hash for height %d: %w", height, err)
}
res.checkpoint = *blockHash
matched, err := w.matchPkScript(blockHash, [][]byte{pkScript})
if err != nil {
return nil, fmt.Errorf("matchPkScript error: %w", err)
}

res.checkpoint = *blockHash
if !matched {
continue search
}
Expand Down Expand Up @@ -1401,6 +1424,11 @@ func (w *spvWallet) matchPkScript(blockHash *chainhash.Hash, scripts [][]byte) (
if err != nil {
return false, fmt.Errorf("GetCFilter error: %w", err)
}

if filter.N() == 0 {
return false, fmt.Errorf("unexpected empty filter for %s", blockHash)
}

var filterKey [gcs.KeySize]byte
copy(filterKey[:], blockHash[:gcs.KeySize])

Expand Down
5 changes: 4 additions & 1 deletion client/asset/btc/spv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
dexbtc "decred.org/dcrdex/dex/networks/btc"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/btcjson"
Expand Down Expand Up @@ -319,7 +320,9 @@ func (c *tNeutrinoClient) GetBlockHeader(blkHash *chainhash.Hash) (*wire.BlockHe
func (c *tNeutrinoClient) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) {
var key [gcs.KeySize]byte
copy(key[:], blockHash.CloneBytes()[:])
return gcs.BuildGCSFilter(builder.DefaultP, builder.DefaultM, key, c.getCFilterScripts[blockHash])
scripts := c.getCFilterScripts[blockHash]
scripts = append(scripts, encode.RandomBytes(10))
return gcs.BuildGCSFilter(builder.DefaultP, builder.DefaultM, key, scripts)
}

func (c *tNeutrinoClient) GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) {
Expand Down
4 changes: 4 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ type WalletInfo struct {
// be the initial form offered to the user for configuration, with others
// available to select.
AvailableWallets []*WalletDefinition `json:"availablewallets"`
// LegacyWalletIndex should be set for assets that existed before wallets
// were typed. The index should point to the WalletDefinition that should
// be assumed when the type is provided as an empty string.
LegacyWalletIndex int `json:"emptyidx"`
// UnitInfo is the information about unit names and conversion factors for
// the asset.
UnitInfo dex.UnitInfo `json:"unitinfo"`
Expand Down
86 changes: 67 additions & 19 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -1797,27 +1797,15 @@ func (c *Core) CreateWallet(appPW, walletPW []byte, form *WalletForm) error {
// derived deterministically from the app seed and a random password. The
// password is returned for encrypting and storing.
func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form *WalletForm) ([]byte, error) {
creds := c.creds()
if creds == nil {
return nil, errors.New("no v2 credentials stored")
}

appSeed, err := crypter.Decrypt(creds.EncSeed)
seed, pw, err := c.assetSeedAndPass(assetID, crypter)
if err != nil {
return nil, fmt.Errorf("app seed decryption error: %w", err)
return nil, err
}

c.log.Infof("Initializing a built-in %s wallet", unbip(assetID))

b := make([]byte, len(appSeed)+4)
copy(b, appSeed)
binary.BigEndian.PutUint32(b[len(appSeed):], assetID)

seed := blake256.Sum256(b)
pw := encode.RandomBytes(32)
if err = asset.CreateWallet(assetID, &asset.CreateWalletParams{
Type: form.Type,
Seed: seed[:],
Seed: seed,
Pass: pw,
Settings: form.Config,
DataDir: c.assetDataDirectory(assetID),
Expand All @@ -1830,6 +1818,26 @@ func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form
return pw, nil
}

func (c *Core) assetSeedAndPass(assetID uint32, crypter encrypt.Crypter) (seed, pass []byte, err error) {
creds := c.creds()
if creds == nil {
return nil, nil, errors.New("no v2 credentials stored")
}

appSeed, err := crypter.Decrypt(creds.EncSeed)
if err != nil {
return nil, nil, fmt.Errorf("app seed decryption error: %w", err)
}

b := make([]byte, len(appSeed)+4)
copy(b, appSeed)
binary.BigEndian.PutUint32(b[len(appSeed):], assetID)

s := blake256.Sum256(b)
p := blake256.Sum256(seed)
return s[:], p[:], nil
}

// assetDataDirectory is a directory for a wallet to use for local storage.
func (c *Core) assetDataDirectory(assetID uint32) string {
return filepath.Join(filepath.Dir(c.cfg.DBPath), "assetdb", unbip(assetID))
Expand Down Expand Up @@ -2026,7 +2034,7 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er
assetID, unbip(assetID))
}
seeded := oldWallet.Info().Seeded
if seeded && len(newWalletPW) > 0 {
if seeded && newWalletPW != nil {
return newError(passwordErr, "cannot set a password on a built-in wallet")
}
oldDepositAddr := oldWallet.currentDepositAddress()
Expand All @@ -2039,10 +2047,23 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er
Address: oldDepositAddr,
}

fmt.Printf("--ReconfigureWallet.0 %+v \n", walletDef)
var restartOnFail bool

defer func() {
if restartOnFail {
if _, err := c.connectWallet(oldWallet); err != nil {
c.log.Errorf("Failed to reconnect wallet after a failed reconfiguration attempt: %v", err)
}
}
}()

oldDef, err := walletDefinition(assetID, oldWallet.walletType)
if err != nil {
return fmt.Errorf("failed to locate old wallet definition: %v", err)
}

if walletDef.Seeded {
if len(newWalletPW) > 0 {
if newWalletPW != nil {
return newError(passwordErr, "cannot set a password on a seeded wallet")
}

Expand All @@ -2056,8 +2077,27 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er
if err != nil {
return newError(createWalletErr, "error creating new %q-type %s wallet: %v", form.Type, unbip(assetID), err)
}
// return newError(existenceCheckErr, "cannot reconfigure wallet that doesn't exist")
} else if oldDef.Seeded {
_, newWalletPW, err = c.assetSeedAndPass(assetID, crypter)
if err != nil {
return newError(authErr, "error retrieving wallet password: %v", err)
}
}

if oldWallet.connected() {
oldDef, err := walletDefinition(assetID, oldWallet.walletType)
// Error can be normal if the wallet was created before wallet types
// were a thing. Just assume this is an old wallet and therefore not
// seeded.
if err == nil && oldDef.Seeded {
Comment on lines +2088 to +2092
Copy link
Member

@chappjc chappjc Oct 21, 2021

Choose a reason for hiding this comment

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

You already have oldDef from 30 lines up before if walletDef.Seeded.
It also looks like you solved the problem in the comment with LegacyWalletIndex

Copy link
Member

@chappjc chappjc Oct 21, 2021

Choose a reason for hiding this comment

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

Should be good without the walletDefinition call and using the oldDef you have like:

if oldDef.Seeded && oldWallet.connected() {
	oldWallet.Disconnect()
	restartOnFail = true
}

Copy link
Member

Choose a reason for hiding this comment

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

NVM, I have some updates that will follow. Going to merge this shortly.

oldWallet.Disconnect()
restartOnFail = true
}
}
} else if newWalletPW == nil && oldDef.Seeded {
// If we're switching from a seeded wallet and no password was provided,
// use empty string = wallet not encrypted.
newWalletPW = []byte{}
}

// Reload the wallet with the new settings.
Expand Down Expand Up @@ -2159,6 +2199,8 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er
c.wallets[assetID] = wallet
c.walletMtx.Unlock()

restartOnFail = false

if oldWallet.connected() {
// NOTE: Cannot lock the wallet backend because it may be the same as
// the one just connected.
Expand Down Expand Up @@ -6235,6 +6277,12 @@ func walletDefinition(assetID uint32, walletType string) (*asset.WalletDefinitio
if err != nil {
return nil, newError(assetSupportErr, "asset.Info error: %v", err)
}
if walletType == "" {
if len(winfo.AvailableWallets) <= winfo.LegacyWalletIndex {
return nil, fmt.Errorf("legacy wallet index out of range")
}
return winfo.AvailableWallets[winfo.LegacyWalletIndex], nil
}
var walletDef *asset.WalletDefinition
for _, def := range winfo.AvailableWallets {
if def.Type == walletType {
Expand Down
6 changes: 4 additions & 2 deletions client/core/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -2029,7 +2029,7 @@ func (t *trackedTrade) processAuditMsg(msgID uint64, audit *msgjson.Audit) error
err := t.auditContract(match, audit.CoinID, audit.Contract, audit.TxData)
if err != nil {
contractID := coinIDString(t.wallets.toAsset.ID, audit.CoinID)
t.dc.log.Error("Failed to audit contract coin %v (%s) for match %s: %v",
t.dc.log.Errorf("Failed to audit contract coin %v (%s) for match %s: %v",
contractID, t.wallets.toAsset.Symbol, match, err)
// Don't revoke in case server sends a revised audit request before
// the match is revoked.
Expand Down Expand Up @@ -2075,6 +2075,7 @@ func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contr
var auditInfo *asset.AuditInfo
var tries int
contractID, contractSymb := coinIDString(t.wallets.toAsset.ID, coinID), t.wallets.toAsset.Symbol
tLastWarning := time.Now()
t.latencyQ.Wait(&wait.Waiter{
Expiration: time.Now().Add(24 * time.Hour), // effectively forever
TryFunc: func() bool {
Expand All @@ -2093,7 +2094,8 @@ func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contr
"Check your internet and wallet connections!", contractID, contractSymb))
return wait.DontTryAgain
}
if tries > 0 && tries%12 == 0 {
if time.Since(tLastWarning) > 30*time.Minute {
tLastWarning = time.Now()
subject, detail := t.formatDetails(TopicAuditTrouble, contractID, contractSymb, match)
t.notify(newOrderNote(TopicAuditTrouble, subject, detail, db.WarningLevel, t.coreOrder()))
}
Expand Down
14 changes: 7 additions & 7 deletions client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,7 @@ func (c *TCore) walletState(assetID uint32) *core.WalletState {
Balance: c.balances[assetID],
Units: winfos[assetID].UnitInfo.AtomicUnit,
Encrypted: true,
Synced: true,
}
}

Expand Down Expand Up @@ -1224,13 +1225,12 @@ func (c *TCore) SupportedAssets() map[uint32]*core.SupportedAsset {
c.mtx.RLock()
defer c.mtx.RUnlock()
return map[uint32]*core.SupportedAsset{
0: mkSupportedAsset("btc", c.wallets[0], c.balances[0]),
42: mkSupportedAsset("dcr", c.wallets[42], c.balances[42]),
2: mkSupportedAsset("ltc", c.wallets[2], c.balances[2]),
22: mkSupportedAsset("mona", c.wallets[22], c.balances[22]),
3: mkSupportedAsset("doge", c.wallets[3], c.balances[3]),
28: mkSupportedAsset("vtc", c.wallets[28], c.balances[28]),
141: mkSupportedAsset("kmd", c.wallets[141], c.balances[141]),
0: mkSupportedAsset("btc", c.wallets[0], c.balances[0]),
42: mkSupportedAsset("dcr", c.wallets[42], c.balances[42]),
2: mkSupportedAsset("ltc", c.wallets[2], c.balances[2]),
22: mkSupportedAsset("mona", c.wallets[22], c.balances[22]),
3: mkSupportedAsset("doge", c.wallets[3], c.balances[3]),
28: mkSupportedAsset("vtc", c.wallets[28], c.balances[28]),
}
}

Expand Down
2 changes: 1 addition & 1 deletion client/webserver/site/src/html/wallets.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
</form>

{{- /* RECONFIGURE WALLET */ -}}
<form class="card bg1 border1 pb-3 d-hide mt-3" id="walletReconfig" autocomplete="off">
<form class="card bg1 border1 pb-3 d-hide mt-3" id="reconfigForm" autocomplete="off">
<div class="bg2 px-2 py-1 text-center position-relative fs18">
[[[Reconfigure]]]
<img id="recfgAssetLogo" class="micro-icon mx-1">
Expand Down
Loading