Skip to content

Commit daddc89

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 4365927 + 94b22a3 commit daddc89

11 files changed

Lines changed: 318 additions & 26 deletions

api/fiat_rates_api.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
"github.com/trezor/blockbook/common"
1111
)
1212

13+
// MaxFiatRatesTimestamps limits batch fiat-rate lookups to bounded request work.
14+
const MaxFiatRatesTimestamps = 1000
15+
1316
// removeEmpty removes empty strings from a slice.
1417
func removeEmpty(stringSlice []string) []string {
1518
ret := make([]string, 0, len(stringSlice))
@@ -128,6 +131,9 @@ func (w *Worker) GetFiatRatesForTimestamps(timestamps []int64, currencies []stri
128131
if len(timestamps) == 0 {
129132
return nil, NewAPIError("No timestamps provided", true)
130133
}
134+
if len(timestamps) > MaxFiatRatesTimestamps {
135+
return nil, NewAPIError(fmt.Sprintf("too many timestamps, max %d", MaxFiatRatesTimestamps), true)
136+
}
131137
vsCurrency := ""
132138
currencies = removeEmpty(currencies)
133139
if len(currencies) == 1 {

api/fiat_rates_api_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package api
44

55
import (
6+
"fmt"
67
"reflect"
78
"strings"
89
"testing"
@@ -191,6 +192,17 @@ func TestGetFiatRatesForTimestamps_EmptyInput(t *testing.T) {
191192
}
192193
}
193194

195+
func TestGetFiatRatesForTimestamps_LimitsInput(t *testing.T) {
196+
w := &Worker{}
197+
timestamps := make([]int64, MaxFiatRatesTimestamps+1)
198+
_, err := w.GetFiatRatesForTimestamps(timestamps, []string{"usd"}, "")
199+
apiErr := requireAPIError(t, err, true)
200+
want := fmt.Sprintf("too many timestamps, max %d", MaxFiatRatesTimestamps)
201+
if apiErr.Text != want {
202+
t.Fatalf("unexpected error text: got %q, want %q", apiErr.Text, want)
203+
}
204+
}
205+
194206
func TestGetFiatRatesForTimestamps_LenMismatchReturnsNonPublicError(t *testing.T) {
195207
w := &Worker{fiatRates: &fiat.FiatRates{}}
196208
originalGetter := getTickersForTimestamps

api/xpub.go

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const txInput = 1
2222
const txOutput = 2
2323

2424
const xpubCacheExpirationSeconds = 3600
25+
const xpubCacheMaxEntries = 128
26+
const maxXpubAddressDerivations = (maxAddressesGap + 1) * 2
2527

2628
var cachedXpubs map[string]xpubData
2729
var cachedXpubsMux sync.Mutex
@@ -85,18 +87,62 @@ func (w *Worker) initXpubCache() {
8587
}
8688

8789
func (w *Worker) evictXpubCacheItems() {
90+
now := time.Now().Unix()
8891
cachedXpubsMux.Lock()
89-
defer cachedXpubsMux.Unlock()
90-
threshold := time.Now().Unix() - xpubCacheExpirationSeconds
92+
count := evictXpubCacheItemsLocked(now)
93+
cacheSize := len(cachedXpubs)
94+
cachedXpubsMux.Unlock()
95+
96+
w.metrics.XPubCacheSize.Set(float64(cacheSize))
97+
glog.Info("Evicted ", count, " items from xpub cache, cache size ", cacheSize)
98+
}
99+
100+
func evictXpubCacheItemsLocked(now int64) int {
101+
threshold := now - xpubCacheExpirationSeconds
91102
count := 0
92103
for k, v := range cachedXpubs {
93104
if v.accessed < threshold {
94105
delete(cachedXpubs, k)
95106
count++
96107
}
97108
}
98-
w.metrics.XPubCacheSize.Set(float64(len(cachedXpubs)))
99-
glog.Info("Evicted ", count, " items from xpub cache, cache size ", len(cachedXpubs))
109+
return count + trimXpubCacheItemsLocked()
110+
}
111+
112+
func trimXpubCacheItemsLocked() int {
113+
if len(cachedXpubs) <= xpubCacheMaxEntries {
114+
return 0
115+
}
116+
type cacheEntry struct {
117+
key string
118+
accessed int64
119+
}
120+
entries := make([]cacheEntry, 0, len(cachedXpubs))
121+
for k, v := range cachedXpubs {
122+
entries = append(entries, cacheEntry{key: k, accessed: v.accessed})
123+
}
124+
sort.Slice(entries, func(i, j int) bool {
125+
if entries[i].accessed == entries[j].accessed {
126+
return entries[i].key < entries[j].key
127+
}
128+
return entries[i].accessed < entries[j].accessed
129+
})
130+
count := len(cachedXpubs) - xpubCacheMaxEntries
131+
for i := 0; i < count; i++ {
132+
delete(cachedXpubs, entries[i].key)
133+
}
134+
return count
135+
}
136+
137+
func validateXpubScanLimits(xd *bchain.XpubDescriptor, gap int) error {
138+
if len(xd.ChangeIndexes) > bchain.MaxXpubChangeIndexes {
139+
return errors.Errorf("Xpub descriptor change index count %d exceeds limit %d", len(xd.ChangeIndexes), bchain.MaxXpubChangeIndexes)
140+
}
141+
derivations := len(xd.ChangeIndexes) * gap
142+
if derivations > maxXpubAddressDerivations {
143+
return errors.Errorf("Xpub descriptor scan size %d exceeds limit %d", derivations, maxXpubAddressDerivations)
144+
}
145+
return nil
100146
}
101147

102148
func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, fromHeight, toHeight uint32, maxResults int) ([]xpubTxid, bool, error) {
@@ -203,7 +249,10 @@ func (w *Worker) xpubDerivedAddressBalance(data *xpubData, ad *xpubAddress) (boo
203249
return false, nil
204250
}
205251

206-
func (w *Worker) xpubScanAddresses(xd *bchain.XpubDescriptor, data *xpubData, addresses []xpubAddress, gap int, change uint32, minDerivedIndex int, fork bool) (int, []xpubAddress, error) {
252+
func (w *Worker) xpubScanAddresses(xd *bchain.XpubDescriptor, data *xpubData, addresses []xpubAddress, gap int, change uint32, minDerivedIndex int, fork bool, derivedBefore int) (int, []xpubAddress, error) {
253+
if total := derivedBefore + len(addresses); total > maxXpubAddressDerivations {
254+
return 0, nil, errors.Errorf("Xpub descriptor scan size %d exceeds limit %d", total, maxXpubAddressDerivations)
255+
}
207256
// rescan known addresses
208257
lastUsed := 0
209258
for i := range addresses {
@@ -231,6 +280,9 @@ func (w *Worker) xpubScanAddresses(xd *bchain.XpubDescriptor, data *xpubData, ad
231280
if to < minDerivedIndex {
232281
to = minDerivedIndex
233282
}
283+
if total := derivedBefore + to; total > maxXpubAddressDerivations {
284+
return 0, nil, errors.Errorf("Xpub descriptor scan size %d exceeds limit %d", total, maxXpubAddressDerivations)
285+
}
234286
descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xd, change, uint32(from), uint32(to))
235287
if err != nil {
236288
return 0, nil, err
@@ -325,6 +377,9 @@ func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int,
325377
}
326378
// gap is increased one as there must be gap of empty addresses before the derivation is stopped
327379
gap++
380+
if err := validateXpubScanLimits(xd, gap); err != nil {
381+
return nil, 0, false, err
382+
}
328383
var processedHash string
329384
cachedXpubsMux.Lock()
330385
data, inCache := cachedXpubs[xd.XpubDescriptor]
@@ -366,11 +421,13 @@ func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int,
366421
data.sentSat = *new(big.Int)
367422
data.txCountEstimate = 0
368423
var minDerivedIndex int
424+
totalDerived := 0
369425
for i, change := range xd.ChangeIndexes {
370-
minDerivedIndex, data.addresses[i], err = w.xpubScanAddresses(xd, &data, data.addresses[i], gap, change, minDerivedIndex, fork)
426+
minDerivedIndex, data.addresses[i], err = w.xpubScanAddresses(xd, &data, data.addresses[i], gap, change, minDerivedIndex, fork, totalDerived)
371427
if err != nil {
372428
return nil, 0, inCache, err
373429
}
430+
totalDerived += len(data.addresses[i])
374431
}
375432
}
376433
if option >= AccountDetailsTxidHistory {
@@ -385,8 +442,14 @@ func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int,
385442
}
386443
data.accessed = time.Now().Unix()
387444
cachedXpubsMux.Lock()
445+
if cachedXpubs == nil {
446+
cachedXpubs = make(map[string]xpubData)
447+
}
388448
cachedXpubs[xd.XpubDescriptor] = data
449+
trimXpubCacheItemsLocked()
450+
cacheSize := len(cachedXpubs)
389451
cachedXpubsMux.Unlock()
452+
w.metrics.XPubCacheSize.Set(float64(cacheSize))
390453
return &data, bestheight, inCache, nil
391454
}
392455

api/xpub_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//go:build unittest
2+
3+
package api
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
9+
"github.com/trezor/blockbook/bchain"
10+
)
11+
12+
func TestValidateXpubScanLimits(t *testing.T) {
13+
if err := validateXpubScanLimits(&bchain.XpubDescriptor{ChangeIndexes: []uint32{0, 1}}, maxAddressesGap+1); err != nil {
14+
t.Fatalf("expected default change indexes at max gap to pass, got %v", err)
15+
}
16+
17+
changes := make([]uint32, bchain.MaxXpubChangeIndexes+1)
18+
if err := validateXpubScanLimits(&bchain.XpubDescriptor{ChangeIndexes: changes}, defaultAddressesGap+1); err == nil {
19+
t.Fatal("expected change index count above limit to fail")
20+
}
21+
22+
changes = make([]uint32, 3)
23+
if err := validateXpubScanLimits(&bchain.XpubDescriptor{ChangeIndexes: changes}, maxAddressesGap+1); err == nil {
24+
t.Fatal("expected scan size above limit to fail")
25+
}
26+
}
27+
28+
func TestTrimXpubCacheItemsLocked(t *testing.T) {
29+
cachedXpubsMux.Lock()
30+
defer cachedXpubsMux.Unlock()
31+
32+
originalCache := cachedXpubs
33+
defer func() {
34+
cachedXpubs = originalCache
35+
}()
36+
37+
cachedXpubs = make(map[string]xpubData, xpubCacheMaxEntries+2)
38+
for i := 0; i < xpubCacheMaxEntries+2; i++ {
39+
cachedXpubs[fmt.Sprintf("xpub-%03d", i)] = xpubData{accessed: int64(i)}
40+
}
41+
42+
if got := trimXpubCacheItemsLocked(); got != 2 {
43+
t.Fatalf("trimXpubCacheItemsLocked() evicted %d entries, want 2", got)
44+
}
45+
if got := len(cachedXpubs); got != xpubCacheMaxEntries {
46+
t.Fatalf("cachedXpubs length = %d, want %d", got, xpubCacheMaxEntries)
47+
}
48+
if _, ok := cachedXpubs["xpub-000"]; ok {
49+
t.Fatal("oldest cache entry was not evicted")
50+
}
51+
if _, ok := cachedXpubs["xpub-001"]; ok {
52+
t.Fatal("second oldest cache entry was not evicted")
53+
}
54+
}

bchain/coins/btc/bitcoinlikeparser.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,11 @@ var (
441441
)
442442

443443
func init() {
444-
xpubDesriptorRegex, _ = regexp.Compile(`^(?P<type>(sh\(wpkh|wpkh|pk|pkh|wpkh|wsh|tr))\((\[\w+/(?P<bip>\d+)['h]/\d+['h]?/\d+['h]?\])?(?P<xpub>\w+)(/(({(?P<changelist1>\d+(,\d+)*)})|(<(?P<changelist2>\d+(;\d+)*)>)|(?P<change>\d+))/\*)?\)+`)
444+
var err error
445+
xpubDesriptorRegex, err = regexp.Compile(`^(?P<type>(sh\(wpkh|wpkh|pk|pkh|wpkh|wsh|tr))\((\[\w+/(?P<bip>\d+)['h]/\d+['h]?/\d+['h]?\])?(?P<xpub>\w+)(/(({(?P<changelist1>\d+(,\d+)*)})|(<(?P<changelist2>\d+(;\d+)*)>)|(?P<change>\d+))/\*)?\)+`)
446+
if err != nil {
447+
panic(errors.Annotate(err, "Invalid bitcoinparser xpubDesriptorRegex"))
448+
}
445449
typeSubexpIndex = xpubDesriptorRegex.SubexpIndex("type")
446450
bipSubexpIndex = xpubDesriptorRegex.SubexpIndex("bip")
447451
xpubSubexpIndex = xpubDesriptorRegex.SubexpIndex("xpub")
@@ -502,6 +506,9 @@ func (p *BitcoinLikeParser) ParseXpub(xpub string) (*bchain.XpubDescriptor, erro
502506
if len(changes) == 0 {
503507
return nil, errors.New("Invalid xpub descriptor, cannot parse change")
504508
}
509+
if len(changes) > bchain.MaxXpubChangeIndexes {
510+
return nil, errors.Errorf("Xpub descriptor change index count exceeds limit %d", bchain.MaxXpubChangeIndexes)
511+
}
505512
descriptor.ChangeIndexes = make([]uint32, len(changes))
506513
for i, ch := range changes {
507514
change, err := strconv.ParseUint(ch, 10, 32)

bchain/coins/btc/bitcoinparser_test.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"math/big"
88
"os"
99
"reflect"
10+
"strconv"
11+
"strings"
1012
"testing"
1113

1214
"github.com/martinboehm/btcutil/chaincfg"
@@ -19,6 +21,22 @@ func TestMain(m *testing.M) {
1921
os.Exit(c)
2022
}
2123

24+
func testChangeList(count int, separator string) string {
25+
changes := make([]string, count)
26+
for i := range changes {
27+
changes[i] = strconv.Itoa(i)
28+
}
29+
return strings.Join(changes, separator)
30+
}
31+
32+
func testChangeIndexes(count int) []uint32 {
33+
indexes := make([]uint32, count)
34+
for i := range indexes {
35+
indexes[i] = uint32(i)
36+
}
37+
return indexes
38+
}
39+
2240
func TestGetAddrDescFromAddress(t *testing.T) {
2341
type args struct {
2442
address string
@@ -781,11 +799,12 @@ func TestParseXpubDescriptors(t *testing.T) {
781799
btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518})
782800
btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198})
783801
tests := []struct {
784-
name string
785-
xpub string
786-
parser *BitcoinParser
787-
want *bchain.XpubDescriptor
788-
wantErr bool
802+
name string
803+
xpub string
804+
parser *BitcoinParser
805+
want *bchain.XpubDescriptor
806+
wantErr bool
807+
wantErrContains string
789808
}{
790809
{
791810
name: "tpub",
@@ -847,6 +866,32 @@ func TestParseXpubDescriptors(t *testing.T) {
847866
ChangeIndexes: []uint32{0, 1, 2},
848867
},
849868
},
869+
{
870+
name: "tr([5c9e228d/86'/1'/0']tpubD/{max changes}/*)#4rqwxvej",
871+
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{" + testChangeList(bchain.MaxXpubChangeIndexes, ",") + "}/*)#4rqwxvej",
872+
parser: btcTestnetParser,
873+
want: &bchain.XpubDescriptor{
874+
XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{" + testChangeList(bchain.MaxXpubChangeIndexes, ",") + "}/*)#4rqwxvej",
875+
Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
876+
Type: bchain.P2TR,
877+
Bip: "86",
878+
ChangeIndexes: testChangeIndexes(bchain.MaxXpubChangeIndexes),
879+
},
880+
},
881+
{
882+
name: "tr([5c9e228d/86'/1'/0']tpubD/{too many changes}/*)#4rqwxvej error - change list limit",
883+
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{" + testChangeList(bchain.MaxXpubChangeIndexes+1, ",") + "}/*)#4rqwxvej",
884+
parser: btcTestnetParser,
885+
wantErr: true,
886+
wantErrContains: "change index count exceeds limit",
887+
},
888+
{
889+
name: "tr([5c9e228d/86'/1'/0']tpubD/<too many changes>/*)#4rqwxvej error - change list limit",
890+
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/<" + testChangeList(bchain.MaxXpubChangeIndexes+1, ";") + ">/*)#4rqwxvej",
891+
parser: btcTestnetParser,
892+
wantErr: true,
893+
wantErrContains: "change index count exceeds limit",
894+
},
850895
{
851896
name: "tr([5c9e228d/86'/1'/0']tpubD/3/*)#4rqwxvej",
852897
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/3/*)#4rqwxvej",
@@ -981,6 +1026,9 @@ func TestParseXpubDescriptors(t *testing.T) {
9811026
t.Errorf("ParseXpub() error = %v, wantErr %v", err, tt.wantErr)
9821027
return
9831028
}
1029+
if err != nil && tt.wantErrContains != "" && !strings.Contains(err.Error(), tt.wantErrContains) {
1030+
t.Errorf("ParseXpub() error = %v, want error containing %q", err, tt.wantErrContains)
1031+
}
9841032
if err == nil {
9851033
if got.ExtKey == nil {
9861034
t.Errorf("ParseXpub() got nil ExtKey")

bchain/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ const (
267267
P2TR
268268
)
269269

270+
// MaxXpubChangeIndexes limits how many change branches one xpub descriptor can
271+
// expand during account scans.
272+
const MaxXpubChangeIndexes = 10
273+
270274
// XpubDescriptor contains parsed data from xpub descriptor
271275
type XpubDescriptor struct {
272276
XpubDescriptor string `ts_doc:"Full descriptor string including xpub and script type."`

server/public.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,22 @@ func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{},
16991699
return result, nil
17001700
}
17011701

1702+
func countCommaSeparatedValues(s string, limit int) int {
1703+
if s == "" {
1704+
return 0
1705+
}
1706+
count := 1
1707+
for i := 0; i < len(s); i++ {
1708+
if s[i] == ',' {
1709+
count++
1710+
if count > limit {
1711+
return count
1712+
}
1713+
}
1714+
}
1715+
return count
1716+
}
1717+
17021718
// apiMultiTickers returns FiatRates ticker prices for the specified comma separated list of timestamps.
17031719
func (s *PublicServer) apiMultiTickers(r *http.Request, apiVersion int) (interface{}, error) {
17041720
var result []api.FiatTicker
@@ -1713,6 +1729,9 @@ func (s *PublicServer) apiMultiTickers(r *http.Request, apiVersion int) (interfa
17131729
if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" {
17141730
// Get tickers for specified timestamp
17151731
s.metrics.ExplorerViews.With(common.Labels{"action": "api-multi-tickers-date"}).Inc()
1732+
if countCommaSeparatedValues(timestampString, api.MaxFiatRatesTimestamps) > api.MaxFiatRatesTimestamps {
1733+
return nil, api.NewAPIError(fmt.Sprintf("too many timestamps, max %d", api.MaxFiatRatesTimestamps), true)
1734+
}
17161735
timestamps := strings.Split(timestampString, ",")
17171736
t := make([]int64, len(timestamps))
17181737
for i := range timestamps {

0 commit comments

Comments
 (0)