diff --git a/byzcoin/bcadmin/README.md b/byzcoin/bcadmin/README.md index 0dab94a8b5..8445fa5fda 100644 --- a/byzcoin/bcadmin/README.md +++ b/byzcoin/bcadmin/README.md @@ -229,7 +229,7 @@ and return success: ```bash bcadmin db catchup cached.db _bcID_ _url_ -bcadmin db replay cached.db _bcID_ --continue +bcadmin db replay cached.db _bcID_ ``` The `_bcID_` has to be replaced by the hexadecimal representation of the diff --git a/byzcoin/bcadmin/cmd_db.go b/byzcoin/bcadmin/cmd_db.go index 4f8583074f..23d5bdce28 100644 --- a/byzcoin/bcadmin/cmd_db.go +++ b/byzcoin/bcadmin/cmd_db.go @@ -1,10 +1,12 @@ package main import ( + "encoding/binary" "encoding/hex" "errors" "flag" "fmt" + "go.dedis.ch/cothority/v3/byzcoin/trie" "strconv" "strings" "time" @@ -90,12 +92,10 @@ func dbReplay(c *cli.Context) error { log.Info("Preparing db") start := *fb.bcID err = fb.boltDB.Update(func(tx *bbolt.Tx) error { - if !fb.flagReplayCont { - if tx.Bucket(fb.bucketName) != nil { - err := tx.DeleteBucket(fb.bucketName) - if err != nil { - return err - } + if tx.Bucket(fb.bucketName) != nil { + err := tx.DeleteBucket(fb.bucketName) + if err != nil { + return err } } _, err := tx.CreateBucketIfNotExists(fb.bucketName) @@ -108,46 +108,51 @@ func dbReplay(c *cli.Context) error { return xerrors.Errorf("couldn't add bucket: %+v", err) } - if !fb.flagReplayCont { - _, err = fb.service.ReplayStateDB(fb.boltDB, fb.bucketName, fb.genesis) - if err != nil { - return xerrors.Errorf("couldn't create stateDB: %+v", err) - } - } else { - index, err := fb.service.ReplayStateDB(fb.boltDB, fb.bucketName, nil) - if err != nil { - return xerrors.Errorf("couldn't replay blocks: %+v", err) - } - - log.Info("Searching for block with index", index+1) - sb := fb.db.GetByID(start) - for sb != nil && sb.Index < index+1 { - if len(sb.ForwardLink) == 0 { - break - } - sb = fb.db.GetByID(sb.ForwardLink[0].To) - start = sb.Hash - } - if sb.Index <= index { - log.Info("No new blocks available") - return nil - } - } + log.Lvl2("Copying blocks to skipchain's DB") + fb.skipchain.GetDB().DB = fb.db.DB log.Info("Replaying blocks") - _, err = fb.service.ReplayStateContLog(start, - &sumFetcher{summarizeBlocks: c.Int("summarize"), bff: fb.blockFetcher}) + rso := byzcoin.ReplayStateOptions{ + MaxBlocks: c.Int("blocks"), + VerifyFLSig: c.Bool("verifyFLSig"), + } + if c.Bool("continue") { + rso.StartingTrie = fb.trieDB + } + st, err := fb.service.ReplayState(start, newSumFetcher(c), rso) if err != nil { return xerrors.Errorf("couldn't replay blocks: %+v", err) } log.Info("Successfully checked and replayed all blocks.") + if c.Bool("write") { + log.Info("Writing new stateTrie to DB") + err := fb.boltDB.Update(func(tx *bbolt.Tx) error { + if tx.Bucket(fb.trieBucketName) != nil { + err := tx.DeleteBucket(fb.trieBucketName) + if err != nil { + return fmt.Errorf("while deleting bucket: %v", err) + } + } + bucket, err := tx.CreateBucket(fb.trieBucketName) + if err != nil { + return fmt.Errorf("while creating bucket: %v", err) + } + return st.View(func(b trie.Bucket) error { + return b.ForEach(func(k, v []byte) error { + return bucket.Put(k, v) + }) + }) + }) + if err != nil { + return fmt.Errorf("couldn't update bucket: %v", err) + } + } return nil } type sumFetcher struct { summarizeBlocks int - bff byzcoin.BlockFetcherFunc totalTXs int accepted int seenBlocks int @@ -156,11 +161,12 @@ type sumFetcher struct { maxTPS float64 maxBlockSize int totalBlockSize int + verifyFLSig bool + maxBlocks int } -func (sf sumFetcher) BlockFetcherFunc(sid skipchain.SkipBlockID) (*skipchain. - SkipBlock, error) { - return sf.bff(sid) +func newSumFetcher(c *cli.Context) *sumFetcher { + return &sumFetcher{summarizeBlocks: c.Int("summarize")} } func (sf sumFetcher) LogNewBlock(sb *skipchain.SkipBlock) { @@ -370,6 +376,28 @@ func dbRemove(c *cli.Context) error { return fmt.Errorf("couldn't convert %s to int: %v", c.Args().Get(2), err) } + if blocks >= latest.Index { + return errors.New("cannot delete up the genesis block or earlier") + } + } + + // Checking the removal of blocks will not lead to an unrecoverable state + // of the node. + tr := trie.NewDiskDB(fb.boltDB, fb.trieBucketName) + err = tr.View(func(b trie.Bucket) error { + buf := b.Get([]byte("trieIndexKey")) + if buf == nil { + return errors.New("couldn't get index key") + } + if latest.Index-blocks <= int(binary.LittleEndian.Uint32(buf)) { + return errors.New("need to keep blocks up to the trie-state." + + "\nUse `bcadmin db replay --write` to replay the db to a" + + " previous state.") + } + return nil + }) + if err != nil { + return err } for i := 0; i < blocks; i++ { @@ -509,6 +537,7 @@ func dbCheck(c *cli.Context) error { type fetchBlocks struct { cl *skipchain.Client service *byzcoin.Service + skipchain *skipchain.Service bcID *skipchain.SkipBlockID local *onet.LocalTest roster *onet.Roster @@ -520,8 +549,8 @@ type fetchBlocks struct { db *skipchain.SkipBlockDB bucketName []byte flagCatchupBatch int - flagReplayBlocks int - flagReplayCont bool + trieDB trie.DB + trieBucketName []byte } func newFetchBlocks(c *cli.Context) (*fetchBlocks, @@ -535,13 +564,12 @@ func newFetchBlocks(c *cli.Context) (*fetchBlocks, local: onet.NewLocalTest(cothority.Suite), bucketName: []byte("replayStateBucket"), flagCatchupBatch: c.Int("batch"), - flagReplayBlocks: c.Int("blocks"), - flagReplayCont: c.Bool("continue"), } var err error servers := fb.local.GenServers(1) fb.service = servers[0].Service(byzcoin.ServiceName).(*byzcoin.Service) + fb.skipchain = servers[0].Service(skipchain.ServiceName).(*skipchain.Service) log.Info("Opening database", c.Args().First()) fb.db, fb.boltDB, err = fb.openDB(c.Args().First()) @@ -562,6 +590,10 @@ func newFetchBlocks(c *cli.Context) (*fetchBlocks, return nil, xerrors.Errorf("couldn't check bcID: %+v", err) } } + + fb.trieBucketName = []byte(fmt.Sprintf("ByzCoin_%x", *fb.bcID)) + fb.trieDB = trie.NewDiskDB(fb.boltDB, fb.trieBucketName) + return fb, nil } @@ -630,11 +662,6 @@ func (fb *fetchBlocks) addURL(url string) error { } func (fb *fetchBlocks) blockFetcher(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - fb.flagReplayBlocks-- - if fb.flagReplayBlocks == 0 { - log.Info("reached end of task") - return nil, nil - } sb := fb.db.GetByID(sib) if sb == nil { return nil, nil diff --git a/byzcoin/bcadmin/commands.go b/byzcoin/bcadmin/commands.go index c68a11261d..0b7295b9e2 100644 --- a/byzcoin/bcadmin/commands.go +++ b/byzcoin/bcadmin/commands.go @@ -656,19 +656,29 @@ var cmds = cli.Commands{ Usage: "Replay a chain and check the global state is consistent", Action: dbReplay, Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "continue, cont", - Usage: "continue an aborted replay", - }, cli.IntFlag{ Name: "blocks", Usage: "how many blocks to apply", + Value: -1, }, cli.IntFlag{ Name: "summarize, sum", Usage: "summarize this many blocks in output", Value: 1, }, + cli.BoolFlag{ + Name: "verifyFLSig", + Usage: "turns on forward-link signature verification", + }, + cli.BoolFlag{ + Name: "continue", + Usage: "take the existing trie and replay from the" + + " latest block", + }, + cli.BoolFlag{ + Name: "write", + Usage: "write the new trie to the db", + }, }, }, { diff --git a/byzcoin/bcadmin/test.sh b/byzcoin/bcadmin/test.sh index 0fe7d55d22..c6abca18d6 100755 --- a/byzcoin/bcadmin/test.sh +++ b/byzcoin/bcadmin/test.sh @@ -5,7 +5,7 @@ # Options: # -b re-builds bcadmin package -DBG_TEST=2 +DBG_TEST=1 DBG_SRV=1 DBG_BCADMIN=2 @@ -84,15 +84,15 @@ testDbReplay(){ testOK runBA db catchup conode.db $bcID http://localhost:2003 testGrep "Replaying block at index 0" runBA db replay conode.db $bcID + # replay with more than 1 block testOK runBA mint $bc $key $keyPub 1000 testOK runBA mint $bc $key $keyPub 1000 - - # replay with more than 1 block runBA db catchup conode.db $bcID http://localhost:2003 - testNGrep "Replaying block at index 0" runBA db replay conode.db $bcID --cont - testReGrep "Replaying block at index 1" testGrep "Replaying block at index 0" runBA db replay conode.db $bcID - testOK runBA db replay conode.db $bcID --cont + testGrep "index 0" runBA db catchup conode.db $bcID --write --blocks 1 + testReNGrep "index 1" + testNGrep "index 0" runBA db catchup conode.db $bcID --write --continue + testReGrep "index 1" } testDbMerge(){ @@ -322,7 +322,7 @@ testAddDarc(){ testGrep "${ID:5:${#ID}-0}" runBA darc show --darc "$ID" # checks the --shortPrint option - OUTRES=$(runBA darc add --shortPrint) + OUTRES=$(runBA0 darc add --shortPrint) matchOK "$OUTRES" "darc:[0-9a-f]{64} \[ed25519:[0-9a-f]{64}\]" } @@ -478,7 +478,7 @@ runBA(){ } runBA0(){ - dbgRun ./bcadmin -c config/ --debug 0 "$@" + ./bcadmin -c config/ --debug 0 "$@" } testQR() { diff --git a/byzcoin/contracts.go b/byzcoin/contracts.go index e386256fb8..0fe0ae517f 100644 --- a/byzcoin/contracts.go +++ b/byzcoin/contracts.go @@ -541,6 +541,13 @@ func (c *contractConfig) Invoke(rst ReadOnlyStateTrie, inst Instruction, coins [ if err != nil { return nil, nil, xerrors.Errorf("decoding: %v", err) } + + _, err := rst.(ReadOnlySkipChain).GetBlockByIndex(rst.GetIndex()) + if err != nil { + return nil, nil, + fmt.Errorf("couldn't get latest skipblock: %v", err) + } + if rst.GetVersion() < VersionViewchange { // If everything is correctly signed, then we trust it, no need // to do additional verification. diff --git a/byzcoin/service.go b/byzcoin/service.go index fcdf174b8a..ecabaf0bb9 100644 --- a/byzcoin/service.go +++ b/byzcoin/service.go @@ -2173,7 +2173,7 @@ func (s *Service) createStateChanges(sst *stagingStateTrie, scID skipchain.SkipB if err != nil { tx.Accepted = false txOut = append(txOut, tx) - log.Error(s.ServerIdentity(), err) + log.Warn(s.ServerIdentity(), err) } else { // We would like to be able to check if this txn is so big it could never fit into a block, // and if so, drop it. But we can't with the current API of createStateChanges. diff --git a/byzcoin/statereplay.go b/byzcoin/statereplay.go index 0d184c3461..f89afcabde 100644 --- a/byzcoin/statereplay.go +++ b/byzcoin/statereplay.go @@ -2,34 +2,34 @@ package byzcoin import ( "bytes" + "errors" "fmt" - "time" - - "go.etcd.io/bbolt" + "go.dedis.ch/cothority/v3/byzcoin/trie" + "go.dedis.ch/kyber/v3/pairing" "golang.org/x/xerrors" "go.dedis.ch/cothority/v3" - "go.dedis.ch/kyber/v3/pairing" - "go.dedis.ch/cothority/v3/skipchain" - "go.dedis.ch/onet/v3" "go.dedis.ch/onet/v3/log" "go.dedis.ch/protobuf" ) -// BlockFetcherFunc is a function that takes the roster and the block ID as parameter -// and returns the block or an error -type BlockFetcherFunc func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) - -// BlockFetcher is an interface that can be passed to ReplayStateLog so that +// ReplayStateLog is an interface that can be passed to ReplayState so that // the output of the replay can be adapted to what the user wants. -type BlockFetcher interface { - BlockFetcherFunc(sid skipchain.SkipBlockID) (*skipchain.SkipBlock, error) +type ReplayStateLog interface { LogNewBlock(sb *skipchain.SkipBlock) LogAppliedBlock(sb *skipchain.SkipBlock, head DataHeader, body DataBody) LogWarn(sb *skipchain.SkipBlock, msg, dump string) } +// ReplayStateOptions is a placeholder for all future options. +// If you add a new option, be sure to keep the empty value as default. +type ReplayStateOptions struct { + MaxBlocks int + VerifyFLSig bool + StartingTrie trie.DB +} + func replayError(sb *skipchain.SkipBlock, err error) error { return cothority.ErrorOrNilSkip(err, fmt.Sprintf("replay failed in block at index %d with message", sb.Index), 2) } @@ -38,76 +38,67 @@ func replayError(sb *skipchain.SkipBlock, err error) error { // the callback returns nil or a block without forward links. // If a client wants to replay the states until the block at index x, the callback function // must be implemented to return nil when the next block has the index x. -func (s *Service) ReplayState(id skipchain.SkipBlockID, ro *onet.Roster, cb BlockFetcherFunc) (ReadOnlyStateTrie, error) { - return s.ReplayStateCont(id, cb) -} - -var replayTrie = "replayTrie" +func (s *Service) ReplayState(id skipchain.SkipBlockID, + rlog ReplayStateLog, opt ReplayStateOptions) (trie.DB, error) { + sb := s.db().GetByID(id) + if sb == nil { + return nil, fmt.Errorf("failed to get the first block") + } + if sb.Index > 0 { + return nil, errors.New("must start from genesis block") + } -// ReplayStateDB creates a stateTrie tied to the boltdb and the bucket. Every -// change to the statetrie will be reflected in the db, allowing for saving -// and resuming replays. -func (s *Service) ReplayStateDB(db *bbolt.DB, bucket []byte, - genesis *skipchain.SkipBlock) (int, error) { - if len(s.stateTries) == 0 { - s.stateTries = make(map[string]*stateTrie) + // Create a memory state trie that can be thrown away, but which is much + // faster than the disk state trie. + var dBody DataBody + err := protobuf.Decode(sb.Payload, &dBody) + if err != nil { + return nil, replayError(sb, err) } - var st *stateTrie - if genesis == nil { - var err error - st, err = loadStateTrie(db, bucket) - if err != nil { - return 0, xerrors.Errorf("couldn't load state trie: %+v", err) - } - } else { - var dBody DataBody - err := protobuf.Decode(genesis.Payload, &dBody) - if err != nil { - return 0, xerrors.Errorf("couldn't decode payload: %+v", err) - } - nonce, err := loadNonceFromTxs(dBody.TxResults) + nonce, err := loadNonceFromTxs(dBody.TxResults) + if err != nil { + return nil, replayError(sb, err) + } + st, err := newMemStateTrie(nonce) + if err != nil { + return nil, fmt.Errorf("couldn't create memory stateTrie: %v", err) + } + + if opt.StartingTrie != nil { + err := st.Trie.DB().Update(func(mem trie.Bucket) error { + return opt.StartingTrie.View(func(db trie.Bucket) error { + return db.ForEach(func(k, v []byte) error { + return mem.Put(k, v) + }) + }) + }) if err != nil { - return 0, xerrors.Errorf("couldn't get nonce: %+v", err) + return nil, fmt.Errorf("couldn't copy db-trie to mem-trie: %v", err) } - st, err = newStateTrie(db, bucket, nonce) + log.LLvl2("Getting latest block:", st.GetIndex()+1) + rep, err := s.skService().GetSingleBlockByIndex(&skipchain. + GetSingleBlockByIndex{ + Genesis: sb.SkipChainID(), + Index: st.GetIndex() + 1, + }) if err != nil { - return 0, xerrors.Errorf("couldn't get new state trie: %+v", err) + return nil, fmt.Errorf("couldn't get latest block from trie: %v", + err) } - } - s.stateTries[replayTrie] = st - return st.GetIndex(), nil -} - -// ReplayStateContLog builds the state changes from the genesis of the given -// skipchain ID until the callback returns nil or a block without forward -// links. -// If a client wants to replay the states until the block at index x, the -// callback function must be implemented to return nil when the next block has -// the index x. -func (s *Service) ReplayStateContLog(id skipchain.SkipBlockID, - bf BlockFetcher) (ReadOnlyStateTrie, error) { - sb, err := bf.BlockFetcherFunc(id) - if err != nil { - return nil, xerrors.Errorf("fail to get the first block: %v", err) + sb = rep.SkipBlock } - var st *stateTrie - if len(s.stateTries) > 0 { - st = s.stateTries[replayTrie] - } - if st == nil { - if sb.Index != 0 { - // It could be possible to start from a non-genesis block but you need to download - // the state trie first from a conode in addition to the block - return nil, xerrors.Errorf("must start from genesis block but found index %d", sb.Index) + if opt.MaxBlocks < 0 { + latest, err := s.db().GetLatest(sb) + if err != nil { + return nil, fmt.Errorf("couldn't fetch latest block: %v", err) } - } else if st.GetIndex()+1 != sb.Index { - return nil, xerrors.Errorf("got a skipblock with index %d for trie with"+ - " index %d", sb.Index, st.GetIndex()+1) + opt.MaxBlocks = latest.Index + 1 } - for sb != nil { - bf.LogNewBlock(sb) + // Start processing the blocks + for block := 0; block < opt.MaxBlocks; block++ { + rlog.LogNewBlock(sb) if sb.Payload != nil { var dBody DataBody @@ -127,17 +118,6 @@ func (s *Service) ReplayStateContLog(id skipchain.SkipBlockID, return nil, replayError(sb, xerrors.New("client transaction hash does not match")) } - if sb.Index == 0 && st == nil { - nonce, err := loadNonceFromTxs(dBody.TxResults) - if err != nil { - return nil, replayError(sb, err) - } - st, err = newMemStateTrie(nonce) - if err != nil { - return nil, replayError(sb, err) - } - } - sst := st.MakeStagingStateTrie() var scs StateChanges @@ -181,19 +161,31 @@ func (s *Service) ReplayStateContLog(id skipchain.SkipBlockID, } log.Lvl2("Checking links for block", sb.Index) - pubs := sb.Roster.ServicePublics(skipchain.ServiceName) for j, fl := range sb.ForwardLink { + var errStr string if fl.From == nil || fl.To == nil || len(fl.From) == 0 || len(fl.To) == 0 { - bf.LogWarn(sb, - fmt.Sprintf("Forward-link %d looks broken", j), - fmt.Sprintf("%+v", fl)) + errStr = "is missing" continue + } else if !fl.From.Equal(sb.Hash) { + errStr = "from-field doesn't match block-hash" + } else if sbTmp := s.db().GetByID(fl.To); sbTmp == nil { + errStr = "to-field points to non-existing block" } - err = fl.VerifyWithScheme(pairing.NewSuiteBn256(), pubs, sb.SignatureScheme) - if err != nil { - log.Errorf("Found error in forward-link: '%s' - #%d: %+v", err, j, fl) - return nil, xerrors.Errorf("invalid forward-link: %v", err) + if errStr != "" { + rlog.LogWarn(sb, fmt.Sprintf( + "bad forward-link %d/%d: %s", j, len(sb.ForwardLink), + errStr), fmt.Sprintf("%+v", fl)) + continue + } + + if opt.VerifyFLSig { + pubs := sb.Roster.ServicePublics(skipchain.ServiceName) + err = fl.VerifyWithScheme(pairing.NewSuiteBn256(), pubs, sb.SignatureScheme) + if err != nil { + log.Errorf("Found error in forward-link: '%s' - #%d: %+v", err, j, fl) + return nil, xerrors.Errorf("invalid forward-link: %v", err) + } } } @@ -201,59 +193,20 @@ func (s *Service) ReplayStateContLog(id skipchain.SkipBlockID, if err != nil { return nil, replayError(sb, err) } - bf.LogAppliedBlock(sb, dHead, dBody) + rlog.LogAppliedBlock(sb, dHead, dBody) } - if len(sb.ForwardLink) > 0 { + if len(sb.ForwardLink) == 0 { + break + } else { // The level 0 forward link must be used as we need to rebuild the global // states for each block. - sb, err = bf.BlockFetcherFunc(sb.ForwardLink[0].To) - if err != nil { - return nil, xerrors.Errorf("replay failed to get the next block: %v", err) + sb = s.db().GetByID(sb.ForwardLink[0].To) + if sb == nil { + return nil, errors.New("replay failed to get the next block") } - } else { - sb = nil } } - return st, nil -} - -// ReplayStateCont is a wrapper over ReplayStateContLog and outputs every -// block to the std-output. -func (s *Service) ReplayStateCont(id skipchain.SkipBlockID, - cb BlockFetcherFunc) (ReadOnlyStateTrie, error) { - return s.ReplayStateContLog(id, stdFetcher{cb}) -} - -// stdFetcher is a fetcher method that outputs using the onet.log library. -type stdFetcher struct { - cb BlockFetcherFunc -} - -func (sf stdFetcher) BlockFetcherFunc(sib skipchain.SkipBlockID) (*skipchain. - SkipBlock, error) { - return sf.cb(sib) -} - -func (sf stdFetcher) LogNewBlock(sb *skipchain.SkipBlock) { - log.Infof("Replaying block at index %d", sb.Index) -} - -func (sf stdFetcher) LogWarn(sb *skipchain.SkipBlock, msg, dump string) { - log.Infof(msg, dump) -} - -func (sf stdFetcher) LogAppliedBlock(sb *skipchain.SkipBlock, - head DataHeader, body DataBody) { - txAccepted := 0 - for _, tx := range body.TxResults { - if tx.Accepted { - txAccepted++ - } - } - t := time.Unix(head.Timestamp/1e9, 0) - log.Infof("Got correct block from %s with %d txs, "+ - "out of which %d txs got accepted", - t.String(), len(body.TxResults), txAccepted) + return st.DB(), nil } diff --git a/byzcoin/statereplay_test.go b/byzcoin/statereplay_test.go index fc28a13501..7252274a21 100644 --- a/byzcoin/statereplay_test.go +++ b/byzcoin/statereplay_test.go @@ -1,12 +1,15 @@ package byzcoin import ( + "go.dedis.ch/onet/v3/log" + "go.dedis.ch/onet/v3/network" + "go.etcd.io/bbolt" "testing" + "time" "github.com/stretchr/testify/require" "go.dedis.ch/cothority/v3/skipchain" "go.dedis.ch/protobuf" - "golang.org/x/xerrors" ) // Test the expected use case @@ -28,21 +31,29 @@ func TestService_StateReplay(t *testing.T) { require.NoError(t, err) } - cb := func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - return s.service().skService().GetSingleBlock(&skipchain.GetSingleBlock{ID: sib}) - } - - st, err := s.service().ReplayState(s.genesis.Hash, s.roster, cb) + _, err := s.service().ReplayState(s.genesis.Hash, stdFetcher{}, + ReplayStateOptions{}) require.NoError(t, err) - require.Equal(t, 2, st.GetIndex()) } -func tryReplay(t *testing.T, s *ser, cb BlockFetcherFunc, msg string) { - _, err := s.service().ReplayState(s.genesis.Hash, s.roster, cb) +func tryReplayBlock(t *testing.T, s *ser, sbID skipchain.SkipBlockID, msg string) { + _, err := s.service().ReplayState(sbID, stdFetcher{}, + ReplayStateOptions{MaxBlocks: 1}) require.Error(t, err) require.Contains(t, err.Error(), msg) } +func forceStoreBlock(s *ser, sb *skipchain.SkipBlock) error { + return s.service().db().Update(func(tx *bbolt.Tx) error { + buf, err := network.Marshal(sb) + if err != nil { + return err + } + sb.Hash = sb.CalculateHash() + return tx.Bucket([]byte("Skipchain_skipblocks")).Put(sb.Hash, buf) + }) +} + // Test that it catches failing chains and return meaningful errors func TestService_StateReplayFailures(t *testing.T) { s := newSer(t, 1, testInterval) @@ -60,115 +71,89 @@ func TestService_StateReplayFailures(t *testing.T) { require.NoError(t, err) // 1. error when fetching the genesis block - cb := func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - return nil, xerrors.New("") - } - tryReplay(t, s, cb, "fail to get the first block:") + tryReplayBlock(t, s, skipchain.SkipBlockID{}, + "failed to get the first block") // 2. not a genesis block for the first block - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - sb := skipchain.NewSkipBlock() - sb.Index = 1 - sb.Roster = s.roster - return sb, nil - } - tryReplay(t, s, cb, "must start from genesis block") - - // 3. error when getting the next block - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - if !sib.Equal(s.genesis.Hash) { - return nil, xerrors.New("") - } - - return s.service().skService().GetSingleBlock(&skipchain.GetSingleBlock{ID: sib}) - } - tryReplay(t, s, cb, "replay failed to get the next block") - - // 4. bad payload - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - sb := skipchain.NewSkipBlock() - sb.Roster = s.roster - sb.Payload = []byte{1, 1, 1, 1, 1} - sb.ForwardLink = []*skipchain.ForwardLink{{}} - return sb, nil - } - tryReplay(t, s, cb, "Error while decoding field") - - // 5. bad data - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - sb := skipchain.NewSkipBlock() - sb.Roster = s.roster - sb.Payload = []byte{} - sb.Data = []byte{1, 1, 1, 1, 1} - sb.ForwardLink = []*skipchain.ForwardLink{{}} - return sb, nil - } - tryReplay(t, s, cb, "Error while decoding field") - - // 6. non matching hash - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - sb := skipchain.NewSkipBlock() - sb.Payload = []byte{} - sb.ForwardLink = []*skipchain.ForwardLink{{}} - return sb, nil - } - tryReplay(t, s, cb, "client transaction hash does not match") - - // 7. mismatching merkle trie root - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - sb, err := s.service().skService().GetSingleBlock(&skipchain.GetSingleBlock{ID: sib}) - if err != nil { - return nil, err - } - - var dHead DataHeader - err = protobuf.Decode(sb.Data, &dHead) - if err != nil { - return nil, err - } - - dHead.TrieRoot = []byte{1, 2, 3} - buf, err := protobuf.Encode(&dHead) - require.NoError(t, err) - sb.Data = buf - return sb, nil - } - tryReplay(t, s, cb, "merkle tree root doesn't match with trie root") + genesis := s.service().db().GetByID(s.genesis.Hash) + tryReplayBlock(t, s, genesis.ForwardLink[0].To, + "must start from genesis block") + + // 3. bad payload + sb := skipchain.NewSkipBlock() + sb.Roster = s.roster + sb.Payload = []byte{1, 1, 1, 1, 1} + sb.ForwardLink = []*skipchain.ForwardLink{{}} + require.NoError(t, forceStoreBlock(s, sb)) + tryReplayBlock(t, s, sb.Hash, "Error while decoding field") + + // 4. bad data + sb.Payload = genesis.Payload + sb.Data = []byte{1, 1, 1, 1, 1} + require.NoError(t, forceStoreBlock(s, sb)) + tryReplayBlock(t, s, sb.Hash, "Error while decoding field") + + // 5. non matching hash + sb.Data = []byte{} + sb.ForwardLink = []*skipchain.ForwardLink{{}} + require.NoError(t, forceStoreBlock(s, sb)) + tryReplayBlock(t, s, sb.Hash, "client transaction hash does not match") + + // 6. mismatching merkle trie root + sb = s.service().db().GetByID(s.genesis.SkipChainID()) + var dHead DataHeader + require.NoError(t, protobuf.Decode(sb.Data, &dHead)) + dHead.TrieRoot = []byte{1, 2, 3} + buf, err := protobuf.Encode(&dHead) + require.NoError(t, err) + sb.Data = buf + require.NoError(t, forceStoreBlock(s, sb)) + tryReplayBlock(t, s, sb.Hash, "merkle tree root doesn't match with trie root") + + // 7. failing instruction + sb = s.service().db().GetByID(s.genesis.SkipChainID()) + var dBody DataBody + require.NoError(t, protobuf.Decode(sb.Payload, &dBody)) + dBody.TxResults = append(dBody.TxResults, TxResult{ + Accepted: true, + ClientTransaction: ClientTransaction{ + Instructions: Instructions{Instruction{}}, + }, + }) + buf, err = protobuf.Encode(&dBody) + require.NoError(t, err) + sb.Payload = buf + require.NoError(t, protobuf.Decode(sb.Data, &dHead)) + dHead.ClientTransactionHash = dBody.TxResults.Hash() + buf, err = protobuf.Encode(&dHead) + require.NoError(t, err) + sb.Data = buf + require.NoError(t, forceStoreBlock(s, sb)) + tryReplayBlock(t, s, sb.Hash, "instruction verification failed") +} - // 8. failing instruction - cb = func(sib skipchain.SkipBlockID) (*skipchain.SkipBlock, error) { - sb, err := s.service().skService().GetSingleBlock(&skipchain.GetSingleBlock{ID: sib}) - if err != nil { - return nil, err - } +// stdFetcher is a fetcher method that outputs using the onet.log library. +type stdFetcher struct { +} - var dBody DataBody - err = protobuf.Decode(sb.Payload, &dBody) - if err != nil { - return nil, err - } +func (sf stdFetcher) LogNewBlock(sb *skipchain.SkipBlock) { + log.Infof("Replaying block at index %d", sb.Index) +} - dBody.TxResults = append(dBody.TxResults, TxResult{ - Accepted: true, - ClientTransaction: ClientTransaction{ - Instructions: Instructions{Instruction{}}, - }, - }) - buf, err := protobuf.Encode(&dBody) - require.NoError(t, err) - sb.Payload = buf +func (sf stdFetcher) LogWarn(sb *skipchain.SkipBlock, msg, dump string) { + log.Info(msg, dump) +} - var dHead DataHeader - err = protobuf.Decode(sb.Data, &dHead) - if err != nil { - return nil, err +func (sf stdFetcher) LogAppliedBlock(sb *skipchain.SkipBlock, + head DataHeader, body DataBody) { + txAccepted := 0 + for _, tx := range body.TxResults { + if tx.Accepted { + txAccepted++ } - - dHead.ClientTransactionHash = dBody.TxResults.Hash() - buf, err = protobuf.Encode(&dHead) - require.NoError(t, err) - sb.Data = buf - return sb, nil } - tryReplay(t, s, cb, "instruction verification failed") + t := time.Unix(head.Timestamp/1e9, 0) + log.Infof("Got correct block from %s with %d txs, "+ + "out of which %d txs got accepted", + t.String(), len(body.TxResults), txAccepted) }