lotus/chain/sync.go

655 lines
17 KiB
Go
Raw Normal View History

2019-07-05 14:29:17 +00:00
package chain
import (
"context"
"fmt"
"sync"
2019-07-26 18:16:57 +00:00
"golang.org/x/xerrors"
2019-07-12 23:52:25 +00:00
"github.com/filecoin-project/go-lotus/chain/actors"
"github.com/filecoin-project/go-lotus/chain/address"
2019-07-26 04:54:22 +00:00
"github.com/filecoin-project/go-lotus/chain/store"
"github.com/filecoin-project/go-lotus/chain/types"
2019-07-26 04:54:22 +00:00
"github.com/filecoin-project/go-lotus/chain/vm"
"github.com/filecoin-project/go-lotus/lib/vdf"
2019-07-08 12:51:45 +00:00
2019-07-05 14:29:17 +00:00
"github.com/ipfs/go-cid"
dstore "github.com/ipfs/go-datastore"
"github.com/ipfs/go-hamt-ipld"
bstore "github.com/ipfs/go-ipfs-blockstore"
2019-07-26 04:54:22 +00:00
logging "github.com/ipfs/go-log"
2019-07-30 16:39:07 +00:00
"github.com/libp2p/go-libp2p-core/peer"
2019-07-05 14:29:17 +00:00
"github.com/pkg/errors"
"github.com/whyrusleeping/sharray"
)
2019-07-26 04:54:22 +00:00
const ForkLengthThreshold = 20
var log = logging.Logger("chain")
2019-07-05 14:29:17 +00:00
type Syncer struct {
// The heaviest known tipset in the network.
// The interface for accessing and putting tipsets into local storage
2019-07-26 04:54:22 +00:00
store *store.ChainStore
2019-07-05 14:29:17 +00:00
2019-07-05 14:46:21 +00:00
// The known Genesis tipset
2019-07-26 04:54:22 +00:00
Genesis *types.TipSet
2019-07-05 14:29:17 +00:00
syncLock sync.Mutex
// TipSets known to be invalid
bad BadTipSetCache
// handle to the block sync service
2019-07-08 14:07:09 +00:00
Bsync *BlockSync
2019-07-05 14:29:17 +00:00
2019-07-11 02:36:43 +00:00
self peer.ID
2019-07-05 14:29:17 +00:00
// peer heads
// Note: clear cache on disconnects
2019-07-26 04:54:22 +00:00
peerHeads map[peer.ID]*types.TipSet
2019-07-05 14:29:17 +00:00
peerHeadsLk sync.Mutex
}
2019-07-26 04:54:22 +00:00
func NewSyncer(cs *store.ChainStore, bsync *BlockSync, self peer.ID) (*Syncer, error) {
2019-07-05 14:29:17 +00:00
gen, err := cs.GetGenesis()
if err != nil {
return nil, err
}
2019-07-26 04:54:22 +00:00
gent, err := types.NewTipSet([]*types.BlockHeader{gen})
2019-07-05 14:29:17 +00:00
if err != nil {
return nil, err
}
return &Syncer{
2019-07-05 14:46:21 +00:00
Genesis: gent,
2019-07-08 14:07:09 +00:00
Bsync: bsync,
2019-07-26 04:54:22 +00:00
peerHeads: make(map[peer.ID]*types.TipSet),
store: cs,
self: self,
2019-07-05 14:29:17 +00:00
}, nil
}
type BadTipSetCache struct {
badBlocks map[cid.Cid]struct{}
}
const BootstrapPeerThreshold = 1
// InformNewHead informs the syncer about a new potential tipset
// This should be called when connecting to new peers, and additionally
// when receiving new blocks from the network
2019-07-26 04:54:22 +00:00
func (syncer *Syncer) InformNewHead(from peer.ID, fts *store.FullTipSet) {
2019-07-05 14:29:17 +00:00
if fts == nil {
panic("bad")
}
2019-07-11 02:36:43 +00:00
if from == syncer.self {
// TODO: this is kindof a hack...
log.Infof("got block from ourselves")
2019-07-30 16:39:07 +00:00
if err := syncer.Sync(fts); err != nil {
2019-07-11 02:36:43 +00:00
log.Errorf("failed to sync our own block: %s", err)
}
return
}
2019-07-05 14:29:17 +00:00
syncer.peerHeadsLk.Lock()
syncer.peerHeads[from] = fts.TipSet()
syncer.peerHeadsLk.Unlock()
2019-07-08 14:07:09 +00:00
syncer.Bsync.AddPeer(from)
2019-07-05 14:29:17 +00:00
go func() {
2019-07-30 16:39:07 +00:00
if err := syncer.Sync(fts); err != nil {
2019-07-26 18:16:57 +00:00
log.Errorf("sync error: %s", err)
2019-07-05 14:29:17 +00:00
}
}()
}
func (syncer *Syncer) InformNewBlock(from peer.ID, blk *types.FullBlock) {
2019-07-05 14:29:17 +00:00
// TODO: search for other blocks that could form a tipset with this block
// and then send that tipset to InformNewHead
2019-07-26 04:54:22 +00:00
fts := &store.FullTipSet{Blocks: []*types.FullBlock{blk}}
2019-07-05 14:29:17 +00:00
syncer.InformNewHead(from, fts)
}
2019-07-26 04:54:22 +00:00
func reverse(tips []*types.TipSet) []*types.TipSet {
out := make([]*types.TipSet, len(tips))
2019-07-05 14:29:17 +00:00
for i := 0; i < len(tips); i++ {
out[i] = tips[len(tips)-(i+1)]
}
return out
}
func copyBlockstore(from, to bstore.Blockstore) error {
cids, err := from.AllKeysChan(context.TODO())
if err != nil {
return err
}
for c := range cids {
b, err := from.Get(c)
if err != nil {
return err
}
if err := to.Put(b); err != nil {
return err
}
}
return nil
}
// TODO: this function effectively accepts unchecked input from the network,
// either validate it here, or ensure that its validated elsewhere (maybe make
// sure the blocksync code checks it?)
// maybe this code should actually live in blocksync??
2019-08-22 01:29:19 +00:00
func zipTipSetAndMessages(cst *hamt.CborIpldStore, ts *types.TipSet, allbmsgs []*types.Message, allsmsgs []*types.SignedMessage, bmi, smi [][]uint64) (*store.FullTipSet, error) {
if len(ts.Blocks()) != len(smi) || len(ts.Blocks()) != len(bmi) {
2019-07-05 14:29:17 +00:00
return nil, fmt.Errorf("msgincl length didnt match tipset size")
}
2019-07-26 04:54:22 +00:00
fts := &store.FullTipSet{}
2019-07-05 14:29:17 +00:00
for bi, b := range ts.Blocks() {
var smsgs []*types.SignedMessage
var smsgCids []interface{}
for _, m := range smi[bi] {
smsgs = append(smsgs, allsmsgs[m])
smsgCids = append(smsgCids, allsmsgs[m].Cid())
2019-07-05 14:29:17 +00:00
}
smroot, err := sharray.Build(context.TODO(), 4, smsgCids, cst)
2019-07-05 14:29:17 +00:00
if err != nil {
return nil, err
}
var bmsgs []*types.Message
var bmsgCids []interface{}
for _, m := range bmi[bi] {
bmsgs = append(bmsgs, allbmsgs[m])
bmsgCids = append(bmsgCids, allbmsgs[m].Cid())
}
bmroot, err := sharray.Build(context.TODO(), 4, bmsgCids, cst)
if err != nil {
return nil, err
}
mrcid, err := cst.Put(context.TODO(), &types.MsgMeta{
BlsMessages: bmroot,
SecpkMessages: smroot,
})
if err != nil {
return nil, err
}
if b.Messages != mrcid {
2019-07-05 14:29:17 +00:00
return nil, fmt.Errorf("messages didnt match message root in header")
}
fb := &types.FullBlock{
Header: b,
BlsMessages: bmsgs,
SecpkMessages: smsgs,
2019-07-05 14:29:17 +00:00
}
fts.Blocks = append(fts.Blocks, fb)
}
return fts, nil
}
2019-07-26 04:54:22 +00:00
func (syncer *Syncer) selectHead(heads map[peer.ID]*types.TipSet) (*types.TipSet, error) {
var headsArr []*types.TipSet
2019-07-05 14:29:17 +00:00
for _, ts := range heads {
headsArr = append(headsArr, ts)
}
sel := headsArr[0]
for i := 1; i < len(headsArr); i++ {
cur := headsArr[i]
yes, err := syncer.store.IsAncestorOf(cur, sel)
if err != nil {
return nil, err
}
if yes {
continue
}
yes, err = syncer.store.IsAncestorOf(sel, cur)
if err != nil {
return nil, err
}
if yes {
sel = cur
continue
}
nca, err := syncer.store.NearestCommonAncestor(cur, sel)
if err != nil {
return nil, err
}
if sel.Height()-nca.Height() > ForkLengthThreshold {
// TODO: handle this better than refusing to sync
return nil, fmt.Errorf("Conflict exists in heads set")
}
if syncer.store.Weight(cur) > syncer.store.Weight(sel) {
sel = cur
}
}
return sel, nil
}
2019-07-26 04:54:22 +00:00
func (syncer *Syncer) FetchTipSet(ctx context.Context, p peer.ID, cids []cid.Cid) (*store.FullTipSet, error) {
2019-07-05 14:29:17 +00:00
if fts, err := syncer.tryLoadFullTipSet(cids); err == nil {
return fts, nil
}
2019-07-08 14:07:09 +00:00
return syncer.Bsync.GetFullTipSet(ctx, p, cids)
2019-07-05 14:29:17 +00:00
}
2019-07-26 04:54:22 +00:00
func (syncer *Syncer) tryLoadFullTipSet(cids []cid.Cid) (*store.FullTipSet, error) {
2019-07-05 14:29:17 +00:00
ts, err := syncer.store.LoadTipSet(cids)
if err != nil {
return nil, err
}
2019-07-26 04:54:22 +00:00
fts := &store.FullTipSet{}
2019-07-05 14:29:17 +00:00
for _, b := range ts.Blocks() {
bmsgs, smsgs, err := syncer.store.MessagesForBlock(b)
2019-07-05 14:29:17 +00:00
if err != nil {
return nil, err
}
fb := &types.FullBlock{
Header: b,
BlsMessages: bmsgs,
SecpkMessages: smsgs,
2019-07-05 14:29:17 +00:00
}
fts.Blocks = append(fts.Blocks, fb)
}
return fts, nil
}
2019-07-30 16:39:07 +00:00
func (syncer *Syncer) Sync(maybeHead *store.FullTipSet) error {
2019-07-26 18:16:57 +00:00
syncer.syncLock.Lock()
defer syncer.syncLock.Unlock()
2019-07-05 14:29:17 +00:00
ts := maybeHead.TipSet()
if syncer.Genesis.Equals(ts) || syncer.store.GetHeaviestTipSet().Equals(ts) {
2019-07-05 14:29:17 +00:00
return nil
}
2019-07-31 07:13:49 +00:00
if err := syncer.collectChain(maybeHead); err != nil {
return xerrors.Errorf("collectChain failed: %w", err)
2019-07-05 14:29:17 +00:00
}
if err := syncer.store.PutTipSet(maybeHead); err != nil {
return errors.Wrap(err, "failed to put synced tipset to chainstore")
}
return nil
}
func (syncer *Syncer) ValidateTipSet(ctx context.Context, fts *store.FullTipSet) error {
2019-07-05 14:29:17 +00:00
ts := fts.TipSet()
2019-07-05 14:46:21 +00:00
if ts.Equals(syncer.Genesis) {
2019-07-05 14:29:17 +00:00
return nil
}
for _, b := range fts.Blocks {
if err := syncer.ValidateBlock(ctx, b); err != nil {
2019-07-05 14:29:17 +00:00
return err
}
}
return nil
}
func (syncer *Syncer) minerIsValid(ctx context.Context, maddr address.Address, baseTs *types.TipSet) error {
var err error
enc, err := actors.SerializeParams(&actors.IsMinerParam{Addr: maddr})
if err != nil {
return err
}
ret, err := vm.Call(ctx, syncer.store, &types.Message{
To: actors.StorageMarketAddress,
From: maddr,
Method: actors.SMAMethods.IsMiner,
Params: enc,
}, baseTs)
if err != nil {
return xerrors.Errorf("checking if block miner is valid failed: %w", err)
}
if ret.ExitCode != 0 {
return xerrors.Errorf("StorageMarket.IsMiner check failed (exit code %d)", ret.ExitCode)
}
// TODO: ensure the miner is currently not late on their PoSt submission (this hasnt landed in the spec yet)
return nil
}
func (syncer *Syncer) validateTickets(ctx context.Context, mworker address.Address, tickets []*types.Ticket, base *types.TipSet) error {
if len(tickets) == 0 {
return xerrors.Errorf("block had no tickets")
}
cur := base.MinTicket()
for i := 0; i < len(tickets); i++ {
next := tickets[i]
sig := &types.Signature{
Type: types.KTBLS,
Data: next.VRFProof,
}
log.Infof("About to verify signature: ", sig.Data, mworker, cur.VDFResult)
if err := sig.Verify(mworker, cur.VDFResult); err != nil {
return xerrors.Errorf("invalid ticket, VRFProof invalid: %w", err)
}
// now verify the VDF
if err := vdf.Verify(next.VRFProof, next.VDFResult, next.VDFProof); err != nil {
return xerrors.Errorf("ticket %d had invalid VDF: %w", err)
}
cur = next
}
return nil
}
func getMinerWorker(ctx context.Context, cs *store.ChainStore, st cid.Cid, maddr address.Address) (address.Address, error) {
recp, err := vm.CallRaw(ctx, cs, &types.Message{
To: maddr,
From: maddr,
Method: actors.MAMethods.GetWorkerAddr,
}, st, 0)
if err != nil {
return address.Undef, xerrors.Errorf("callRaw failed: %w", err)
}
if recp.ExitCode != 0 {
return address.Undef, xerrors.Errorf("getting miner worker addr failed (exit code %d)", recp.ExitCode)
}
worker, err := address.NewFromBytes(recp.Return)
if err != nil {
return address.Undef, err
}
if worker.Protocol() == address.ID {
return address.Undef, xerrors.Errorf("need to resolve worker address to a pubkeyaddr")
}
return worker, nil
}
// Should match up with 'Semantical Validation' in validation.md in the spec
func (syncer *Syncer) ValidateBlock(ctx context.Context, b *types.FullBlock) error {
2019-07-05 14:29:17 +00:00
h := b.Header
stateroot, err := syncer.store.TipSetState(h.Parents)
if err != nil {
return xerrors.Errorf("get tipsetstate(%d, %s) failed: %w", h.Height, h.Parents, err)
2019-07-05 14:29:17 +00:00
}
baseTs, err := syncer.store.LoadTipSet(h.Parents)
2019-07-05 14:29:17 +00:00
if err != nil {
return xerrors.Errorf("load tipset failed: %w", err)
2019-07-05 14:29:17 +00:00
}
if err := syncer.minerIsValid(ctx, h.Miner, baseTs); err != nil {
return xerrors.Errorf("minerIsValid failed: %w", err)
}
waddr, err := getMinerWorker(ctx, syncer.store, stateroot, h.Miner)
if err != nil {
return xerrors.Errorf("getMinerWorker failed: %w", err)
}
if err := syncer.validateTickets(ctx, waddr, h.Tickets, baseTs); err != nil {
return xerrors.Errorf("validating block tickets failed: %w", err)
}
vmi, err := vm.NewVM(stateroot, h.Height, h.Miner, syncer.store)
2019-07-05 14:29:17 +00:00
if err != nil {
return xerrors.Errorf("failed to instantiate VM: %w", err)
2019-07-05 14:29:17 +00:00
}
2019-07-25 22:15:33 +00:00
if err := vmi.TransferFunds(actors.NetworkAddress, b.Header.Miner, vm.MiningRewardForBlock(baseTs)); err != nil {
return xerrors.Errorf("fund transfer failed: %w", err)
2019-07-05 14:29:17 +00:00
}
var receipts []interface{}
for i, m := range b.BlsMessages {
receipt, err := vmi.ApplyMessage(ctx, m)
if err != nil {
return xerrors.Errorf("failed executing bls message %d in block %s: %w", i, b.Header.Cid(), err)
}
receipts = append(receipts, receipt.MessageReceipt)
}
for i, m := range b.SecpkMessages {
2019-07-25 22:15:33 +00:00
receipt, err := vmi.ApplyMessage(ctx, &m.Message)
2019-07-05 14:29:17 +00:00
if err != nil {
return xerrors.Errorf("failed executing secpk message %d in block %s: %w", i, b.Header.Cid(), err)
2019-07-05 14:29:17 +00:00
}
receipts = append(receipts, receipt.MessageReceipt)
2019-07-05 14:29:17 +00:00
}
2019-07-26 04:54:22 +00:00
cst := hamt.CSTFromBstore(syncer.store.Blockstore())
2019-07-05 14:29:17 +00:00
recptRoot, err := sharray.Build(context.TODO(), 4, receipts, cst)
if err != nil {
return xerrors.Errorf("building receipts sharray failed: %w", err)
2019-07-05 14:29:17 +00:00
}
if recptRoot != b.Header.MessageReceipts {
return fmt.Errorf("receipts mismatched")
}
2019-07-25 22:15:33 +00:00
final, err := vmi.Flush(context.TODO())
2019-07-05 14:29:17 +00:00
if err != nil {
return xerrors.Errorf("failed to flush VM state: %w", err)
2019-07-05 14:29:17 +00:00
}
if b.Header.StateRoot != final {
return fmt.Errorf("final state root does not match block")
}
return nil
}
func (syncer *Syncer) collectHeaders(from *types.TipSet, to *types.TipSet) ([]*types.TipSet, error) {
2019-07-31 07:13:49 +00:00
blockSet := []*types.TipSet{from}
2019-07-05 14:29:17 +00:00
2019-07-31 07:13:49 +00:00
at := from.Parents()
2019-07-05 14:29:17 +00:00
// we want to sync all the blocks until the height above the block we have
untilHeight := to.Height() + 1
2019-07-31 07:13:49 +00:00
// If, for some reason, we have a suffix of the chain locally, handle that here
for blockSet[len(blockSet)-1].Height() > untilHeight {
2019-07-31 07:13:49 +00:00
log.Warn("syncing local: ", at)
ts, err := syncer.store.LoadTipSet(at)
if err != nil {
if err == bstore.ErrNotFound {
log.Info("tipset not found locally, starting sync: ", at)
break
}
log.Warn("loading local tipset: %s", err)
continue // TODO: verify
}
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
blockSet = append(blockSet, ts)
at = ts.Parents()
}
for blockSet[len(blockSet)-1].Height() > untilHeight {
2019-07-31 07:13:49 +00:00
// NB: GetBlocks validates that the blocks are in-fact the ones we
// requested, and that they are correctly linked to eachother. It does
// not validate any state transitions
fmt.Println("Get blocks")
blks, err := syncer.Bsync.GetBlocks(context.TODO(), at, 10)
if err != nil {
// Most likely our peers aren't fully synced yet, but forwarded
// new block message (ideally we'd find better peers)
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
log.Error("failed to get blocks: ", err)
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
// This error will only be logged above,
return nil, xerrors.Errorf("failed to get blocks: %w", err)
}
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
for _, b := range blks {
if b.Height() < untilHeight {
break
}
2019-07-31 07:13:49 +00:00
blockSet = append(blockSet, b)
}
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
at = blks[len(blks)-1].Parents()
}
if !cidArrsEqual(blockSet[len(blockSet)-1].Parents(), to.Cids()) {
// TODO: handle the case where we are on a fork and its not a simple fast forward
return nil, xerrors.Errorf("synced header chain does not link to our best block")
2019-07-31 07:13:49 +00:00
}
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
return blockSet, nil
}
2019-07-26 18:16:57 +00:00
2019-07-31 07:13:49 +00:00
func (syncer *Syncer) syncMessagesAndCheckState(headers []*types.TipSet) error {
return syncer.iterFullTipsets(headers, func(fts *store.FullTipSet) error {
log.Warn("validating tipset: ", fts.TipSet().Height())
if err := syncer.ValidateTipSet(context.TODO(), fts); err != nil {
log.Errorf("failed to validate tipset: %s", err)
return xerrors.Errorf("message processing failed: %w", err)
}
return nil
})
}
// fills out each of the given tipsets with messages and calls the callback with it
func (syncer *Syncer) iterFullTipsets(headers []*types.TipSet, cb func(*store.FullTipSet) error) error {
beg := len(headers) - 1
// handle case where we have a prefix of these locally
for ; beg >= 0; beg-- {
fts, err := syncer.store.TryFillTipSet(headers[beg])
if err != nil {
return err
}
if fts == nil {
break
}
if err := cb(fts); err != nil {
return err
}
}
headers = headers[:beg+1]
windowSize := 10
for i := len(headers) - 1; i >= 0; i -= windowSize {
// temp storage so we don't persist data we dont want to
2019-07-31 07:13:49 +00:00
ds := dstore.NewMapDatastore()
bs := bstore.NewBlockstore(ds)
cst := hamt.CSTFromBstore(bs)
2019-07-26 18:16:57 +00:00
batchSize := windowSize
if i < batchSize {
batchSize = i
}
next := headers[i-batchSize]
bstips, err := syncer.Bsync.GetChainMessages(context.TODO(), next, uint64(batchSize+1))
2019-07-31 07:13:49 +00:00
if err != nil {
return xerrors.Errorf("message processing failed: %w", err)
}
2019-07-31 07:13:49 +00:00
for bsi := 0; bsi < len(bstips); bsi++ {
this := headers[i-bsi]
2019-07-31 07:13:49 +00:00
bstip := bstips[len(bstips)-(bsi+1)]
fts, err := zipTipSetAndMessages(cst, this, bstip.BlsMessages, bstip.SecpkMessages, bstip.BlsMsgIncludes, bstip.SecpkMsgIncludes)
if err != nil {
log.Warn("zipping failed: ", err, bsi, i)
log.Warn("height: ", this.Height())
log.Warn("bstip height: ", bstip.Blocks[0].Height)
log.Warn("bstips: ", bstips)
log.Warn("next height: ", i+batchSize)
2019-07-31 07:13:49 +00:00
return xerrors.Errorf("message processing failed: %w", err)
}
2019-07-26 18:16:57 +00:00
if err := cb(fts); err != nil {
return err
}
2019-07-31 07:13:49 +00:00
}
2019-07-26 18:16:57 +00:00
if err := persistMessages(bs, bstips); err != nil {
return err
2019-07-31 07:13:49 +00:00
}
2019-07-31 07:13:49 +00:00
if err := copyBlockstore(bs, syncer.store.Blockstore()); err != nil {
return xerrors.Errorf("message processing failed: %w", err)
2019-07-05 14:29:17 +00:00
}
2019-07-31 07:13:49 +00:00
}
2019-07-05 14:29:17 +00:00
2019-07-31 07:13:49 +00:00
return nil
}
2019-07-05 14:29:17 +00:00
func persistMessages(bs bstore.Blockstore, bstips []*BSTipSet) error {
for _, bst := range bstips {
for _, m := range bst.BlsMessages {
//log.Infof("putting BLS message: %s", m.Cid())
if _, err := store.PutMessage(bs, m); err != nil {
log.Error("failed to persist messages: ", err)
return xerrors.Errorf("BLS message processing failed: %w", err)
}
}
for _, m := range bst.SecpkMessages {
if m.Signature.Type != types.KTSecp256k1 {
return xerrors.Errorf("unknown signature type on message %s: %q", m.Cid(), m.Signature.TypeCode)
}
//log.Infof("putting secp256k1 message: %s", m.Cid())
if _, err := store.PutMessage(bs, m); err != nil {
log.Error("failed to persist messages: ", err)
return xerrors.Errorf("secp256k1 message processing failed: %w", err)
}
}
}
return nil
}
2019-07-31 07:13:49 +00:00
func (syncer *Syncer) collectChain(fts *store.FullTipSet) error {
headers, err := syncer.collectHeaders(fts.TipSet(), syncer.store.GetHeaviestTipSet())
2019-07-31 07:13:49 +00:00
if err != nil {
return err
}
for _, ts := range headers {
for _, b := range ts.Blocks() {
if err := syncer.store.PersistBlockHeader(b); err != nil {
return xerrors.Errorf("failed to persist synced blocks to the chainstore: %w", err)
}
}
2019-07-05 14:29:17 +00:00
}
2019-07-31 07:13:49 +00:00
if err := syncer.syncMessagesAndCheckState(headers); err != nil {
return xerrors.Errorf("collectChain syncMessages: %w", err)
2019-07-31 07:13:49 +00:00
}
return nil
2019-07-05 14:36:08 +00:00
}