Skip to content
This repository was archived by the owner on Mar 2, 2023. It is now read-only.

Commit f7976dc

Browse files
Fix lookahead bug
The accounter should only schedule additional addresses after checking each transaction's height.
1 parent e96d581 commit f7976dc

6 files changed

Lines changed: 66 additions & 87 deletions

File tree

accounter/accounter.go

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package accounter
22

33
import (
44
"encoding/hex"
5-
"fmt"
65
"log"
76
"sync"
87
"time"
@@ -28,8 +27,10 @@ type Accounter struct {
2827
xpubs []string
2928
blockHeight uint32 // height at which we want to compute the balance
3029

31-
addresses map[string]address // map of address script => (Address, txHashes)
32-
transactions map[string]transaction // map of txhash => transaction
30+
addresses map[string]address // map of address script => (Address, txHashes)
31+
txAddressesMu sync.Mutex
32+
txAddresses map[string][]*deriver.Address // map of txhash => []Address
33+
transactions map[string]transaction // map of txhash => transaction
3334

3435
backend backend.Backend
3536
deriver *deriver.AddressDeriver
@@ -71,20 +72,19 @@ type vout struct {
7172
}
7273

7374
// New instantiates a new Accounter.
74-
// TODO: find a better way to pass options to the NewCounter. Maybe thru a config or functional option params?
7575
func New(b backend.Backend, addressDeriver *deriver.AddressDeriver, lookahead uint32, blockHeight uint32) *Accounter {
76-
a := &Accounter{
76+
return &Accounter{
7777
blockHeight: blockHeight,
7878
backend: b,
7979
deriver: addressDeriver,
8080
lookahead: lookahead,
8181
lastAddresses: [2]uint32{lookahead, lookahead},
82+
addresses: make(map[string]address),
83+
txAddresses: make(map[string][]*deriver.Address),
84+
transactions: make(map[string]transaction),
85+
addrResponses: b.AddrResponses(),
86+
txResponses: b.TxResponses(),
8287
}
83-
a.addresses = make(map[string]address)
84-
a.transactions = make(map[string]transaction)
85-
a.addrResponses = b.AddrResponses()
86-
a.txResponses = b.TxResponses()
87-
return a
8888
}
8989

9090
func (a *Accounter) ComputeBalance() uint64 {
@@ -112,33 +112,17 @@ func (a *Accounter) fetchTransactions() {
112112
}
113113

114114
func (a *Accounter) processTransactions() {
115-
for hash, tx := range a.transactions {
116-
// remove transactions which are too recent
117-
if tx.height > int64(a.blockHeight) {
118-
reporter.GetInstance().Logf("transaction %s has height %d > BLOCK HEIGHT (%d)", hash, tx.height, a.blockHeight)
119-
delete(a.transactions, hash)
120-
}
121-
// remove transactions which haven't been mined
122-
if tx.height <= 0 {
123-
reporter.GetInstance().Logf("transaction %s has not been mined, yet (height=%d)", hash, tx.height)
124-
delete(a.transactions, hash)
125-
}
126-
}
127-
reporter.GetInstance().SetTxAfterFilter(int32(len(a.transactions)))
128-
reporter.GetInstance().Log("done filtering")
129-
130115
// TODO: we could check that scheduled == fetched in the metrics we track in reporter.
131116

132117
// parse the transaction hex
133118
for hash, tx := range a.transactions {
134119
b, err := hex.DecodeString(tx.hex)
135120
if err != nil {
136-
fmt.Printf("failed to unhex transaction %s: %s", hash, tx.hex)
121+
log.Panicf("failed to unhex transaction %s: %s", hash, tx.hex)
137122
}
138123
parsedTx, err := btcutil.NewTxFromBytes(b)
139124
if err != nil {
140-
fmt.Printf("failed to parse transaction %s: %s", hash, tx.hex)
141-
continue
125+
log.Panicf("failed to parse transaction %s: %s", hash, tx.hex)
142126
}
143127
for _, txin := range parsedTx.MsgTx().TxIn {
144128
tx.vin = append(tx.vin, vin{
@@ -234,7 +218,10 @@ func (a *Accounter) sendWork() {
234218
indexes[change]++
235219
}
236220
}
237-
// apparently no more work for us, so we can sleep a bit
221+
// apparently no more work for now.
222+
223+
// TODO: we should either merge sendWork/recvWork or use some kind of mutex to sleep exactly
224+
// until there's more work that needs to be done. For now, a simple sleep works.
238225
time.Sleep(time.Millisecond * 100)
239226
}
240227
}
@@ -251,6 +238,7 @@ func (a *Accounter) recvWork() {
251238
continue
252239
}
253240
reporter.GetInstance().IncAddressesFetched()
241+
reporter.GetInstance().Logf("received address: %s", resp.Address)
254242

255243
a.countMu.Lock()
256244
a.processedAddrCount++
@@ -270,13 +258,15 @@ func (a *Accounter) recvWork() {
270258
}
271259
a.countMu.Unlock()
272260

261+
// we can only update the lastAddresses after we filter the transaction heights
262+
a.txAddressesMu.Lock()
263+
for _, txHash := range resp.TxHashes {
264+
a.txAddresses[txHash] = append(a.txAddresses[txHash], resp.Address)
265+
}
266+
a.txAddressesMu.Unlock()
267+
273268
reporter.GetInstance().Logf("address %s has %d transactions", resp.Address, len(resp.TxHashes))
274269

275-
if resp.HasTransactions() {
276-
a.countMu.Lock()
277-
a.lastAddresses[resp.Address.Change()] = Max(a.lastAddresses[resp.Address.Change()], resp.Address.Index()+a.lookahead)
278-
a.countMu.Unlock()
279-
}
280270
case resp, ok := <-txResponses:
281271
// channel is closed now, so ignore this case by blocking forever
282272
if !ok {
@@ -285,18 +275,38 @@ func (a *Accounter) recvWork() {
285275
}
286276

287277
reporter.GetInstance().IncTxFetched()
278+
reporter.GetInstance().Logf("received tx: %s", resp.Hash)
288279

289280
a.countMu.Lock()
290281
a.processedTxCount++
291282
a.countMu.Unlock()
292283

284+
if resp.Height > int64(a.blockHeight) {
285+
continue
286+
}
287+
if resp.Height == 0 {
288+
continue
289+
}
290+
if resp.Height < 0 {
291+
log.Panicf("tx %s has negative height %d", resp.Hash, resp.Height)
292+
}
293+
293294
tx := transaction{
294295
height: resp.Height,
295296
hex: resp.Hex,
296297
vin: []vin{},
297298
vout: []vout{},
298299
}
299300
a.transactions[resp.Hash] = tx
301+
302+
a.txAddressesMu.Lock()
303+
a.countMu.Lock()
304+
for _, addr := range a.txAddresses[resp.Hash] {
305+
a.lastAddresses[addr.Change()] = Max(a.lastAddresses[addr.Change()], addr.Index()+a.lookahead)
306+
}
307+
a.countMu.Unlock()
308+
a.txAddressesMu.Unlock()
309+
300310
case <-time.Tick(1 * time.Second):
301311
if a.complete() {
302312
return

backend/electrum/blockchain.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,9 @@ func (n *Node) BlockchainAddressGetHistory(address string) ([]*Transaction, erro
225225
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get
226226
func (n *Node) BlockchainTransactionGet(txid string) (string, error) {
227227
var hex string
228-
err := n.request("blockchain.transaction.get", []interface{}{txid, false}, &hex)
228+
// some servers don't handle the second parameter (even though they advertise version 1.2)
229+
// so we leave it out.
230+
err := n.request("blockchain.transaction.get", []interface{}{txid}, &hex)
229231
return hex, err
230232
}
231233

backend/electrum_backend.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func NewElectrumBackend(addr, port string, network utils.Network) (*ElectrumBack
107107

108108
// Connect to a node and handle requests
109109
if err := eb.addNode(addr, port, network); err != nil {
110-
fmt.Printf("failed to connect to initial node: %+v", err)
110+
log.Printf("failed to connect to initial node: %+v", err)
111111
return nil, err
112112
}
113113

@@ -181,6 +181,7 @@ func (eb *ElectrumBackend) ChainHeight() uint32 {
181181
func (eb *ElectrumBackend) addNode(addr, port string, network utils.Network) error {
182182
ident := electrum.NodeIdent(addr, port)
183183

184+
// note: this code contains a TOCTOU bug. We risk connecting to the same node multiple times.
184185
eb.nodeMu.RLock()
185186
_, existsGood := eb.nodes[ident]
186187
_, existsBad := eb.blacklistedNodes[ident]
@@ -443,7 +444,7 @@ func (eb *ElectrumBackend) cacheTxs(txs []*electrum.Transaction) {
443444
for _, tx := range txs {
444445
height, exists := eb.transactions[tx.Hash]
445446
if exists && (height != int64(tx.Height)) {
446-
log.Panicf("inconsistent cache: %s %d != %d", tx.Hash, height, tx.Height)
447+
log.Panicf("inconsistent transactions cache: %s %d != %d", tx.Hash, height, tx.Height)
447448
}
448449
eb.transactions[tx.Hash] = int64(tx.Height)
449450
}
@@ -495,19 +496,19 @@ func (eb *ElectrumBackend) findPeers() {
495496

496497
func (eb *ElectrumBackend) addPeer(peer electrum.Peer) {
497498
if strings.HasSuffix(peer.Host, ".onion") {
498-
log.Printf("skipping %s because of .onion\n", peer.Host)
499+
log.Printf("skipping %s because of .onion", peer.Host)
499500
return
500501
}
501502
err := checkVersion(peer.Version)
502503
if err != nil {
503-
log.Printf("skipping %s because of protocol version %s\n", peer.Host, peer.Version)
504+
log.Printf("skipping %s because of protocol version %s", peer.Host, peer.Version)
504505
return
505506
}
506507
for _, feature := range peer.Features {
507508
if strings.HasPrefix(feature, "t") {
508509
go func(addr, feature string, network utils.Network) {
509510
if err := eb.addNode(addr, feature, network); err != nil {
510-
log.Printf("error on addNode: %+v\n", err)
511+
log.Printf("error on addNode: %+v", err)
511512
}
512513
}(peer.IP, feature, eb.network)
513514
return
@@ -517,11 +518,11 @@ func (eb *ElectrumBackend) addPeer(peer electrum.Peer) {
517518
if strings.HasPrefix(feature, "s") {
518519
go func(addr, feature string, network utils.Network) {
519520
if err := eb.addNode(addr, feature, network); err != nil {
520-
log.Printf("error on addNode: %+v\n", err)
521+
log.Printf("error on addNode: %+v", err)
521522
}
522523
}(peer.IP, feature, eb.network)
523524
return
524525
}
525526
}
526-
log.Printf("skipping %s because of feature mismatch: %+v\n", peer, peer.Features)
527+
log.Printf("skipping %s because of feature mismatch: %+v", peer, peer.Features)
527528
}

main.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ func doKeytree() {
9797
// Check that all the addresses have the same prefix
9898
for i := 1; i < *keytreeN; i++ {
9999
if xpubs[0][0:4] != xpubs[i][0:4] {
100-
fmt.Printf("Prefixes must match: %s %s\n", xpubs[0], xpubs[i])
101-
return
100+
log.Panicf("Prefixes must match: %s %s", xpubs[0], xpubs[i])
102101
}
103102
}
104103

@@ -144,8 +143,7 @@ func doFindAddr() {
144143
// Check that all the addresses have the same prefix
145144
for i := 1; i < *findAddrN; i++ {
146145
if xpubs[0][0:4] != xpubs[i][0:4] {
147-
fmt.Printf("Prefixes must match: %s %s\n", xpubs[0], xpubs[i])
148-
return
146+
log.Panicf("Prefixes must match: %s %s", xpubs[0], xpubs[i])
149147
}
150148
}
151149
network := XpubToNetwork(xpubs[0])
@@ -164,7 +162,7 @@ func doFindAddr() {
164162
}
165163
}
166164
}
167-
fmt.Printf("not found\n")
165+
log.Panic("not found")
168166
}
169167

170168
func doFindBlock() {
@@ -207,6 +205,7 @@ func doComputeBalance() {
207205
if *computeBalanceType == "single-address" {
208206
fmt.Printf("Enter single address:\n")
209207
singleAddress, _ = reader.ReadString('\n')
208+
singleAddress = strings.TrimSpace(singleAddress)
210209
network = AddressToNetwork(singleAddress)
211210
} else {
212211
for i := 0; i < *computeBalanceN; i++ {
@@ -229,12 +228,12 @@ func doComputeBalance() {
229228
backend, err := computeBalanceBuildBackend(network)
230229
PanicOnError(err)
231230

232-
// If blockHeight is 0, we default to current height - 6.
231+
// If blockHeight is 0, we default to current height - 5.
233232
if *computeBalanceBlockHeight == 0 {
234-
*computeBalanceBlockHeight = backend.ChainHeight() - minConfirmations
233+
*computeBalanceBlockHeight = backend.ChainHeight() - minConfirmations + 1
235234
}
236-
if *computeBalanceBlockHeight > backend.ChainHeight()-minConfirmations {
237-
log.Panicf("blockHeight %d is too high (> %d - %d)", *computeBalanceBlockHeight, backend.ChainHeight(), minConfirmations)
235+
if *computeBalanceBlockHeight > backend.ChainHeight()-minConfirmations+1 {
236+
log.Panicf("blockHeight %d is too high (> %d - %d + 1)", *computeBalanceBlockHeight, backend.ChainHeight(), minConfirmations)
238237
}
239238
fmt.Printf("Going to compute balance at %d\n", *computeBalanceBlockHeight)
240239

reporter/reporter.go

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ type Reporter struct {
1414
addressesFetched uint32
1515
txScheduled uint32
1616
txFetched uint32
17-
txAfterFilter int32
1817
peers int32
1918
}
2019

@@ -29,8 +28,8 @@ func GetInstance() *Reporter {
2928
}
3029

3130
func (r *Reporter) Log(msg string) {
32-
fmt.Printf("%d/%d %d/%d/%d %d: %s\n", r.GetAddressesScheduled(), r.GetAddressesFetched(),
33-
r.GetTxScheduled(), r.GetTxFetched(), r.GetTxAfterFilter(), r.GetPeers(), msg)
31+
fmt.Printf("%d/%d %d/%d %d: %s\n", r.GetAddressesScheduled(), r.GetAddressesFetched(),
32+
r.GetTxScheduled(), r.GetTxFetched(), r.GetPeers(), msg)
3433
}
3534

3635
func (r *Reporter) Logf(format string, args ...interface{}) {
@@ -45,10 +44,6 @@ func (r *Reporter) GetAddressesFetched() uint32 {
4544
return atomic.LoadUint32(&r.addressesFetched)
4645
}
4746

48-
func (r *Reporter) SetAddressesFetched(n uint32) {
49-
atomic.StoreUint32(&r.addressesFetched, n)
50-
}
51-
5247
func (r *Reporter) IncAddressesScheduled() {
5348
atomic.AddUint32(&r.addressesScheduled, 1)
5449
}
@@ -57,10 +52,6 @@ func (r *Reporter) GetAddressesScheduled() uint32 {
5752
return atomic.LoadUint32(&r.addressesScheduled)
5853
}
5954

60-
func (r *Reporter) SetddressesScheduled(n uint32) {
61-
atomic.StoreUint32(&r.addressesScheduled, n)
62-
}
63-
6455
func (r *Reporter) IncTxFetched() {
6556
atomic.AddUint32(&r.txFetched, 1)
6657
}
@@ -69,10 +60,6 @@ func (r *Reporter) GetTxFetched() uint32 {
6960
return atomic.LoadUint32(&r.txFetched)
7061
}
7162

72-
func (r *Reporter) SetTxFetched(n uint32) {
73-
atomic.StoreUint32(&r.txFetched, n)
74-
}
75-
7663
func (r *Reporter) IncTxScheduled() {
7764
atomic.AddUint32(&r.txScheduled, 1)
7865
}
@@ -81,26 +68,6 @@ func (r *Reporter) GetTxScheduled() uint32 {
8168
return atomic.LoadUint32(&r.txScheduled)
8269
}
8370

84-
func (r *Reporter) SetTxScheduled(n uint32) {
85-
atomic.StoreUint32(&r.txScheduled, n)
86-
}
87-
88-
func (r *Reporter) IncTxAfterFilter() {
89-
atomic.AddInt32(&r.txAfterFilter, 1)
90-
}
91-
92-
func (r *Reporter) GetTxAfterFilter() int32 {
93-
return atomic.LoadInt32(&r.txAfterFilter)
94-
}
95-
96-
func (r *Reporter) SetTxAfterFilter(n int32) {
97-
atomic.StoreInt32(&r.txAfterFilter, n)
98-
}
99-
100-
func (r *Reporter) IncPeers() {
101-
atomic.AddInt32(&r.peers, 1)
102-
}
103-
10471
func (r *Reporter) GetPeers() int32 {
10572
return atomic.LoadInt32(&r.peers)
10673
}

utils/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func AddressToNetwork(addr string) Network {
5959
case 'n':
6060
return Testnet // pubkey hash
6161
case '2':
62-
return Testnet //script hash
62+
return Testnet // script hash
6363
case '1':
6464
return Mainnet // pubkey hash
6565
case '3':

0 commit comments

Comments
 (0)