diff options
author | Jimmy Hu <jimmy.hu@dexon.org> | 2018-11-08 11:57:22 +0800 |
---|---|---|
committer | Wei-Ning Huang <w@dexon.org> | 2019-04-09 21:32:53 +0800 |
commit | 50dae389e78fd24a8d8728e95e22a337dc1d1fd0 (patch) | |
tree | 66b2c6c0827e34f3ae15f207af4a55d14d57b571 | |
parent | dd2e293f8dfced342cec8e709099b201687f4638 (diff) | |
download | dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.tar dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.tar.gz dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.tar.bz2 dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.tar.lz dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.tar.xz dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.tar.zst dexon-50dae389e78fd24a8d8728e95e22a337dc1d1fd0.zip |
vendor: sync to latest core
6 files changed, 134 insertions, 107 deletions
diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/blockpool.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/blockpool.go index 7861a73f2..fbd84f21c 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/blockpool.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/blockpool.go @@ -23,8 +23,8 @@ import ( "github.com/dexon-foundation/dexon-consensus/core/types" ) -// blockPool is a heaped slice of blocks, indexed by chainID, and each in it is -// sorted by block's height. +// blockPool is a heaped slice of blocks ([][]*types.Block), indexed by chainID, +// and blocks in each is sorted by block's height. type blockPool []types.ByPosition func newBlockPool(chainNum uint32) (pool blockPool) { @@ -56,7 +56,7 @@ func (p blockPool) addBlock(b *types.Block) { } // purgeBlocks purges blocks of a specified chain with less-or-equal heights. -// NOTE: "chainID" is not checked here, this should be ensured by the called. +// NOTE: "chainID" is not checked here, this should be ensured by the caller. func (p blockPool) purgeBlocks(chainID uint32, height uint64) { for len(p[chainID]) > 0 && p[chainID][0].Position.Height <= height { heap.Pop(&p[chainID]) diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus-timestamp.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus-timestamp.go index 9750a74c3..833194bd9 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus-timestamp.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus-timestamp.go @@ -52,9 +52,10 @@ var ( // newConsensusTimestamp creates timestamper object. func newConsensusTimestamp( dMoment time.Time, round uint64, numChains uint32) *consensusTimestamp { - ts := make([]time.Time, 0, numChains) - for i := uint32(0); i < numChains; i++ { - ts = append(ts, dMoment) + + ts := make([]time.Time, numChains) + for i := range ts { + ts[i] = dMoment } return &consensusTimestamp{ numChainsOfRounds: []uint32{numChains}, diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go index 09bc0a873..ddf635921 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go @@ -105,9 +105,8 @@ func (recv *consensusBAReceiver) ConfirmBlock( hash common.Hash, votes map[types.NodeID]*types.Vote) { var block *types.Block if (hash == common.Hash{}) { - aID := recv.agreementModule.agreementID() recv.consensus.logger.Info("Empty block is confirmed", - "position", &aID) + "position", recv.agreementModule.agreementID()) var err error block, err = recv.consensus.proposeEmptyBlock(recv.chainID) if err != nil { @@ -268,8 +267,9 @@ func (recv *consensusDKGReceiver) ProposeDKGFinalize(final *typesDKG.Finalize) { // Consensus implements DEXON Consensus algorithm. type Consensus struct { // Node Info. - ID types.NodeID - authModule *Authenticator + ID types.NodeID + authModule *Authenticator + currentConfig *types.Config // BA. baModules []*agreement @@ -351,6 +351,7 @@ func NewConsensus( // Construct Consensus instance. con := &Consensus{ ID: ID, + currentConfig: config, ccModule: newCompactionChain(gov), lattice: lattice, app: app, @@ -369,11 +370,25 @@ func NewConsensus( roundToNotify: roundToNotify, } - validLeader := func(block *types.Block) bool { + validLeader := func(block *types.Block) (bool, error) { if block.Timestamp.After(time.Now()) { - return false + return false, nil } - return lattice.SanityCheck(block) == nil + if err := lattice.SanityCheck(block); err != nil { + if err == ErrRetrySanityCheckLater { + return false, nil + } + return false, err + } + logger.Debug("Calling Application.VerifyBlock", "block", block) + switch app.VerifyBlock(block) { + case types.VerifyInvalidBlock: + return false, ErrInvalidBlock + case types.VerifyRetryLater: + return false, nil + default: + } + return true, nil } con.baModules = make([]*agreement, config.NumChains) @@ -388,7 +403,7 @@ func NewConsensus( agreementModule := newAgreement( con.ID, recv, - newLeaderSelector(validLeader), + newLeaderSelector(validLeader, logger), con.authModule, ) // Hacky way to make agreement module self contained. @@ -405,21 +420,19 @@ func (con *Consensus) Run(initBlock *types.Block) { con.logger.Debug("Calling Governance.NotifyRoundHeight for genesis rounds", "block", initBlock) notifyGenesisRounds(initBlock, con.gov) - initRound := initBlock.Position.Round - con.logger.Debug("Calling Governance.Configuration", "round", initRound) - initConfig := con.gov.Configuration(initRound) // Setup context. con.ctx, con.ctxCancel = context.WithCancel(context.Background()) con.ccModule.init(initBlock) // TODO(jimmy-dexon): change AppendConfig to add config for specific round. - for i := uint64(0); i <= initRound; i++ { + for i := uint64(0); i < initBlock.Position.Round; i++ { con.logger.Debug("Calling Governance.Configuration", "round", i+1) cfg := con.gov.Configuration(i + 1) if err := con.lattice.AppendConfig(i+1, cfg); err != nil { panic(err) } } - dkgSet, err := con.nodeSetCache.GetDKGSet(initRound) + round0 := uint64(0) + dkgSet, err := con.nodeSetCache.GetDKGSet(round0) if err != nil { panic(err) } @@ -428,19 +441,29 @@ func (con *Consensus) Run(initBlock *types.Block) { // Sleep until dMoment come. time.Sleep(con.dMoment.Sub(time.Now().UTC())) if _, exist := dkgSet[con.ID]; exist { - con.logger.Info("Selected as DKG set", "round", initRound) - con.cfgModule.registerDKG(initRound, int(initConfig.DKGSetSize)/3+1) - con.event.RegisterTime(con.dMoment.Add(initConfig.RoundInterval/4), + con.logger.Info("Selected as DKG set", "round", round0) + con.cfgModule.registerDKG(round0, int(con.currentConfig.DKGSetSize)/3+1) + con.event.RegisterTime(con.dMoment.Add(con.currentConfig.RoundInterval/4), func(time.Time) { - con.runDKGTSIG(initRound, initConfig) + con.runDKGTSIG(round0) }) } - con.initialRound(con.dMoment, initRound, initConfig) - ticks := make([]chan struct{}, 0, initConfig.NumChains) - for i := uint32(0); i < initConfig.NumChains; i++ { + round1 := uint64(1) + con.logger.Debug("Calling Governance.Configuration", "round", round1) + con.lattice.AppendConfig(round1, con.gov.Configuration(round1)) + con.initialRound(con.dMoment) + ticks := make([]chan struct{}, 0, con.currentConfig.NumChains) + for i := uint32(0); i < con.currentConfig.NumChains; i++ { tick := make(chan struct{}) ticks = append(ticks, tick) - go con.runBA(i, tick) + // TODO(jimmy-dexon): this is a temporary solution to offset BA time. + // The complelete solution should be delivered along with config change. + offset := time.Duration(i*uint32(4)/con.currentConfig.NumChains) * + con.currentConfig.LambdaBA + go func(chainID uint32, offset time.Duration) { + time.Sleep(offset) + con.runBA(chainID, tick) + }(i, offset) } // Reset ticker. @@ -476,9 +499,8 @@ BALoop: select { case newNotary := <-recv.restartNotary: if newNotary { - configForNewRound := con.gov.Configuration(recv.round) recv.changeNotaryTime = - recv.changeNotaryTime.Add(configForNewRound.RoundInterval) + recv.changeNotaryTime.Add(con.currentConfig.RoundInterval) nodes, err := con.nodeSetCache.GetNodeSet(recv.round) if err != nil { panic(err) @@ -488,7 +510,7 @@ BALoop: con.logger.Debug("Calling Governance.Configuration", "round", recv.round) nIDs = nodes.GetSubSet( - int(configForNewRound.NotarySetSize), + int(con.gov.Configuration(recv.round).NotarySetSize), types.NewNotarySetTarget(crs, chainID)) } nextPos := con.lattice.NextPosition(chainID) @@ -499,7 +521,7 @@ BALoop: if agreement.pullVotes() { pos := agreement.agreementID() con.logger.Debug("Calling Network.PullVotes for syncing votes", - "position", &pos) + "position", pos) con.network.PullVotes(pos) } err := agreement.nextState() @@ -526,7 +548,7 @@ BALoop: } // runDKGTSIG starts running DKG+TSIG protocol. -func (con *Consensus) runDKGTSIG(round uint64, config *types.Config) { +func (con *Consensus) runDKGTSIG(round uint64) { con.dkgReady.L.Lock() defer con.dkgReady.L.Unlock() if con.dkgRunning != 0 { @@ -542,7 +564,7 @@ func (con *Consensus) runDKGTSIG(round uint64, config *types.Config) { con.dkgRunning = 2 DKGTime := time.Now().Sub(startTime) if DKGTime.Nanoseconds() >= - config.RoundInterval.Nanoseconds()/2 { + con.currentConfig.RoundInterval.Nanoseconds()/2 { con.logger.Warn("Your computer cannot finish DKG on time!", "nodeID", con.ID.String()) } @@ -582,10 +604,11 @@ func (con *Consensus) runDKGTSIG(round uint64, config *types.Config) { }() } -func (con *Consensus) runCRS(round uint64) { +func (con *Consensus) runCRS() { // Start running next round CRS. - con.logger.Debug("Calling Governance.CRS", "round", round) - psig, err := con.cfgModule.preparePartialSignature(round, con.gov.CRS(round)) + con.logger.Debug("Calling Governance.CRS", "round", con.round) + psig, err := con.cfgModule.preparePartialSignature( + con.round, con.gov.CRS(con.round)) if err != nil { con.logger.Error("Failed to prepare partial signature", "error", err) } else if err = con.authModule.SignDKGPartialSignature(psig); err != nil { @@ -598,89 +621,85 @@ func (con *Consensus) runCRS(round uint64) { "round", psig.Round, "hash", psig.Hash) con.network.BroadcastDKGPartialSignature(psig) - con.logger.Debug("Calling Governance.CRS", "round", round) - crs, err := con.cfgModule.runCRSTSig(round, con.gov.CRS(round)) + con.logger.Debug("Calling Governance.CRS", "round", con.round) + crs, err := con.cfgModule.runCRSTSig(con.round, con.gov.CRS(con.round)) if err != nil { con.logger.Error("Failed to run CRS Tsig", "error", err) } else { con.logger.Debug("Calling Governance.ProposeCRS", - "round", round+1, + "round", con.round+1, "crs", hex.EncodeToString(crs)) - con.gov.ProposeCRS(round+1, crs) + con.gov.ProposeCRS(con.round+1, crs) } } } -func (con *Consensus) initialRound( - startTime time.Time, round uint64, config *types.Config) { +func (con *Consensus) initialRound(startTime time.Time) { select { case <-con.ctx.Done(): return default: } - curDkgSet, err := con.nodeSetCache.GetDKGSet(round) + con.logger.Debug("Calling Governance.Configuration", "round", con.round) + con.currentConfig = con.gov.Configuration(con.round) + curDkgSet, err := con.nodeSetCache.GetDKGSet(con.round) if err != nil { - con.logger.Error("Error getting DKG set", "round", round, "error", err) + con.logger.Error("Error getting DKG set", "round", con.round, "error", err) curDkgSet = make(map[types.NodeID]struct{}) } if _, exist := curDkgSet[con.ID]; exist { - con.event.RegisterTime(startTime.Add(config.RoundInterval/2), + con.event.RegisterTime(startTime.Add(con.currentConfig.RoundInterval/2), func(time.Time) { go func() { - con.runCRS(round) + con.runCRS() }() }) } - con.event.RegisterTime(startTime.Add(config.RoundInterval/2+config.LambdaDKG), + con.event.RegisterTime(startTime.Add(con.currentConfig.RoundInterval/2), func(time.Time) { - go func(nextRound uint64) { + go func() { + ticker := newTicker(con.gov, con.round, TickerDKG) + <-ticker.Tick() // Normally, gov.CRS would return non-nil. Use this for in case of // unexpected network fluctuation and ensure the robustness. - for (con.gov.CRS(nextRound) == common.Hash{}) { + for (con.gov.CRS(con.round+1) == common.Hash{}) { con.logger.Info("CRS is not ready yet. Try again later...", "nodeID", con.ID) time.Sleep(500 * time.Millisecond) } - nextDkgSet, err := con.nodeSetCache.GetDKGSet(nextRound) + nextDkgSet, err := con.nodeSetCache.GetDKGSet(con.round + 1) if err != nil { con.logger.Error("Error getting DKG set", - "round", nextRound, - "error", err) + "round", con.round+1, "error", err) return } if _, exist := nextDkgSet[con.ID]; !exist { return } - con.logger.Info("Selected as DKG set", "round", nextRound) + con.logger.Info("Selected as DKG set", "round", con.round+1) con.cfgModule.registerDKG( - nextRound, int(config.DKGSetSize/3)+1) + con.round+1, int(con.currentConfig.DKGSetSize/3)+1) con.event.RegisterTime( - startTime.Add(config.RoundInterval*2/3), + startTime.Add(con.currentConfig.RoundInterval*2/3), func(time.Time) { func() { con.dkgReady.L.Lock() defer con.dkgReady.L.Unlock() con.dkgRunning = 0 }() - con.logger.Debug("Calling Governance.Configuration", - "round", nextRound) - nextConfig := con.gov.Configuration(nextRound) - con.runDKGTSIG(nextRound, nextConfig) + con.runDKGTSIG(con.round + 1) }) - }(round + 1) + }() }) - con.event.RegisterTime(startTime.Add(config.RoundInterval), + con.event.RegisterTime(startTime.Add(con.currentConfig.RoundInterval), func(time.Time) { // Change round. - nextRound := round + 1 + con.round++ con.logger.Debug("Calling Governance.Configuration", - "round", nextRound) - nextConfig := con.gov.Configuration(nextRound) - con.lattice.AppendConfig(nextRound, nextConfig) - con.initialRound( - startTime.Add(config.RoundInterval), nextRound, nextConfig) - con.round = nextRound + "round", con.round+1) + con.lattice.AppendConfig(con.round+1, con.gov.Configuration(con.round+1)) + con.initialRound(startTime.Add(con.currentConfig.RoundInterval)) }) } @@ -719,6 +738,14 @@ MessageLoop: continue MessageLoop } } + // TODO(mission): check with full node if this verification is required. + con.logger.Debug("Calling Application.VerifyBlock", "block", val) + switch con.app.VerifyBlock(val) { + case types.VerifyInvalidBlock: + con.logger.Error("VerifyBlock fail") + continue MessageLoop + default: + } func() { con.lock.Lock() defer con.lock.Unlock() @@ -845,7 +872,7 @@ func (con *Consensus) ProcessAgreementResult( agreement := con.baModules[rand.Position.ChainID] aID := agreement.agreementID() if rand.Position.Newer(&aID) { - con.logger.Info("Syncing BA", "position", &rand.Position) + con.logger.Info("Syncing BA", "position", rand.Position) nodes, err := con.nodeSetCache.GetNodeSet(rand.Position.Round) if err != nil { return err @@ -946,7 +973,7 @@ func (con *Consensus) ProcessBlockRandomnessResult( } con.logger.Debug("Calling Network.BroadcastRandomnessResult", "hash", rand.BlockHash, - "position", &rand.Position, + "position", rand.Position, "randomness", hex.EncodeToString(rand.Randomness)) con.network.BroadcastRandomnessResult(rand) if err := con.ccModule.processBlockRandomnessResult(rand); err != nil { @@ -959,11 +986,6 @@ func (con *Consensus) ProcessBlockRandomnessResult( // preProcessBlock performs Byzantine Agreement on the block. func (con *Consensus) preProcessBlock(b *types.Block) (err error) { - if err = con.lattice.SanityCheck(b); err != nil { - if err != ErrRetrySanityCheckLater { - return - } - } if err = con.baModules[b.Position.ChainID].processBlock(b); err != nil { return err } diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/lattice.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/lattice.go index db13e7eba..6c69d5272 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/lattice.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/lattice.go @@ -67,7 +67,7 @@ func NewLattice( pool: newBlockPool(cfg.NumChains), data: newLatticeData(db, dataConfig), toModule: newTotalOrdering(toConfig), - ctModule: newConsensusTimestamp(dMoment, 0, cfg.NumChains), + ctModule: newConsensusTimestamp(dMoment, round, cfg.NumChains), logger: logger, } } @@ -111,8 +111,8 @@ func (l *Lattice) PrepareEmptyBlock(b *types.Block) (err error) { // SanityCheck checks the validity of a block. // -// If any acking blocks of this block does not exist, Lattice helps caching this -// block and retries when Lattice.ProcessBlock is called. +// If any acking block of this block does not exist, Lattice caches this block +// and retries when Lattice.ProcessBlock is called. func (l *Lattice) SanityCheck(b *types.Block) (err error) { if b.IsEmpty() { // Only need to verify block's hash. @@ -153,14 +153,6 @@ func (l *Lattice) SanityCheck(b *types.Block) (err error) { }(); err != nil { return } - // Verify data in application layer. - l.logger.Debug("Calling Application.VerifyBlock", "block", b) - switch l.app.VerifyBlock(b) { - case types.VerifyInvalidBlock: - err = ErrInvalidBlock - case types.VerifyRetryLater: - err = ErrRetrySanityCheckLater - } return } diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/leader-selector.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/leader-selector.go index 08006dbfb..247ce8933 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/leader-selector.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/leader-selector.go @@ -32,7 +32,7 @@ var ( ErrIncorrectCRSSignature = fmt.Errorf("incorrect CRS signature") ) -type validLeaderFn func(*types.Block) bool +type validLeaderFn func(*types.Block) (bool, error) // Some constant value. var ( @@ -57,12 +57,15 @@ type leaderSelector struct { pendingBlocks []*types.Block validLeader validLeaderFn lock sync.Mutex + logger common.Logger } -func newLeaderSelector(validLeader validLeaderFn) *leaderSelector { +func newLeaderSelector( + validLeader validLeaderFn, logger common.Logger) *leaderSelector { return &leaderSelector{ minCRSBlock: maxHash, validLeader: validLeader, + logger: logger, } } @@ -98,7 +101,12 @@ func (l *leaderSelector) leaderBlockHash() common.Hash { defer l.lock.Unlock() newPendingBlocks := []*types.Block{} for _, b := range l.pendingBlocks { - if l.validLeader(b) { + ok, err := l.validLeader(b) + if err != nil { + l.logger.Error("Error checking validLeader", "error", err, "block", b) + continue + } + if ok { l.updateLeader(b) } else { newPendingBlocks = append(newPendingBlocks, b) @@ -118,7 +126,11 @@ func (l *leaderSelector) processBlock(block *types.Block) error { } l.lock.Lock() defer l.lock.Unlock() - if !l.validLeader(block) { + ok, err = l.validLeader(block) + if err != nil { + return err + } + if !ok { l.pendingBlocks = append(l.pendingBlocks, block) return nil } diff --git a/vendor/vendor.json b/vendor/vendor.json index b0f8c0717..797545be6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -105,50 +105,50 @@ { "checksumSHA1": "ev84RyegNbt2Pr/sK26LK9LoQNI=", "path": "github.com/dexon-foundation/dexon-consensus/common", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { - "checksumSHA1": "NxDI5JSfgv3GWC4/0WbIHlYDUVM=", + "checksumSHA1": "pMuKuiISXB5NN/FBZYWlPlXIN1A=", "path": "github.com/dexon-foundation/dexon-consensus/core", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "vNsaBvsrXJF+W6K5DCLpgy1rUZY=", "path": "github.com/dexon-foundation/dexon-consensus/core/blockdb", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "tQSbYCu5P00lUhKsx3IbBZCuSLY=", "path": "github.com/dexon-foundation/dexon-consensus/core/crypto", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "p2jOAulavUU2xyj018pYPHlj8XA=", "path": "github.com/dexon-foundation/dexon-consensus/core/crypto/dkg", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "6Pf6caC8LTNCI7IflFmglKYnxYo=", "path": "github.com/dexon-foundation/dexon-consensus/core/crypto/ecdsa", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "o7RiigeU3kIDbZ96Gh95/h/7yZo=", "path": "github.com/dexon-foundation/dexon-consensus/core/types", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "ovChyW9OfDGnk/7CDAR+A5vJymc=", "path": "github.com/dexon-foundation/dexon-consensus/core/types/dkg", - "revision": "3714ebf2f1054d9984d37b89cf17e885a5856532", - "revisionTime": "2018-11-06T08:53:19Z" + "revision": "b2625a45a5de1df99811437d015cf5a3777ee62e", + "revisionTime": "2018-11-08T03:17:14Z" }, { "checksumSHA1": "TAkwduKZqLyimyTPPWIllZWYFuE=", |