diff --git a/go.mod b/go.mod index bd513aad..fcaaee2e 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/unicitynetwork/bft-go-base v1.0.3-0.20260316092951-afcfbc83f42f + github.com/unicitynetwork/bft-go-base v1.1.0 go.etcd.io/bbolt v1.4.0 go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 diff --git a/go.sum b/go.sum index 0832bbad..99505aaf 100644 --- a/go.sum +++ b/go.sum @@ -462,8 +462,8 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/unicitynetwork/bft-go-base v1.0.3-0.20260316092951-afcfbc83f42f h1:HiqLjm+aM0VX5oLJLAEiqZCUnQhf1KrL6rCpco9FMFA= -github.com/unicitynetwork/bft-go-base v1.0.3-0.20260316092951-afcfbc83f42f/go.mod h1:hBnOG52VRy/vpgIBUulTgk7PBTwODZ2xkVjCEu5yRcQ= +github.com/unicitynetwork/bft-go-base v1.1.0 h1:x1+kX0X+n4CmNibBs0f8oWwoZ5UCW40Gaq6WYhUcUZQ= +github.com/unicitynetwork/bft-go-base v1.1.0/go.mod h1:hBnOG52VRy/vpgIBUulTgk7PBTwODZ2xkVjCEu5yRcQ= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= diff --git a/rootchain/consensus/storage/block_executor_test.go b/rootchain/consensus/storage/block_executor_test.go index f6f8991b..cc66b804 100644 --- a/rootchain/consensus/storage/block_executor_test.go +++ b/rootchain/consensus/storage/block_executor_test.go @@ -251,7 +251,7 @@ func TestExecutedBlock_Extend(t *testing.T) { func TestExecutedBlock_GenerateCertificates(t *testing.T) { const partitionID1 types.PartitionID = 1 const partitionID2 types.PartitionID = 2 - rh, err := hex.DecodeString("51592107828763663BE3378AD1F4BAE7D9C1A921DEEC1A6B28247770A8B4F526") + rh, err := hex.DecodeString("CC44C256E1AC47E941FCA307FABFE7BF27F318175A865D6891D31988AE90DDF7") require.NoError(t, err) validBlock := func() *ExecutedBlock { @@ -454,7 +454,8 @@ func Test_ExecutedBlock_serialization(t *testing.T) { Leader: "ldr", }, UC: types.UnicityCertificate{ - Version: 1, + Version: 1, + ShardTreeCertificate: types.NewShardTreeCertificate(), }, }, } diff --git a/rootchain/consensus/storage/block_tree_test.go b/rootchain/consensus/storage/block_tree_test.go index 60aa9252..bffe4a9c 100644 --- a/rootchain/consensus/storage/block_tree_test.go +++ b/rootchain/consensus/storage/block_tree_test.go @@ -486,7 +486,7 @@ func Test_BlockTree_Add(t *testing.T) { require.NoError(t, err) k := types.PartitionShardID{PartitionID: 1, ShardID: types.ShardID{}.Key()} b.ShardState.States[k] = &ShardInfo{PartitionID: 1, IR: &types.InputRecord{}} - b.RootHash = hexToBytes("F8C1F929F9E718FE5B19DD72BFD23802FFFE5FAC21711BF425548548262942E5") + b.RootHash = hexToBytes("02D6563BFE92C38E0FD37A647F982B49C5B8980A441BCD395E5CC304E2217FCD") commitQc := &drctypes.QuorumCert{ VoteInfo: &drctypes.RoundInfo{ diff --git a/rootchain/consensus/storage/db_bolt.go b/rootchain/consensus/storage/db_bolt.go index 1c953ca6..0767d8e5 100644 --- a/rootchain/consensus/storage/db_bolt.go +++ b/rootchain/consensus/storage/db_bolt.go @@ -37,6 +37,8 @@ type BoltDB struct { db *bbolt.DB } +const currentDBVersion uint64 = 1 + func NewBoltStorage(file string) (db BoltDB, err error) { _, err = os.Stat(file) newDB := err != nil && errors.Is(err, fs.ErrNotExist) @@ -53,18 +55,50 @@ func NewBoltStorage(file string) (db BoltDB, err error) { }() if newDB { - if err := db.db.Update(initVersion1Buckets); err != nil { + if err := db.db.Update(initBuckets); err != nil { return db, fmt.Errorf("initializing new database: %w", err) } - } else { - if err := db.migrateTo(1); err != nil { - return db, fmt.Errorf("upgrading DB version: %w", err) - } + return db, nil } + ver, err := db.getVersion() + if err != nil { + return db, fmt.Errorf("reading database version: %w", err) + } + if ver != currentDBVersion { + return db, fmt.Errorf("unsupported database version %d, expected %d", ver, currentDBVersion) + } return db, nil } +// initBuckets creates the bucket layout and writes the current version marker +// into a fresh bbolt database. +func initBuckets(tx *bbolt.Tx) error { + if _, err := tx.CreateBucket(bucketBlocks); err != nil { + return fmt.Errorf("creating bucket for blocks: %w", err) + } + if _, err := tx.CreateBucket(bucketCertificates); err != nil { + return fmt.Errorf("creating bucket for certificates: %w", err) + } + if _, err := tx.CreateBucket(bucketVotes); err != nil { + return fmt.Errorf("creating bucket for votes: %w", err) + } + b, err := tx.CreateBucket(bucketSafety) + if err != nil { + return fmt.Errorf("creating bucket for safety: %w", err) + } + if err := writeUint64(b, keyHighestQc, rctypes.GenesisRootRound); err != nil { + return fmt.Errorf("storing highest QC round: %w", err) + } + if err := writeUint64(b, keyHighestVoted, rctypes.GenesisRootRound); err != nil { + return fmt.Errorf("storing highest voted round: %w", err) + } + if _, err := tx.CreateBucket(bucketMetadata); err != nil { + return fmt.Errorf("creating bucket for metadata: %w", err) + } + return setVersion(tx, currentDBVersion) +} + func (db BoltDB) Close() error { return db.db.Close() } var errNoBlocksBucket = errors.New("blocks bucket not found") @@ -321,39 +355,11 @@ func (db BoltDB) SetHighestQcRound(qcRound, votedRound uint64) error { }) } -/* -migrateTo upgrades database to version "ver" if the current version is older. -*/ -func (db BoltDB) migrateTo(ver uint64) error { - curVer, err := db.getVersion() - if err != nil { - return fmt.Errorf("determining current version of the database: %w", err) - } - if curVer > ver { - return fmt.Errorf("downgrading database version not supported, current is %d, asking for %d", curVer, ver) - } - - for ; curVer < ver; curVer++ { - switch curVer { - case 0: - err = db.migrate_0_to_1() - default: - return fmt.Errorf("migration from version %d to the next version not implemented", curVer) - } - - if err != nil { - return fmt.Errorf("migrating from version %d: %w", curVer, err) - } - } - return nil -} - func (db BoltDB) getVersion() (ver uint64, _ error) { return ver, db.db.View(func(tx *bbolt.Tx) (err error) { b := tx.Bucket(bucketMetadata) if b == nil { - // no bucket, must be version 0 database - return nil + return errors.New("metadata bucket not found") } ver, err = readUint64(b, keyDbVersion) return err diff --git a/rootchain/consensus/storage/db_migrate_0_1.go b/rootchain/consensus/storage/db_migrate_0_1.go deleted file mode 100644 index 5f2b6e9e..00000000 --- a/rootchain/consensus/storage/db_migrate_0_1.go +++ /dev/null @@ -1,152 +0,0 @@ -package storage - -import ( - "encoding/binary" - "errors" - "fmt" - "slices" - "strings" - - "go.etcd.io/bbolt" - - rctypes "github.com/unicitynetwork/bft-core/rootchain/consensus/types" - "github.com/unicitynetwork/bft-go-base/types" -) - -/* -init empty DB to version 1 structure (create buckets and default values) -*/ -func initVersion1Buckets(tx *bbolt.Tx) error { - if _, err := tx.CreateBucket(bucketBlocks); err != nil { - return fmt.Errorf("creating bucket for blocks: %w", err) - } - if _, err := tx.CreateBucket(bucketCertificates); err != nil { - return fmt.Errorf("creating bucket for certificates: %w", err) - } - if _, err := tx.CreateBucket(bucketVotes); err != nil { - return fmt.Errorf("creating bucket for votes: %w", err) - } - - b, err := tx.CreateBucket(bucketSafety) - if err != nil { - return fmt.Errorf("creating bucket for votes: %w", err) - } - if err = writeUint64(b, keyHighestQc, rctypes.GenesisRootRound); err != nil { - return fmt.Errorf("storing highest QC round: %w", err) - } - if err = writeUint64(b, keyHighestVoted, rctypes.GenesisRootRound); err != nil { - return fmt.Errorf("storing highest voted round: %w", err) - } - - if _, err := tx.CreateBucket(bucketMetadata); err != nil { - return fmt.Errorf("creating bucket for metadata: %w", err) - } - return setVersion(tx, 1) -} - -/* -migrate_0_to_1 migrate "everything in 'default' bucket" to type safe storage implementation -*/ -func (db BoltDB) migrate_0_to_1() error { - return db.db.Update(func(tx *bbolt.Tx) error { - // read current data - b := tx.Bucket([]byte("default")) - if b == nil { - return errors.New(`the "default" bucket not found`) - } - var vote, tc, highQCR, highVR []byte - var blocks []*ExecutedBlock - err := b.ForEach(func(k, v []byte) error { - switch keyStr := string(k); { - case strings.HasPrefix(keyStr, "block_"): - var b ExecutedBlock - if err := types.Cbor.Unmarshal(v, &b); err != nil { - return fmt.Errorf("loading block %x: %w", k, err) - } - blocks = append(blocks, &b) - case keyStr == string("vote"): - vote = slices.Clone(v) - case keyStr == string("tc"): - tc = slices.Clone(v) - case keyStr == string("votedRound"): - highVR = slices.Clone(v) - case keyStr == string("qcRound"): - highQCR = slices.Clone(v) - default: - return fmt.Errorf("unknown key %x", k) - } - return nil - }) - if err != nil { - return fmt.Errorf("loading current data: %w", err) - } - - // create new DB bucket structure - if err := initVersion1Buckets(tx); err != nil { - return err - } - - // move blocks into blocks bucket - if b = tx.Bucket(bucketBlocks); b == nil { - return fmt.Errorf("bucket %s doesn't exist", bucketBlocks) - } - for _, v := range blocks { - data, err := types.Cbor.Marshal(v) - if err != nil { - return fmt.Errorf("serializing block: %w", err) - } - if err := b.Put(binary.BigEndian.AppendUint64(make([]byte, 0, 8), v.GetRound()), data); err != nil { - return fmt.Errorf("saving block %d: %w", v.GetRound(), err) - } - } - - // move votes - if vote != nil { - if b = tx.Bucket(bucketVotes); b == nil { - return fmt.Errorf("bucket %s doesn't exist", bucketVotes) - } - if err := b.Put(keyVote, vote); err != nil { - return fmt.Errorf("moving vote: %w", err) - } - } - - // move TC - if tc != nil { - if b = tx.Bucket(bucketCertificates); b == nil { - return fmt.Errorf("bucket %s doesn't exist", bucketCertificates) - } - if err := b.Put(keyTimeoutCert, tc); err != nil { - return fmt.Errorf("moving vote: %w", err) - } - } - - // move safety module state - if highQCR != nil { - if b = tx.Bucket(bucketSafety); b == nil { - return fmt.Errorf("bucket %s doesn't exist", bucketSafety) - } - var round uint64 - if err := types.Cbor.Unmarshal(highQCR, &round); err != nil { - return fmt.Errorf("loading high QC round: %w", err) - } - if err = writeUint64(b, keyHighestQc, round); err != nil { - return err - } - } - if highVR != nil { - if b = tx.Bucket(bucketSafety); b == nil { - return fmt.Errorf("bucket %s doesn't exist", bucketSafety) - } - var round uint64 - if err := types.Cbor.Unmarshal(highVR, &round); err != nil { - return fmt.Errorf("loading high voted round: %w", err) - } - if err = writeUint64(b, keyHighestVoted, round); err != nil { - return err - } - } - - // delete version 0 data - return tx.DeleteBucket([]byte("default")) - }) -} diff --git a/rootchain/consensus/storage/db_migrate_test.go b/rootchain/consensus/storage/db_migrate_test.go deleted file mode 100644 index ebc08f44..00000000 --- a/rootchain/consensus/storage/db_migrate_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package storage - -import ( - "embed" - "errors" - "os" - "path/filepath" - "slices" - "testing" - "time" - - "github.com/stretchr/testify/require" - "go.etcd.io/bbolt" - - "github.com/unicitynetwork/bft-core/network/protocol/abdrc" - "github.com/unicitynetwork/bft-core/rootchain/consensus/types" -) - -//go:embed testdata/*.db -var testdata embed.FS - -func Test_migrate_rootchain_0_to_1(t *testing.T) { - dir := t.TempDir() - - t.Run("invalid source database", func(t *testing.T) { - dbName := filepath.Join(dir, "no_bucket.db") - // create empty bolt DB - bDB, err := bbolt.Open(dbName, 0600, &bbolt.Options{Timeout: time.Second}) - require.NoError(t, err) - require.NoError(t, bDB.Close()) - - /*** no default bucket - file exist so we also expect it to contain data ***/ - db, err := NewBoltStorage(dbName) - require.EqualError(t, err, `upgrading DB version: migrating from version 0: the "default" bucket not found`) - require.Nil(t, db.db) - - /*** version 2 db ***/ - bDB, err = bbolt.Open(dbName, 0600, &bbolt.Options{Timeout: time.Second}) - require.NoError(t, err) - err = bDB.Update(func(tx *bbolt.Tx) error { - if _, err := tx.CreateBucket(bucketMetadata); err != nil { - return err - } - return setVersion(tx, 2) - }) - require.NoError(t, err) - require.NoError(t, bDB.Close()) - - db, err = NewBoltStorage(dbName) - require.EqualError(t, err, `upgrading DB version: downgrading database version not supported, current is 2, asking for 1`) - require.Nil(t, db.db) - }) - - t.Run("version 1 bucket already exists", func(t *testing.T) { - dbName := filepath.Join(dir, "bucket_exists.db") - // create new db with ver 1 structure - db, err := NewBoltStorage(dbName) - require.NoError(t, err) - // to trigger upgrade on (next) open delete metadata bucket and add "default" - err = db.db.Update(func(tx *bbolt.Tx) error { - if _, err := tx.CreateBucket([]byte("default")); err != nil { - return err - } - return tx.DeleteBucket(bucketMetadata) - }) - require.NoError(t, err) - require.NoError(t, db.Close()) - - for _, bucketName := range [][]byte{bucketBlocks, bucketCertificates, bucketVotes, bucketSafety} { - _, err := NewBoltStorage(dbName) - require.ErrorIs(t, err, bbolt.ErrBucketExists) - // now delete this bucket to trigger error on the next bucket which shouldn't exist - bDB, err := bbolt.Open(dbName, 0600, &bbolt.Options{Timeout: time.Second}) - require.NoError(t, err) - err = bDB.Update(func(tx *bbolt.Tx) error { - return tx.DeleteBucket(bucketName) - }) - require.NoError(t, err) - require.NoError(t, bDB.Close()) - } - }) - - // make a copy of known good version 0 database and return it's name (path) - ver0db := func(t *testing.T, fileName string) string { - b, err := testdata.ReadFile("testdata/rootchain_v0.db") - require.NoError(t, err) - dbName := filepath.Join(dir, fileName) - f, err := os.Create(dbName) - require.NoError(t, err) - _, err = f.Write(b) - require.NoError(t, err) - return dbName - } - - t.Run("invalid data", func(t *testing.T) { - // attempts to migrate version 0 DB which contains invalid data - we start - // with known good ver 0 DB and make different keys invalid - dbName := ver0db(t, "invData.db") - - makeInvalid := func(action func(b *bbolt.Bucket) error) { - bDB, err := bbolt.Open(dbName, 0600, &bbolt.Options{Timeout: time.Second}) - require.NoError(t, err) - require.NoError(t, bDB.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket([]byte("default")) - return action(b) - })) - require.NoError(t, bDB.Close()) - } - - // ver 0 DB stores "votedRound" as CBOR encoded int - makeInvalid(func(b *bbolt.Bucket) error { - return b.Put([]byte("votedRound"), []byte("not CBOR int")) - }) - _, err := NewBoltStorage(dbName) - require.EqualError(t, err, `upgrading DB version: migrating from version 0: loading high voted round: unexpected EOF`) - - // ver 0 DB stores "qcRound" as CBOR encoded int - makeInvalid(func(b *bbolt.Bucket) error { - return b.Put([]byte("qcRound"), []byte{0x81, 0}) - }) - _, err = NewBoltStorage(dbName) - require.EqualError(t, err, `upgrading DB version: migrating from version 0: loading high QC round: cbor: cannot unmarshal array into Go value of type uint64`) - - // unknown key - makeInvalid(func(b *bbolt.Bucket) error { - return b.Put([]byte("unknown key"), []byte{0, 0, 0, 0}) - }) - _, err = NewBoltStorage(dbName) - require.EqualError(t, err, `upgrading DB version: migrating from version 0: loading current data: unknown key 756e6b6e6f776e206b6579`) - - // invalid block data in the ver 0 DB - makeInvalid(func(b *bbolt.Bucket) error { - k := append([]byte("block_"), 0, 0, 0, 0, 0, 0x35, 0x9d, 0xbf) - return b.Put(k, []byte("not CBOR of a block")) - }) - _, err = NewBoltStorage(dbName) - require.EqualError(t, err, `upgrading DB version: migrating from version 0: loading current data: loading block 626c6f636b5f0000000000359dbf: cbor: 4 bytes of extraneous data starting at index 15`) - }) - - t.Run("success", func(t *testing.T) { - // make a copy of known version 0 database - dbName := ver0db(t, "success.db") - // migrate it to ver 1 - db, err := NewBoltStorage(dbName) - require.NoError(t, err) - defer db.Close() - // the DB version should be 1 - ver, err := db.getVersion() - require.NoError(t, err) - require.EqualValues(t, 1, ver, "DB version") - // bucket "default" must be gone - err = db.db.View(func(tx *bbolt.Tx) error { - if tx.Bucket([]byte("default")) != nil { - return errors.New("the 'default' bucket is still present") - } - return nil - }) - require.NoError(t, err) - - /*** check do we have all the expected data (ie migration from 0 to 1 was a success) ***/ - - require.EqualValues(t, 3513791, db.GetHighestQcRound()) - require.EqualValues(t, 3513792, db.GetHighestVotedRound()) - - msg, err := db.ReadLastVote() - require.NoError(t, err) - vm, ok := msg.(*abdrc.VoteMsg) - require.True(t, ok, "expected VoteMsg, got %T", msg) - require.Equal(t, db.GetHighestVotedRound(), vm.VoteInfo.RoundNumber) - - msg, err = db.ReadLastTC() - require.NoError(t, err) - tc, ok := msg.(*types.TimeoutCert) - require.True(t, ok, "expected TimeoutCert, got %T", msg) - require.EqualValues(t, 3354117, tc.GetRound()) - - blocks, err := db.LoadBlocks() - require.NoError(t, err) - require.Len(t, blocks, 67, "expected that there is 67 blocks in the DB") - // find the root block - idx := slices.IndexFunc(blocks, func(b *ExecutedBlock) bool { return b.CommitQc != nil }) - require.Equal(t, 2, idx) - require.EqualValues(t, 3513790, blocks[idx].GetRound()) - - // write committed block into DB again to trigger cleanup - require.NoError(t, db.WriteBlock(blocks[idx], true)) - // now should have only three blocks left - blocks, err = db.LoadBlocks() - require.NoError(t, err) - require.Len(t, blocks, 3) - require.EqualValues(t, 3513792, blocks[0].GetRound()) - require.EqualValues(t, 3513791, blocks[1].GetRound()) - require.EqualValues(t, 3513790, blocks[2].GetRound()) - - /* reopen the DB, should be valid ver 1 DB containing 3 blocks */ - require.NoError(t, db.Close()) - db, err = NewBoltStorage(dbName) - require.NoError(t, err) - defer db.Close() - blocks, err = db.LoadBlocks() - require.NoError(t, err) - require.Len(t, blocks, 3) - require.EqualValues(t, 3513792, blocks[0].GetRound()) - require.EqualValues(t, 3513791, blocks[1].GetRound()) - require.EqualValues(t, 3513790, blocks[2].GetRound()) - }) -} diff --git a/rootchain/consensus/storage/testdata/README.md b/rootchain/consensus/storage/testdata/README.md deleted file mode 100644 index af0b8c30..00000000 --- a/rootchain/consensus/storage/testdata/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Files for testing - -Files with "known content" for testing migrations. - - -## rootchain_v0.db - -Version 0 of the `rootchain.db` from live environment (21.05.2025). - -Contains: -- 67 blocks (bug, old blocks are not properly cleaned out), root block (block with non nil CommitQC) for round 3513790; -- high QC round: 3513791 -- high voted round: 3513792 -- vote message (`VoteMsg`) for round 3513792; -- timeout certificate (`TimeoutCert`) for round 3354117; \ No newline at end of file diff --git a/rootchain/consensus/storage/testdata/rootchain_v0.db b/rootchain/consensus/storage/testdata/rootchain_v0.db deleted file mode 100644 index 21f95210..00000000 Binary files a/rootchain/consensus/storage/testdata/rootchain_v0.db and /dev/null differ diff --git a/rootchain/consensus/storage/testdata/versions.md b/rootchain/consensus/storage/testdata/versions.md deleted file mode 100644 index ea36f0b5..00000000 --- a/rootchain/consensus/storage/testdata/versions.md +++ /dev/null @@ -1,19 +0,0 @@ - -## Version 1 - -Buckets -- `blocks`: keys are round numbers (uint64, big endian) and value is CBOR of the `ExecutedBlock`; -- `certificates`: key `tc` value is CBOR encoded `TimeoutCert` struct; -- `votes`: key `vote` CBOR encoded struct which contains vote type tag (timeout or consensus) and vote struct data; -- `safety`: keys `votedRound` and `qcRound` values are big endian uint64 respectively "highest voted round" and "highest QC round"; -- `metadata`: Contains key `version` which stores current database version as 64 bit uint (big-endian); - - -## Version 0 - -Bucket `default` contains keys: -- `block_N`: where `N` is round number (64bit uint bytes in big-endian). Data is CBOR of the `ExecutedBlock`; -- `tc`: latest timeout certificate (CBOR encoded `TimeoutCert` struct); -- `vote`: latest timeout vote or consensus vote, value is CBOR which has a tag for vote type and raw CBOR of the vote struct; -- `votedRound` - CBOR encoded int of highest voted round; -- `qcRound` - CBOR encoded int of highest QC round;