diff --git a/README.md b/README.md index 848925f2..f0bdd48d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![License](https://img.shields.io/github/license/coinbase/rosetta-cli.svg)](https://github.com/coinbase/rosetta-cli/blob/master/LICENSE.txt) ## What is Rosetta? -Rosetta is a new project from Coinbase to standardize the process +Rosetta is a new project to standardize the process of deploying and interacting with blockchains. With an explicit specification to adhere to, all parties involved in blockchain development can spend less time figuring out how to integrate @@ -181,9 +181,9 @@ Global Flags: * `make release` to run one last check before opening a PR ### Helper/Handler -Many of the internal packages use a `Helper/Handler` interface pattern to acquire +Many of the packages use a `Helper/Handler` interface pattern to acquire required information or to send events to some client implementation. An example -of this is in the `internal/reconciler` package where a `Helper` is used to get +of this is in the `reconciler` package where a `Helper` is used to get the account balance and the `Handler` is called to incidate whether the reconciliation of an account was successful. @@ -194,9 +194,7 @@ examples // examples of different config files internal logger // logic to write syncing information to stdout/files processor // Helper/Handler implementations for reconciler, storage, and syncer - reconciler // checks for equality between computed balance and node balance storage // persists block to temporary storage and allows for querying balances - syncer // coordinates block syncing (inlcuding re-orgs) utils // useful functions ``` @@ -220,24 +218,22 @@ negative from any operations. ### Balance Reconciliation #### Active Addresses -The validator checks that the balance of an account computed by +The CLI checks that the balance of an account computed by its operations is equal to the balance of the account according -to the node. If this balance is not identical, the validator will +to the node. If this balance is not identical, the CLI will exit. #### Inactive Addresses -The validator randomly checks the balances of accounts that aren't +The CLI randomly checks the balances of accounts that aren't involved in any transactions. The balances of accounts could change on the blockchain node without being included in an operation -returned by the Rosetta Server. Recall that all balance-changing -operations must be returned by the Rosetta Server. +returned by the Rosetta Node API. Recall that all balance-changing +operations should be returned by the Rosetta Node API. ## Future Work -* Move syncer, reconciler, and storage packages to rosetta-sdk-go for better re-use. * Automatically test the correctness of a Rosetta Client SDK by constructing, signing, and submitting a transaction. This can be further extended by ensuring broadcast transactions eventually land in a block. -* Change logging to utilize a more advanced output mechanism than CSV. ## License This project is available open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0). diff --git a/cmd/check.go b/cmd/check.go index 8d029b79..684043b7 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -25,12 +25,12 @@ import ( "github.com/coinbase/rosetta-cli/internal/logger" "github.com/coinbase/rosetta-cli/internal/processor" - "github.com/coinbase/rosetta-cli/internal/reconciler" "github.com/coinbase/rosetta-cli/internal/storage" - "github.com/coinbase/rosetta-cli/internal/syncer" "github.com/coinbase/rosetta-cli/internal/utils" "github.com/coinbase/rosetta-sdk-go/fetcher" + "github.com/coinbase/rosetta-sdk-go/reconciler" + "github.com/coinbase/rosetta-sdk-go/syncer" "github.com/coinbase/rosetta-sdk-go/types" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" diff --git a/go.mod b/go.mod index 73f858e4..7f9c5574 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/coinbase/rosetta-cli go 1.13 require ( - github.com/coinbase/rosetta-sdk-go v0.1.10 + github.com/coinbase/rosetta-sdk-go v0.1.11-0.20200512194317-06d0663c9207 github.com/dgraph-io/badger v1.6.0 + github.com/mattn/goveralls v0.0.5 // indirect github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.5.1 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a + golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c // indirect ) diff --git a/go.sum b/go.sum index c0817b1d..ebfaf1dd 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/coinbase/rosetta-sdk-go v0.1.9 h1:HW60xBt2h54QL0AlfMpBM05lrwlV4FE/ZnM github.com/coinbase/rosetta-sdk-go v0.1.9/go.mod h1:Z3yIflVjfPH1tYN/ucYcnJuXnxIr1xzO26YLla6jYLw= github.com/coinbase/rosetta-sdk-go v0.1.10 h1:AzmvZXhsTNjXAcLfkgdbO0OWTj++ZeyW4ZXFyd9Gn1w= github.com/coinbase/rosetta-sdk-go v0.1.10/go.mod h1:Z3yIflVjfPH1tYN/ucYcnJuXnxIr1xzO26YLla6jYLw= +github.com/coinbase/rosetta-sdk-go v0.1.11-0.20200512194317-06d0663c9207 h1:Xg6sVVLsMjrlg3CFylzW8uNy258DJ81BMSggrXSQZjg= +github.com/coinbase/rosetta-sdk-go v0.1.11-0.20200512194317-06d0663c9207/go.mod h1:XKM7urGHLqGQJi9kM97N+GpMLJuCAGYXy2wOm3KzxEE= 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= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= diff --git a/internal/processor/storage_helper.go b/internal/processor/storage_helper.go index 9f637102..9a087136 100644 --- a/internal/processor/storage_helper.go +++ b/internal/processor/storage_helper.go @@ -18,11 +18,10 @@ import ( "context" "fmt" - "github.com/coinbase/rosetta-cli/internal/reconciler" - "github.com/coinbase/rosetta-sdk-go/asserter" "github.com/coinbase/rosetta-sdk-go/fetcher" "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/reconciler" "github.com/coinbase/rosetta-sdk-go/types" ) diff --git a/internal/processor/syncer_handler.go b/internal/processor/syncer_handler.go index d18fd771..4f7d244b 100644 --- a/internal/processor/syncer_handler.go +++ b/internal/processor/syncer_handler.go @@ -19,8 +19,8 @@ import ( "log" "github.com/coinbase/rosetta-cli/internal/logger" - "github.com/coinbase/rosetta-cli/internal/reconciler" "github.com/coinbase/rosetta-cli/internal/storage" + "github.com/coinbase/rosetta-sdk-go/reconciler" "github.com/coinbase/rosetta-sdk-go/fetcher" "github.com/coinbase/rosetta-sdk-go/types" diff --git a/internal/reconciler/reconciler.go b/internal/reconciler/reconciler.go deleted file mode 100644 index d8ffe586..00000000 --- a/internal/reconciler/reconciler.go +++ /dev/null @@ -1,698 +0,0 @@ -// 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. - -package reconciler - -import ( - "context" - "errors" - "fmt" - "log" - "sync" - "time" - - "github.com/coinbase/rosetta-cli/internal/utils" - - "github.com/coinbase/rosetta-sdk-go/fetcher" - "github.com/coinbase/rosetta-sdk-go/parser" - "github.com/coinbase/rosetta-sdk-go/types" - "golang.org/x/sync/errgroup" -) - -const ( - // backlogThreshold is the limit of account lookups - // that can be enqueued to reconcile before new - // requests are dropped. - // TODO: Make configurable - backlogThreshold = 1000 - - // waitToCheckDiff is the syncing difference (live-head) - // to retry instead of exiting. In other words, if the - // processed head is behind the live head by < - // waitToCheckDiff we should try again after sleeping. - // TODO: Make configurable - waitToCheckDiff = 10 - - // waitToCheckDiffSleep is the amount of time to wait - // to check a balance difference if the syncer is within - // waitToCheckDiff from the block a balance was queried at. - waitToCheckDiffSleep = 5 * time.Second - - // activeReconciliation is included in the reconciliation - // error message if reconciliation failed during active - // reconciliation. - activeReconciliation = "ACTIVE" - - // inactiveReconciliation is included in the reconciliation - // error message if reconciliation failed during inactive - // reconciliation. - inactiveReconciliation = "INACTIVE" - - // zeroString is a string of value 0. - zeroString = "0" - - // inactiveReconciliationSleep is used as the time.Duration - // to sleep when there are no seen accounts to reconcile. - inactiveReconciliationSleep = 5 * time.Second - - // inactiveReconciliationRequiredDepth is the minimum - // number of blocks the reconciler should wait between - // inactive reconciliations. - // TODO: make configurable - inactiveReconciliationRequiredDepth = 500 -) - -var ( - // ErrHeadBlockBehindLive is returned when the processed - // head is behind the live head. Sometimes, it is - // preferrable to sleep and wait to catch up when - // we are close to the live head (waitToCheckDiff). - ErrHeadBlockBehindLive = errors.New("head block behind") - - // ErrAccountUpdated is returned when the - // account was updated at a height later than - // the live height (when the account balance was fetched). - ErrAccountUpdated = errors.New("account updated") - - // ErrBlockGone is returned when the processed block - // head is greater than the live head but the block - // does not exist in the store. This likely means - // that the block was orphaned. - ErrBlockGone = errors.New("block gone") -) - -// Helper functions are used by Reconciler to compare -// computed balances from a block with the balance calculated -// by the node. Defining an interface allows the client to determine -// what sort of storage layer they want to use to provide the required -// information. -type Helper interface { - BlockExists( - ctx context.Context, - block *types.BlockIdentifier, - ) (bool, error) - - CurrentBlock( - ctx context.Context, - ) (*types.BlockIdentifier, error) - - AccountBalance( - ctx context.Context, - account *types.AccountIdentifier, - currency *types.Currency, - headBlock *types.BlockIdentifier, - ) (*types.Amount, *types.BlockIdentifier, error) -} - -// Handler is called by Reconciler after a reconciliation -// is performed. When a reconciliation failure is observed, -// it is up to the client to halt syncing or log the result. -type Handler interface { - ReconciliationFailed( - ctx context.Context, - reconciliationType string, - account *types.AccountIdentifier, - currency *types.Currency, - computedBalance string, - nodeBalance string, - block *types.BlockIdentifier, - ) error - - ReconciliationSucceeded( - ctx context.Context, - reconciliationType string, - account *types.AccountIdentifier, - currency *types.Currency, - balance string, - block *types.BlockIdentifier, - ) error -} - -// Reconciler contains all logic to reconcile balances of -// types.AccountIdentifiers returned in types.Operations -// by a Rosetta Server. -type Reconciler struct { - network *types.NetworkIdentifier - helper Helper - handler Handler - fetcher *fetcher.Fetcher - accountConcurrency uint64 - lookupBalanceByBlock bool - interestingAccounts []*AccountCurrency - changeQueue chan *parser.BalanceChange - - // highWaterMark is used to skip requests when - // we are very far behind the live head. - highWaterMark int64 - - // seenAccts are stored for inactive account - // reconciliation. - seenAccts []*AccountCurrency - inactiveQueue []*parser.BalanceChange - - // inactiveQueueMutex needed because we can't peek at the tip - // of a channel to determine when it is ready to look at. - inactiveQueueMutex sync.Mutex -} - -// NewReconciler creates a new Reconciler. -func NewReconciler( - network *types.NetworkIdentifier, - helper Helper, - handler Handler, - fetcher *fetcher.Fetcher, - accountConcurrency uint64, - lookupBalanceByBlock bool, - interestingAccounts []*AccountCurrency, - // TODO: load seenAccts and inactiveQueue from prior run (if exists) -) *Reconciler { - r := &Reconciler{ - network: network, - helper: helper, - handler: handler, - fetcher: fetcher, - accountConcurrency: accountConcurrency, - lookupBalanceByBlock: lookupBalanceByBlock, - interestingAccounts: interestingAccounts, - highWaterMark: -1, - seenAccts: make([]*AccountCurrency, 0), - inactiveQueue: make([]*parser.BalanceChange, 0), - } - - if lookupBalanceByBlock { - // When lookupBalanceByBlock is enabled, we check - // balance changes synchronously. - r.changeQueue = make(chan *parser.BalanceChange) - } else { - // When lookupBalanceByBlock is disabled, we must check - // balance changes asynchronously. Using a buffered - // channel allows us to add balance changes without blocking. - r.changeQueue = make(chan *parser.BalanceChange, backlogThreshold) - } - - return r -} - -// QueueChanges enqueues a slice of *BalanceChanges -// for reconciliation. -func (r *Reconciler) QueueChanges( - ctx context.Context, - block *types.BlockIdentifier, - balanceChanges []*parser.BalanceChange, -) error { - // Ensure all interestingAccounts are checked - for _, account := range r.interestingAccounts { - skipAccount := false - // Look through balance changes for account + currency - for _, change := range balanceChanges { - if utils.Equal(change.Account, account.Account) && - utils.Equal(change.Currency, account.Currency) { - skipAccount = true - break - } - } - // Account changed on this block - if skipAccount { - continue - } - - // If account + currency not found, add with difference 0 - balanceChanges = append(balanceChanges, &parser.BalanceChange{ - Account: account.Account, - Currency: account.Currency, - Difference: zeroString, - Block: block, - }) - } - - if !r.lookupBalanceByBlock { - // All changes will have the same block. Return - // if we are too far behind to start reconciling. - if block.Index < r.highWaterMark { - return nil - } - - for _, change := range balanceChanges { - select { - case r.changeQueue <- change: - default: - log.Println("skipping active enqueue because backlog") - } - } - } else { - // Block until all checked for a block or context is Done - for _, change := range balanceChanges { - select { - case r.changeQueue <- change: - case <-ctx.Done(): - return ctx.Err() - } - } - } - - return nil -} - -// CompareBalance checks to see if the computed balance of an account -// is equal to the live balance of an account. This function ensures -// balance is checked correctly in the case of orphaned blocks. -func (r *Reconciler) CompareBalance( - ctx context.Context, - account *types.AccountIdentifier, - currency *types.Currency, - amount string, - liveBlock *types.BlockIdentifier, -) (string, string, int64, error) { - // Head block should be set before we CompareBalance - head, err := r.helper.CurrentBlock(ctx) - if err != nil { - return zeroString, "", 0, fmt.Errorf( - "%w: unable to get current block for reconciliation", - err, - ) - } - - // Check if live block is < head (or wait) - if liveBlock.Index > head.Index { - return zeroString, "", head.Index, fmt.Errorf( - "%w live block %d > head block %d", - ErrHeadBlockBehindLive, - liveBlock.Index, - head.Index, - ) - } - - // Check if live block is in store (ensure not reorged) - exists, err := r.helper.BlockExists(ctx, liveBlock) - if err != nil { - return zeroString, "", 0, fmt.Errorf( - "%w: unable to check if block exists: %+v", - err, - liveBlock, - ) - } - if !exists { - return zeroString, "", head.Index, fmt.Errorf( - "%w %+v", - ErrBlockGone, - liveBlock, - ) - } - - // Check if live block < computed head - cachedBalance, balanceBlock, err := r.helper.AccountBalance( - ctx, - account, - currency, - head, - ) - if err != nil { - return zeroString, "", head.Index, fmt.Errorf( - "%w: unable to get cached balance for %+v:%+v", - err, - account, - currency, - ) - } - - if liveBlock.Index < balanceBlock.Index { - return zeroString, "", head.Index, fmt.Errorf( - "%w %+v updated at %d", - ErrAccountUpdated, - account, - balanceBlock.Index, - ) - } - - difference, err := types.SubtractValues(cachedBalance.Value, amount) - if err != nil { - return "", "", -1, err - } - - return difference, cachedBalance.Value, head.Index, nil -} - -// bestBalance returns the balance for an account -// at either the current block (if lookupBalanceByBlock is -// disabled) or at some historical block. -func (r *Reconciler) bestBalance( - ctx context.Context, - account *types.AccountIdentifier, - currency *types.Currency, - block *types.PartialBlockIdentifier, -) (*types.BlockIdentifier, string, error) { - if !r.lookupBalanceByBlock { - // Use the current balance to reconcile balances when lookupBalanceByBlock - // is disabled. This could be the case when a rosetta server does not - // support historical balance lookups. - block = nil - } - return GetCurrencyBalance( - ctx, - r.fetcher, - r.network, - account, - currency, - block, - ) -} - -// accountReconciliation returns an error if the provided -// AccountAndCurrency's live balance cannot be reconciled -// with the computed balance. -func (r *Reconciler) accountReconciliation( - ctx context.Context, - account *types.AccountIdentifier, - currency *types.Currency, - liveAmount string, - liveBlock *types.BlockIdentifier, - inactive bool, -) error { - accountCurrency := &AccountCurrency{ - Account: account, - Currency: currency, - } - for ctx.Err() == nil { - // If don't have previous balance because stateless, check diff on block - // instead of comparing entire computed balance - difference, cachedBalance, headIndex, err := r.CompareBalance( - ctx, - account, - currency, - liveAmount, - liveBlock, - ) - if err != nil { - if errors.Is(err, ErrHeadBlockBehindLive) { - // This error will only occur when lookupBalanceByBlock - // is disabled and the syncer is behind the current block of - // the node. This error should never occur when - // lookupBalanceByBlock is enabled. - diff := liveBlock.Index - headIndex - if diff < waitToCheckDiff { - time.Sleep(waitToCheckDiffSleep) - continue - } - - // Don't wait to check if we are very far behind - log.Printf( - "Skipping reconciliation for %s: %d blocks behind\n", - simpleAccountCurrency(accountCurrency), - diff, - ) - - // Set a highWaterMark to not accept any new - // reconciliation requests unless they happened - // after this new highWaterMark. - r.highWaterMark = liveBlock.Index - break - } - - if errors.Is(err, ErrBlockGone) { - // Either the block has not been processed in a re-org yet - // or the block was orphaned - break - } - - if errors.Is(err, ErrAccountUpdated) { - // If account was updated, it must be - // enqueued again - break - } - - return err - } - - reconciliationType := activeReconciliation - if inactive { - reconciliationType = inactiveReconciliation - } - - if difference != zeroString { - err := r.handler.ReconciliationFailed( - ctx, - reconciliationType, - accountCurrency.Account, - accountCurrency.Currency, - cachedBalance, - liveAmount, - liveBlock, - ) - if err != nil { - return err - } - - return nil - } - - r.inactiveAccountQueue(inactive, accountCurrency, liveBlock) - return r.handler.ReconciliationSucceeded( - ctx, - reconciliationType, - accountCurrency.Account, - accountCurrency.Currency, - liveAmount, - liveBlock, - ) - } - - return nil -} - -func (r *Reconciler) inactiveAccountQueue( - inactive bool, - accountCurrency *AccountCurrency, - liveBlock *types.BlockIdentifier, -) { - // Only enqueue the first time we see an account on an active reconciliation. - shouldEnqueueInactive := false - if !inactive && !ContainsAccountCurrency(r.seenAccts, accountCurrency) { - r.seenAccts = append(r.seenAccts, accountCurrency) - shouldEnqueueInactive = true - } - - if inactive || shouldEnqueueInactive { - r.inactiveQueueMutex.Lock() - r.inactiveQueue = append(r.inactiveQueue, &parser.BalanceChange{ - Account: accountCurrency.Account, - Currency: accountCurrency.Currency, - Block: liveBlock, - }) - r.inactiveQueueMutex.Unlock() - } -} - -// simpleAccountCurrency returns a string that is a simple -// representation of an AccountCurrency struct. -func simpleAccountCurrency( - accountCurrency *AccountCurrency, -) string { - acctString := accountCurrency.Account.Address - if accountCurrency.Account.SubAccount != nil { - acctString += accountCurrency.Account.SubAccount.Address - } - - acctString += accountCurrency.Currency.Symbol - - return acctString -} - -// reconcileActiveAccounts selects an account -// from the Reconciler account queue and -// reconciles the balance. This is useful -// for detecting if balance changes in operations -// were correct. -func (r *Reconciler) reconcileActiveAccounts( - ctx context.Context, -) error { - for { - select { - case <-ctx.Done(): - return ctx.Err() - case balanceChange := <-r.changeQueue: - if balanceChange.Block.Index < r.highWaterMark { - continue - } - - block, value, err := r.bestBalance( - ctx, - balanceChange.Account, - balanceChange.Currency, - types.ConstructPartialBlockIdentifier(balanceChange.Block), - ) - if err != nil { - return err - } - - err = r.accountReconciliation( - ctx, - balanceChange.Account, - balanceChange.Currency, - value, - block, - false, - ) - if err != nil { - return err - } - } - } -} - -// reconcileInactiveAccounts selects a random account -// from all previously seen accounts and reconciles -// the balance. This is useful for detecting balance -// changes that were not returned in operations. -func (r *Reconciler) reconcileInactiveAccounts( - ctx context.Context, -) error { - for ctx.Err() == nil { - head, err := r.helper.CurrentBlock(ctx) - // When first start syncing, this loop may run before the genesis block is synced. - // If this is the case, we should sleep and try again later instead of exiting. - if err != nil { - time.Sleep(inactiveReconciliationSleep) - log.Printf( - "%s: waiting to start inactive reconciliation until current block set\n", - err.Error(), - ) - continue - } - - r.inactiveQueueMutex.Lock() - if len(r.inactiveQueue) > 0 && - r.inactiveQueue[0].Block.Index+inactiveReconciliationRequiredDepth < head.Index { - randAcct := r.inactiveQueue[0] - r.inactiveQueue = r.inactiveQueue[1:] - r.inactiveQueueMutex.Unlock() - - block, amount, err := r.bestBalance( - ctx, - randAcct.Account, - randAcct.Currency, - types.ConstructPartialBlockIdentifier(head), - ) - if err != nil { - return err - } - - err = r.accountReconciliation( - ctx, - randAcct.Account, - randAcct.Currency, - amount, - block, - true, - ) - if err != nil { - return err - } - } else { - r.inactiveQueueMutex.Unlock() - time.Sleep(inactiveReconciliationSleep) - } - } - - return nil -} - -// Reconcile starts the active and inactive Reconciler goroutines. -// If any goroutine errors, the function will return an error. -func (r *Reconciler) Reconcile(ctx context.Context) error { - g, ctx := errgroup.WithContext(ctx) - for j := uint64(0); j < r.accountConcurrency/2; j++ { - g.Go(func() error { - return r.reconcileActiveAccounts(ctx) - }) - - g.Go(func() error { - return r.reconcileInactiveAccounts(ctx) - }) - } - - if err := g.Wait(); err != nil { - return err - } - - return nil -} - -// ExtractAmount returns the types.Amount from a slice of types.Balance -// pertaining to an AccountAndCurrency. -func ExtractAmount( - balances []*types.Amount, - currency *types.Currency, -) (*types.Amount, error) { - for _, b := range balances { - if !utils.Equal(b.Currency, currency) { - continue - } - - return b, nil - } - - return nil, fmt.Errorf("could not extract amount for %+v", currency) -} - -// AccountCurrency is a simple struct combining -// a *types.Account and *types.Currency. This can -// be useful for looking up balances. -type AccountCurrency struct { - Account *types.AccountIdentifier `json:"account_identifier,omitempty"` - Currency *types.Currency `json:"currency,omitempty"` -} - -// ContainsAccountCurrency returns a boolean indicating if a -// AccountCurrency slice already contains an Account and Currency combination. -func ContainsAccountCurrency( - arr []*AccountCurrency, - change *AccountCurrency, -) bool { - for _, a := range arr { - if utils.Equal(a.Account, change.Account) && - utils.Equal(a.Currency, change.Currency) { - return true - } - } - - return false -} - -// GetCurrencyBalance fetches the balance of a *types.AccountIdentifier -// for a particular *types.Currency. -func GetCurrencyBalance( - ctx context.Context, - fetcher *fetcher.Fetcher, - network *types.NetworkIdentifier, - account *types.AccountIdentifier, - currency *types.Currency, - block *types.PartialBlockIdentifier, -) (*types.BlockIdentifier, string, error) { - liveBlock, liveBalances, _, err := fetcher.AccountBalanceRetry( - ctx, - network, - account, - block, - ) - if err != nil { - return nil, "", err - } - - liveAmount, err := ExtractAmount(liveBalances, currency) - if err != nil { - return nil, "", err - } - - return liveBlock, liveAmount.Value, nil -} diff --git a/internal/reconciler/reconciler_test.go b/internal/reconciler/reconciler_test.go deleted file mode 100644 index 3532b9b6..00000000 --- a/internal/reconciler/reconciler_test.go +++ /dev/null @@ -1,439 +0,0 @@ -// 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. - -package reconciler - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - - "github.com/coinbase/rosetta-sdk-go/types" - "github.com/stretchr/testify/assert" -) - -func TestContainsAccountCurrency(t *testing.T) { - currency1 := &types.Currency{ - Symbol: "Blah", - Decimals: 2, - } - currency2 := &types.Currency{ - Symbol: "Blah2", - Decimals: 2, - } - accts := []*AccountCurrency{ - { - Account: &types.AccountIdentifier{ - Address: "test", - }, - Currency: currency1, - }, - { - Account: &types.AccountIdentifier{ - Address: "cool", - SubAccount: &types.SubAccountIdentifier{ - Address: "test2", - }, - }, - Currency: currency1, - }, - { - Account: &types.AccountIdentifier{ - Address: "cool", - SubAccount: &types.SubAccountIdentifier{ - Address: "test2", - Metadata: map[string]interface{}{ - "neat": "stuff", - }, - }, - }, - Currency: currency1, - }, - } - - t.Run("Non-existent account", func(t *testing.T) { - assert.False(t, ContainsAccountCurrency(accts, &AccountCurrency{ - Account: &types.AccountIdentifier{ - Address: "blah", - }, - Currency: currency1, - })) - }) - - t.Run("Basic account", func(t *testing.T) { - assert.True(t, ContainsAccountCurrency(accts, &AccountCurrency{ - Account: &types.AccountIdentifier{ - Address: "test", - }, - Currency: currency1, - })) - }) - - t.Run("Basic account with bad currency", func(t *testing.T) { - assert.False(t, ContainsAccountCurrency(accts, &AccountCurrency{ - Account: &types.AccountIdentifier{ - Address: "test", - }, - Currency: currency2, - })) - }) - - t.Run("Account with subaccount", func(t *testing.T) { - assert.True(t, ContainsAccountCurrency(accts, &AccountCurrency{ - Account: &types.AccountIdentifier{ - Address: "cool", - SubAccount: &types.SubAccountIdentifier{ - Address: "test2", - }, - }, - Currency: currency1, - })) - }) - - t.Run("Account with subaccount and metadata", func(t *testing.T) { - assert.True(t, ContainsAccountCurrency(accts, &AccountCurrency{ - Account: &types.AccountIdentifier{ - Address: "cool", - SubAccount: &types.SubAccountIdentifier{ - Address: "test2", - Metadata: map[string]interface{}{ - "neat": "stuff", - }, - }, - }, - Currency: currency1, - })) - }) - - t.Run("Account with subaccount and unique metadata", func(t *testing.T) { - assert.False(t, ContainsAccountCurrency(accts, &AccountCurrency{ - Account: &types.AccountIdentifier{ - Address: "cool", - SubAccount: &types.SubAccountIdentifier{ - Address: "test2", - Metadata: map[string]interface{}{ - "neater": "stuff", - }, - }, - }, - Currency: currency1, - })) - }) -} - -func TestExtractAmount(t *testing.T) { - var ( - currency1 = &types.Currency{ - Symbol: "curr1", - Decimals: 4, - } - - currency2 = &types.Currency{ - Symbol: "curr2", - Decimals: 7, - } - - amount1 = &types.Amount{ - Value: "100", - Currency: currency1, - } - - amount2 = &types.Amount{ - Value: "200", - Currency: currency2, - } - - balances = []*types.Amount{ - amount1, - amount2, - } - - badCurr = &types.Currency{ - Symbol: "no curr", - Decimals: 100, - } - ) - - t.Run("Non-existent currency", func(t *testing.T) { - result, err := ExtractAmount(balances, badCurr) - assert.Nil(t, result) - assert.EqualError(t, err, fmt.Errorf("could not extract amount for %+v", badCurr).Error()) - }) - - t.Run("Simple account", func(t *testing.T) { - result, err := ExtractAmount(balances, currency1) - assert.Equal(t, amount1, result) - assert.NoError(t, err) - }) - - t.Run("SubAccount", func(t *testing.T) { - result, err := ExtractAmount(balances, currency2) - assert.Equal(t, amount2, result) - assert.NoError(t, err) - }) -} - -func TestCompareBalance(t *testing.T) { - var ( - account1 = &types.AccountIdentifier{ - Address: "blah", - } - - account2 = &types.AccountIdentifier{ - Address: "blah", - SubAccount: &types.SubAccountIdentifier{ - Address: "sub blah", - }, - } - - currency1 = &types.Currency{ - Symbol: "curr1", - Decimals: 4, - } - - currency2 = &types.Currency{ - Symbol: "curr2", - Decimals: 7, - } - - amount1 = &types.Amount{ - Value: "100", - Currency: currency1, - } - - amount2 = &types.Amount{ - Value: "200", - Currency: currency2, - } - - block0 = &types.BlockIdentifier{ - Hash: "block0", - Index: 0, - } - - block1 = &types.BlockIdentifier{ - Hash: "block1", - Index: 1, - } - - block2 = &types.BlockIdentifier{ - Hash: "block2", - Index: 2, - } - - ctx = context.Background() - - mh = &MockReconcilerHelper{} - ) - - reconciler := NewReconciler( - nil, - mh, - nil, - nil, - 1, - false, - []*AccountCurrency{}, - ) - - t.Run("No head block yet", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount1.Value, - block1, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, "", cachedBalance) - assert.Equal(t, int64(0), headIndex) - assert.Error(t, err) - }) - - // Update head block - mh.HeadBlock = block0 - - t.Run("Live block is ahead of head block", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount1.Value, - block1, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, "", cachedBalance) - assert.Equal(t, int64(0), headIndex) - assert.EqualError(t, err, fmt.Errorf( - "%w live block %d > head block %d", - ErrHeadBlockBehindLive, - 1, - 0, - ).Error()) - }) - - // Update head block - mh.HeadBlock = &types.BlockIdentifier{ - Hash: "hash2", - Index: 2, - } - - t.Run("Live block is not in store", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount1.Value, - block1, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, "", cachedBalance) - assert.Equal(t, int64(2), headIndex) - assert.Contains(t, err.Error(), ErrBlockGone.Error()) - }) - - // Add blocks to store behind head - mh.StoredBlocks = map[string]*types.Block{} - mh.StoredBlocks[block0.Hash] = &types.Block{ - BlockIdentifier: block0, - ParentBlockIdentifier: block0, - } - mh.StoredBlocks[block1.Hash] = &types.Block{ - BlockIdentifier: block1, - ParentBlockIdentifier: block0, - } - mh.StoredBlocks[block2.Hash] = &types.Block{ - BlockIdentifier: block2, - ParentBlockIdentifier: block1, - } - mh.BalanceAccount = account1 - mh.BalanceAmount = amount1 - mh.BalanceBlock = block1 - - t.Run("Account updated after live block", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount1.Value, - block0, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, "", cachedBalance) - assert.Equal(t, int64(2), headIndex) - assert.Contains(t, err.Error(), ErrAccountUpdated.Error()) - }) - - t.Run("Account balance matches", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount1.Value, - block1, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, amount1.Value, cachedBalance) - assert.Equal(t, int64(2), headIndex) - assert.NoError(t, err) - }) - - t.Run("Account balance matches later live block", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount1.Value, - block2, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, amount1.Value, cachedBalance) - assert.Equal(t, int64(2), headIndex) - assert.NoError(t, err) - }) - - t.Run("Balances are not equal", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account1, - currency1, - amount2.Value, - block2, - ) - assert.Equal(t, "-100", difference) - assert.Equal(t, amount1.Value, cachedBalance) - assert.Equal(t, int64(2), headIndex) - assert.NoError(t, err) - }) - - t.Run("Compare balance for non-existent account", func(t *testing.T) { - difference, cachedBalance, headIndex, err := reconciler.CompareBalance( - ctx, - account2, - currency1, - amount2.Value, - block2, - ) - assert.Equal(t, "0", difference) - assert.Equal(t, "", cachedBalance) - assert.Equal(t, int64(2), headIndex) - assert.Error(t, err) - }) -} - -type MockReconcilerHelper struct { - HeadBlock *types.BlockIdentifier - StoredBlocks map[string]*types.Block - - BalanceAccount *types.AccountIdentifier - BalanceAmount *types.Amount - BalanceBlock *types.BlockIdentifier -} - -func (h *MockReconcilerHelper) BlockExists( - ctx context.Context, - block *types.BlockIdentifier, -) (bool, error) { - _, ok := h.StoredBlocks[block.Hash] - if !ok { - return false, nil - } - - return true, nil -} - -func (h *MockReconcilerHelper) CurrentBlock( - ctx context.Context, -) (*types.BlockIdentifier, error) { - if h.HeadBlock == nil { - return nil, errors.New("head block is nil") - } - - return h.HeadBlock, nil -} - -func (h *MockReconcilerHelper) AccountBalance( - ctx context.Context, - account *types.AccountIdentifier, - currency *types.Currency, - headBlock *types.BlockIdentifier, -) (*types.Amount, *types.BlockIdentifier, error) { - if h.BalanceAccount == nil || !reflect.DeepEqual(account, h.BalanceAccount) { - return nil, nil, errors.New("account does not exist") - } - - return h.BalanceAmount, h.BalanceBlock, nil -} diff --git a/internal/storage/block_storage.go b/internal/storage/block_storage.go index 8a78f2c4..c07e8db5 100644 --- a/internal/storage/block_storage.go +++ b/internal/storage/block_storage.go @@ -27,10 +27,9 @@ import ( "math/big" "path" - "github.com/coinbase/rosetta-cli/internal/syncer" - "github.com/coinbase/rosetta-sdk-go/asserter" "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/syncer" "github.com/coinbase/rosetta-sdk-go/types" ) diff --git a/internal/storage/block_storage_test.go b/internal/storage/block_storage_test.go index 302a032d..10656acb 100644 --- a/internal/storage/block_storage_test.go +++ b/internal/storage/block_storage_test.go @@ -24,11 +24,11 @@ import ( "path" "testing" - "github.com/coinbase/rosetta-cli/internal/reconciler" "github.com/coinbase/rosetta-cli/internal/utils" "github.com/coinbase/rosetta-sdk-go/asserter" "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/reconciler" "github.com/coinbase/rosetta-sdk-go/types" "github.com/stretchr/testify/assert" ) diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go deleted file mode 100644 index dd62b976..00000000 --- a/internal/syncer/syncer.go +++ /dev/null @@ -1,321 +0,0 @@ -// 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. - -package syncer - -import ( - "context" - "errors" - "fmt" - "log" - "time" - - "github.com/coinbase/rosetta-cli/internal/utils" - - "github.com/coinbase/rosetta-sdk-go/fetcher" - "github.com/coinbase/rosetta-sdk-go/types" -) - -const ( - // maxSync is the maximum number of blocks - // to try and sync in a given SyncCycle. - maxSync = 999 - - // PastBlockSize is the maximum number of previously - // processed blocks we keep in the syncer to handle - // reorgs correctly. If there is a reorg greater than - // PastBlockSize, it will not be handled correctly. - // - // TODO: make configurable - PastBlockSize = 20 -) - -var ( - // defaultSyncSleep is the amount of time to sleep - // when we are at tip but want to keep syncing. - defaultSyncSleep = 5 * time.Second -) - -// Handler is called at various times during the sync cycle -// to handle different events. It is common to write logs or -// perform reconciliation in the sync processor. -type Handler interface { - BlockAdded( - ctx context.Context, - block *types.Block, - ) error - - BlockRemoved( - ctx context.Context, - block *types.BlockIdentifier, - ) error -} - -// Syncer coordinates blockchain syncing without relying on -// a storage interface. Instead, it calls a provided Handler -// whenever a block is added or removed. This provides the client -// the opportunity to define the logic used to handle each new block. -// In the rosetta-cli, we handle reconciliation, state storage, and -// logging in the handler. -type Syncer struct { - network *types.NetworkIdentifier - fetcher *fetcher.Fetcher - handler Handler - cancel context.CancelFunc - - // Used to keep track of sync state - genesisBlock *types.BlockIdentifier - nextIndex int64 - - // To ensure reorgs are handled correctly, the syncer must be able - // to observe blocks it has previously processed. Without this, the - // syncer may process an index that is not connected to previously added - // blocks (ParentBlockIdentifier != lastProcessedBlock.BlockIdentifier). - // - // If a blockchain does not have reorgs, it is not necessary to populate - // the blockCache on creation. - pastBlocks []*types.BlockIdentifier -} - -// New creates a new Syncer. If pastBlocks is left nil, it will -// be set to an empty slice. -func New( - network *types.NetworkIdentifier, - fetcher *fetcher.Fetcher, - handler Handler, - cancel context.CancelFunc, - pastBlocks []*types.BlockIdentifier, -) *Syncer { - past := pastBlocks - if past == nil { - past = []*types.BlockIdentifier{} - } - - return &Syncer{ - network: network, - fetcher: fetcher, - handler: handler, - cancel: cancel, - pastBlocks: past, - } -} - -func (s *Syncer) setStart( - ctx context.Context, - index int64, -) error { - networkStatus, err := s.fetcher.NetworkStatusRetry( - ctx, - s.network, - nil, - ) - if err != nil { - return err - } - - s.genesisBlock = networkStatus.GenesisBlockIdentifier - - if index != -1 { - s.nextIndex = index - return nil - } - - s.nextIndex = networkStatus.GenesisBlockIdentifier.Index - return nil -} - -// nextSyncableRange returns the next range of indexes to sync -// based on what the last processed block in storage is and -// the contents of the network status response. -func (s *Syncer) nextSyncableRange( - ctx context.Context, - endIndex int64, -) (int64, bool, error) { - if s.nextIndex == -1 { - return -1, false, errors.New("unable to get current head") - } - - if endIndex == -1 { - networkStatus, err := s.fetcher.NetworkStatusRetry( - ctx, - s.network, - nil, - ) - if err != nil { - return -1, false, fmt.Errorf("%w: unable to get network status", err) - } - - endIndex = networkStatus.CurrentBlockIdentifier.Index - } - - if s.nextIndex >= endIndex { - return -1, true, nil - } - - if endIndex-s.nextIndex > maxSync { - endIndex = s.nextIndex + maxSync - } - - return endIndex, false, nil -} - -func (s *Syncer) checkRemove( - block *types.Block, -) (bool, *types.BlockIdentifier, error) { - if len(s.pastBlocks) == 0 { - return false, nil, nil - } - - // Ensure processing correct index - if block.BlockIdentifier.Index != s.nextIndex { - return false, nil, fmt.Errorf( - "Got block %d instead of %d", - block.BlockIdentifier.Index, - s.nextIndex, - ) - } - - // Check if block parent is head - lastBlock := s.pastBlocks[len(s.pastBlocks)-1] - if !utils.Equal(block.ParentBlockIdentifier, lastBlock) { - if utils.Equal(s.genesisBlock, lastBlock) { - return false, nil, fmt.Errorf("cannot remove genesis block") - } - - return true, lastBlock, nil - } - - return false, lastBlock, nil -} - -func (s *Syncer) processBlock( - ctx context.Context, - block *types.Block, -) error { - shouldRemove, lastBlock, err := s.checkRemove(block) - if err != nil { - return err - } - - if shouldRemove { - err = s.handler.BlockRemoved(ctx, lastBlock) - if err != nil { - return err - } - s.pastBlocks = s.pastBlocks[:len(s.pastBlocks)-1] - s.nextIndex = lastBlock.Index - return nil - } - - err = s.handler.BlockAdded(ctx, block) - if err != nil { - return err - } - - s.pastBlocks = append(s.pastBlocks, block.BlockIdentifier) - if len(s.pastBlocks) > PastBlockSize { - s.pastBlocks = s.pastBlocks[1:] - } - s.nextIndex = block.BlockIdentifier.Index + 1 - return nil -} - -func (s *Syncer) syncRange( - ctx context.Context, - endIndex int64, -) error { - blockMap, err := s.fetcher.BlockRange(ctx, s.network, s.nextIndex, endIndex) - if err != nil { - return err - } - - for s.nextIndex <= endIndex { - block, ok := blockMap[s.nextIndex] - if !ok { // could happen in a reorg - block, err = s.fetcher.BlockRetry( - ctx, - s.network, - &types.PartialBlockIdentifier{ - Index: &s.nextIndex, - }, - ) - if err != nil { - return err - } - } else { - // Anytime we re-fetch an index, we - // will need to make another call to the node - // as it is likely in a reorg. - delete(blockMap, s.nextIndex) - } - - if err = s.processBlock(ctx, block); err != nil { - return err - } - } - - return nil -} - -// Sync cycles endlessly until there is an error -// or the requested range is synced. -func (s *Syncer) Sync( - ctx context.Context, - startIndex int64, - endIndex int64, -) error { - defer s.cancel() - - if err := s.setStart(ctx, startIndex); err != nil { - return fmt.Errorf("%w: unable to set start index", err) - } - - for { - rangeEnd, halt, err := s.nextSyncableRange( - ctx, - endIndex, - ) - if err != nil { - return fmt.Errorf("%w: unable to get next syncable range", err) - } - - if halt { - if endIndex != -1 { - break - } - - log.Printf("Syncer at tip %d...sleeping\n", s.nextIndex) - time.Sleep(defaultSyncSleep) - continue - } - - log.Printf("Syncing %d-%d\n", s.nextIndex, rangeEnd) - - err = s.syncRange(ctx, rangeEnd) - if err != nil { - return fmt.Errorf("%w: unable to sync to %d", err, rangeEnd) - } - - if ctx.Err() != nil { - return ctx.Err() - } - } - - if startIndex == -1 { - startIndex = s.genesisBlock.Index - } - - log.Printf("Finished syncing %d-%d\n", startIndex, endIndex) - return nil -} diff --git a/internal/syncer/syncer_test.go b/internal/syncer/syncer_test.go deleted file mode 100644 index c7d8fcef..00000000 --- a/internal/syncer/syncer_test.go +++ /dev/null @@ -1,332 +0,0 @@ -// 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. - -package syncer - -import ( - "context" - "testing" - - "github.com/coinbase/rosetta-sdk-go/types" - - "github.com/stretchr/testify/assert" -) - -var ( - networkIdentifier = &types.NetworkIdentifier{ - Blockchain: "blah", - Network: "testnet", - } - - currency = &types.Currency{ - Symbol: "Blah", - Decimals: 2, - } - - recipient = &types.AccountIdentifier{ - Address: "acct1", - } - - recipientAmount = &types.Amount{ - Value: "100", - Currency: currency, - } - - recipientOperation = &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, - }, - Type: "Transfer", - Status: "Success", - Account: recipient, - Amount: recipientAmount, - } - - recipientFailureOperation = &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: 1, - }, - Type: "Transfer", - Status: "Failure", - Account: recipient, - Amount: recipientAmount, - } - - recipientTransaction = &types.Transaction{ - TransactionIdentifier: &types.TransactionIdentifier{ - Hash: "tx1", - }, - Operations: []*types.Operation{ - recipientOperation, - recipientFailureOperation, - }, - } - - sender = &types.AccountIdentifier{ - Address: "acct2", - } - - senderAmount = &types.Amount{ - Value: "-100", - Currency: currency, - } - - senderOperation = &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, - }, - Type: "Transfer", - Status: "Success", - Account: sender, - Amount: senderAmount, - } - - senderTransaction = &types.Transaction{ - TransactionIdentifier: &types.TransactionIdentifier{ - Hash: "tx2", - }, - Operations: []*types.Operation{ - senderOperation, - }, - } - - orphanGenesis = &types.Block{ - BlockIdentifier: &types.BlockIdentifier{ - Hash: "1", - Index: 1, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "0a", - Index: 0, - }, - Transactions: []*types.Transaction{}, - } - - blockSequence = []*types.Block{ - { // genesis - BlockIdentifier: &types.BlockIdentifier{ - Hash: "0", - Index: 0, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "0", - Index: 0, - }, - }, - { - BlockIdentifier: &types.BlockIdentifier{ - Hash: "1", - Index: 1, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "0", - Index: 0, - }, - Transactions: []*types.Transaction{ - recipientTransaction, - }, - }, - { // reorg - BlockIdentifier: &types.BlockIdentifier{ - Hash: "2", - Index: 2, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "1a", - Index: 1, - }, - }, - { - BlockIdentifier: &types.BlockIdentifier{ - Hash: "1a", - Index: 1, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "0", - Index: 0, - }, - }, - { - BlockIdentifier: &types.BlockIdentifier{ - Hash: "3", - Index: 3, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "2", - Index: 2, - }, - Transactions: []*types.Transaction{ - senderTransaction, - }, - }, - { // invalid block - BlockIdentifier: &types.BlockIdentifier{ - Hash: "5", - Index: 5, - }, - ParentBlockIdentifier: &types.BlockIdentifier{ - Hash: "4", - Index: 4, - }, - }, - } -) - -func lastBlockIdentifier(syncer *Syncer) *types.BlockIdentifier { - return syncer.pastBlocks[len(syncer.pastBlocks)-1] -} - -func TestProcessBlock(t *testing.T) { - ctx := context.Background() - - syncer := New(networkIdentifier, nil, &MockSyncHandler{}, nil, nil) - syncer.genesisBlock = blockSequence[0].BlockIdentifier - - t.Run("No block exists", func(t *testing.T) { - assert.Equal( - t, - []*types.BlockIdentifier{}, - syncer.pastBlocks, - ) - err := syncer.processBlock( - ctx, - blockSequence[0], - ) - assert.NoError(t, err) - assert.Equal(t, int64(1), syncer.nextIndex) - assert.Equal(t, blockSequence[0].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{blockSequence[0].BlockIdentifier}, - syncer.pastBlocks, - ) - }) - - t.Run("Orphan genesis", func(t *testing.T) { - err := syncer.processBlock( - ctx, - orphanGenesis, - ) - - assert.EqualError(t, err, "cannot remove genesis block") - assert.Equal(t, int64(1), syncer.nextIndex) - assert.Equal(t, blockSequence[0].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{blockSequence[0].BlockIdentifier}, - syncer.pastBlocks, - ) - }) - - t.Run("Block exists, no reorg", func(t *testing.T) { - err := syncer.processBlock( - ctx, - blockSequence[1], - ) - assert.NoError(t, err) - assert.Equal(t, int64(2), syncer.nextIndex) - assert.Equal(t, blockSequence[1].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{ - blockSequence[0].BlockIdentifier, - blockSequence[1].BlockIdentifier, - }, - syncer.pastBlocks, - ) - }) - - t.Run("Orphan block", func(t *testing.T) { - err := syncer.processBlock( - ctx, - blockSequence[2], - ) - assert.NoError(t, err) - assert.Equal(t, int64(1), syncer.nextIndex) - assert.Equal(t, blockSequence[0].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{blockSequence[0].BlockIdentifier}, - syncer.pastBlocks, - ) - - err = syncer.processBlock( - ctx, - blockSequence[3], - ) - assert.NoError(t, err) - assert.Equal(t, int64(2), syncer.nextIndex) - assert.Equal(t, blockSequence[3].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{ - blockSequence[0].BlockIdentifier, - blockSequence[3].BlockIdentifier, - }, - syncer.pastBlocks, - ) - - err = syncer.processBlock( - ctx, - blockSequence[2], - ) - assert.NoError(t, err) - assert.Equal(t, int64(3), syncer.nextIndex) - assert.Equal(t, blockSequence[2].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{ - blockSequence[0].BlockIdentifier, - blockSequence[3].BlockIdentifier, - blockSequence[2].BlockIdentifier, - }, - syncer.pastBlocks, - ) - }) - - t.Run("Out of order block", func(t *testing.T) { - err := syncer.processBlock( - ctx, - blockSequence[5], - ) - assert.EqualError(t, err, "Got block 5 instead of 3") - assert.Equal(t, int64(3), syncer.nextIndex) - assert.Equal(t, blockSequence[2].BlockIdentifier, lastBlockIdentifier(syncer)) - assert.Equal( - t, - []*types.BlockIdentifier{ - blockSequence[0].BlockIdentifier, - blockSequence[3].BlockIdentifier, - blockSequence[2].BlockIdentifier, - }, - syncer.pastBlocks, - ) - }) -} - -type MockSyncHandler struct{} - -func (h *MockSyncHandler) BlockAdded( - ctx context.Context, - block *types.Block, -) error { - return nil -} - -func (h *MockSyncHandler) BlockRemoved( - ctx context.Context, - block *types.BlockIdentifier, -) error { - return nil -}