Repository: coinbase/rosetta-cli
Pull request #114 (2020-08-19 17:35:18)

[BUG] Ensure block synced before starting Construction API Testing Loop
In some cases, the automated Construction API tester will attempt to create an address before it has synced any blocks. This throws an error because the balance stored in `BalanceStorage` cannot be checked until at least one block is synced (we always return a *types.BlockIdentifier indicating when a balance was checked).

### Changes
- [x] Check that at least 1 block has been synced (i.e. there is a non-nil `HeadBlockIdentifier`) before starting the `CreateTransactions` loop in `pkg/constructor`


Patch:
diff --git a/go.mod b/go.mod
index 4728109..6491ca0 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/coinbase/rosetta-cli
 go 1.13
 
 require (
-	github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200819052137-5d2bf966818b
+	github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200819181259-3eff56cab108
 	github.com/fatih/color v1.9.0
 	github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
 	github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c
diff --git a/go.sum b/go.sum
index 046e374..ad10bf7 100644
--- a/go.sum
+++ b/go.sum
@@ -57,8 +57,12 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
+github.com/coinbase/rosetta-sdk-go v0.3.4 h1:jWKgajozco/T0FNnZb2TqBsmsUoF6ZuCLnUJkEE+vNg=
+github.com/coinbase/rosetta-sdk-go v0.3.4/go.mod h1:Q6dAY0kdG2X3jNaIYnkxnZOb8XEZQar9Q1RcnBgm/wQ=
 github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200819052137-5d2bf966818b h1:JSV23Q51pp5SEKEQ0ZspXevVz5xNdsCIpW786pVUf7g=
 github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200819052137-5d2bf966818b/go.mod h1:7y6tIrUHdxBAS0kshqdHZXyi0CQ+O9s+Itmodzxs7IQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200819181259-3eff56cab108 h1:mW8fci/qMy7Jl5QjvMf1dkm+UKqoJnciCbyiLXcJeQo=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200819181259-3eff56cab108/go.mod h1:7y6tIrUHdxBAS0kshqdHZXyi0CQ+O9s+Itmodzxs7IQ=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -138,6 +142,11 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26 h1:lMm2hD9Fy0ynom5+85/pbdkiYcBqM1JWmhpAXLmy0fw=
 github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/addlicense v0.0.0-20200622132530-df58acafd6d5 h1:m6Z1Cm53o4VecQFxKCnvULGfIT0Igo3MX131i+00IIo=
+github.com/google/addlicense v0.0.0-20200622132530-df58acafd6d5/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
+github.com/google/addlicense v0.0.0-20200815122332-4295bd52e3cf h1:YE2N9S5+KJNPjtA0uTxDgtfZ7U7/Ia0IkKI5bCUYcp8=
+github.com/google/addlicense v0.0.0-20200815122332-4295bd52e3cf/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
+github.com/google/addlicense v0.0.0-20200817051935-6f4cd4aacc89 h1:oTppmscIAQ2Y1tcsMDcTLR3z4MN/96/pvIsBSLGl7o8=
 github.com/google/addlicense v0.0.0-20200817051935-6f4cd4aacc89/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -253,6 +262,7 @@ github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9Ac
 github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/segmentio/golines v0.0.0-20200306054842-869934f8da7b/go.mod h1:K7zjgP8yJ/U8nb8nxaSykalAKSvbqr6TNbd9B7zzBFU=
 github.com/segmentio/golines v0.0.0-20200808004416-0a9796b248e8/go.mod h1:bQSh5qdVR67XiCKbaVvYO41s50c5hQo+3cY/1CQQ3xQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shirou/gopsutil v2.20.5+incompatible h1:tYH07UPoQt0OCQdgWWMgYHy3/a9bcxNpBIysykNIP7I=
@@ -360,6 +370,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/mocks/constructor/handler.go b/mocks/constructor/handler.go
index 8cef173..4afe30e 100644
--- a/mocks/constructor/handler.go
+++ b/mocks/constructor/handler.go
@@ -1,17 +1,3 @@
-// Copyright 2020 Coinbase, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
 // Code generated by mockery v1.0.0. DO NOT EDIT.
 
 package constructor
diff --git a/mocks/constructor/helper.go b/mocks/constructor/helper.go
index 302a220..be7a65c 100644
--- a/mocks/constructor/helper.go
+++ b/mocks/constructor/helper.go
@@ -1,17 +1,3 @@
-// Copyright 2020 Coinbase, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
 // Code generated by mockery v1.0.0. DO NOT EDIT.
 
 package constructor
@@ -247,6 +233,20 @@ func (_m *Helper) Hash(_a0 context.Context, _a1 *types.NetworkIdentifier, _a2 st
 	return r0, r1
 }
 
+// HeadBlockExists provides a mock function with given fields: _a0
+func (_m *Helper) HeadBlockExists(_a0 context.Context) bool {
+	ret := _m.Called(_a0)
+
+	var r0 bool
+	if rf, ok := ret.Get(0).(func(context.Context) bool); ok {
+		r0 = rf(_a0)
+	} else {
+		r0 = ret.Get(0).(bool)
+	}
+
+	return r0
+}
+
 // LockedAddresses provides a mock function with given fields: _a0
 func (_m *Helper) LockedAddresses(_a0 context.Context) ([]string, error) {
 	ret := _m.Called(_a0)
diff --git a/pkg/constructor/constructor.go b/pkg/constructor/constructor.go
index eb0818e..0e08ba7 100644
--- a/pkg/constructor/constructor.go
+++ b/pkg/constructor/constructor.go
@@ -188,6 +188,10 @@ type Helper interface {
 
 	// RandomAmount returns some integer between min and max.
 	RandomAmount(*big.Int, *big.Int) *big.Int
+
+	// HeadBlockExists returns a boolean indicating if a block
+	// has been synced by BlockStorage.
+	HeadBlockExists(context.Context) bool
 }
 
 // Handler is an interface called by the constructor whenever
@@ -914,6 +918,15 @@ func (c *Constructor) CreateTransactions(
 	}
 
 	for ctx.Err() == nil {
+		if !c.helper.HeadBlockExists(ctx) {
+			// We will sleep until at least one block has been synced.
+			// Many of the storage-based commands require a synced block
+			// to work correctly (i.e. when fetching a balance, a block
+			// must be returned).
+			time.Sleep(defaultSleepTime * time.Second)
+			continue
+		}
+
 		sender, balance, coinIdentifier, err := c.findSender(ctx)
 		if err != nil {
 			return fmt.Errorf("%w: unable to find sender", err)
diff --git a/pkg/processor/constructor_helper.go b/pkg/processor/constructor_helper.go
index 548fa6a..f0034aa 100644
--- a/pkg/processor/constructor_helper.go
+++ b/pkg/processor/constructor_helper.go
@@ -36,6 +36,7 @@ type ConstructorHelper struct {
 	offlineFetcher *fetcher.Fetcher
 	onlineFetcher  *fetcher.Fetcher
 
+	blockStorage     *storage.BlockStorage
 	keyStorage       *storage.KeyStorage
 	balanceStorage   *storage.BalanceStorage
 	coinStorage      *storage.CoinStorage
@@ -46,6 +47,7 @@ type ConstructorHelper struct {
 func NewConstructorHelper(
 	offlineFetcher *fetcher.Fetcher,
 	onlineFetcher *fetcher.Fetcher,
+	blockStorage *storage.BlockStorage,
 	keyStorage *storage.KeyStorage,
 	balanceStorage *storage.BalanceStorage,
 	coinStorage *storage.CoinStorage,
@@ -54,6 +56,7 @@ func NewConstructorHelper(
 	return &ConstructorHelper{
 		offlineFetcher:   offlineFetcher,
 		onlineFetcher:    onlineFetcher,
+		blockStorage:     blockStorage,
 		keyStorage:       keyStorage,
 		balanceStorage:   balanceStorage,
 		coinStorage:      coinStorage,
@@ -267,3 +270,11 @@ func (c *ConstructorHelper) AllAddresses(ctx context.Context) ([]string, error)
 func (c *ConstructorHelper) RandomAmount(min *big.Int, max *big.Int) *big.Int {
 	return utils.RandomNumber(min, max)
 }
+
+// HeadBlockExists returns a boolean indicating if a block has been
+// synced by BlockStorage.
+func (c *ConstructorHelper) HeadBlockExists(ctx context.Context) bool {
+	headBlock, _ := c.blockStorage.GetHeadBlockIdentifier(ctx)
+
+	return headBlock != nil
+}


Test patch:
diff --git a/pkg/tester/construction.go b/pkg/tester/construction.go
index 26b04f7..8139f92 100644
--- a/pkg/tester/construction.go
+++ b/pkg/tester/construction.go
@@ -159,6 +159,7 @@ func InitializeConstruction(
 	constructorHelper := processor.NewConstructorHelper(
 		offlineFetcher,
 		onlineFetcher,
+		blockStorage,
 		keyStorage,
 		balanceStorage,
 		coinStorage,