From 55370b01985bfbd4fd5776692450e3403227f65a Mon Sep 17 00:00:00 2001 From: Matt Kocubinski Date: Fri, 1 Mar 2024 08:17:48 -0800 Subject: [PATCH 1/4] feat(store): remove RawDB dependency from snapshot store (#19608) --- store/migration/manager_test.go | 2 +- store/snapshots/helpers_test.go | 4 +- store/snapshots/manager.go | 5 +- store/snapshots/manager_test.go | 3 +- store/snapshots/store.go | 218 ++++++++++++++++++++------------ store/snapshots/store_test.go | 7 +- 6 files changed, 145 insertions(+), 94 deletions(-) diff --git a/store/migration/manager_test.go b/store/migration/manager_test.go index b02ac5db94a9..c60a2376d4bd 100644 --- a/store/migration/manager_test.go +++ b/store/migration/manager_test.go @@ -31,7 +31,7 @@ func setupMigrationManager(t *testing.T) (*Manager, *commitment.CommitStore) { commitStore, err := commitment.NewCommitStore(multiTrees, db, nil, log.NewNopLogger()) require.NoError(t, err) - snapshotsStore, err := snapshots.NewStore(db, t.TempDir()) + snapshotsStore, err := snapshots.NewStore(t.TempDir()) require.NoError(t, err) snapshotsManager := snapshots.NewManager(snapshotsStore, snapshots.NewSnapshotOptions(1500, 2), commitStore, nil, nil, log.NewNopLogger()) diff --git a/store/snapshots/helpers_test.go b/store/snapshots/helpers_test.go index 8ed5d3d594b1..33dcc14fa6f4 100644 --- a/store/snapshots/helpers_test.go +++ b/store/snapshots/helpers_test.go @@ -17,7 +17,6 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" "cosmossdk.io/store/v2" - dbm "cosmossdk.io/store/v2/db" "cosmossdk.io/store/v2/snapshots" snapshotstypes "cosmossdk.io/store/v2/snapshots/types" ) @@ -189,7 +188,7 @@ func (m *mockErrorCommitSnapshotter) SupportedFormats() []uint32 { // The snapshot will complete when the returned closer is called. func setupBusyManager(t *testing.T) *snapshots.Manager { t.Helper() - store, err := snapshots.NewStore(dbm.NewMemDB(), t.TempDir()) + store, err := snapshots.NewStore(t.TempDir()) require.NoError(t, err) hung := newHungCommitSnapshotter() mgr := snapshots.NewManager(store, opts, hung, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) @@ -292,6 +291,7 @@ func (s *extSnapshotter) RestoreExtension(height uint64, format uint32, payloadR // GetTempDir returns a writable temporary director for the test to use. func GetTempDir(tb testing.TB) string { + //return "/tmp/snapshots" tb.Helper() // os.MkDir() is used instead of testing.T.TempDir() // see https://github.com/cosmos/cosmos-sdk/pull/8475 and diff --git a/store/snapshots/manager.go b/store/snapshots/manager.go index ad29da179671..b713da7134f4 100644 --- a/store/snapshots/manager.go +++ b/store/snapshots/manager.go @@ -599,7 +599,4 @@ func (m *Manager) snapshot(height int64) { } // Close the snapshot database. -func (m *Manager) Close() error { - m.logger.Info("snapshotManager Close Database") - return m.store.db.Close() -} +func (m *Manager) Close() error { return nil } diff --git a/store/snapshots/manager_test.go b/store/snapshots/manager_test.go index da598f7a6910..987d0c3b0e81 100644 --- a/store/snapshots/manager_test.go +++ b/store/snapshots/manager_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/require" "cosmossdk.io/log" - dbm "cosmossdk.io/store/v2/db" "cosmossdk.io/store/v2/snapshots" "cosmossdk.io/store/v2/snapshots/types" ) @@ -237,7 +236,7 @@ func TestManager_Restore(t *testing.T) { func TestManager_TakeError(t *testing.T) { snapshotter := &mockErrorCommitSnapshotter{} - store, err := snapshots.NewStore(dbm.NewMemDB(), GetTempDir(t)) + store, err := snapshots.NewStore(GetTempDir(t)) require.NoError(t, err) manager := snapshots.NewManager(store, opts, snapshotter, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) diff --git a/store/snapshots/store.go b/store/snapshots/store.go index ad2179ddbac7..c7fef66bb424 100644 --- a/store/snapshots/store.go +++ b/store/snapshots/store.go @@ -3,12 +3,15 @@ package snapshots import ( "crypto/sha256" "encoding/binary" + "fmt" "hash" "io" "math" "os" "path/filepath" + "sort" "strconv" + "strings" "sync" "github.com/cosmos/gogoproto/proto" @@ -25,7 +28,6 @@ const ( // Store is a snapshot store, containing snapshot metadata and binary chunks. type Store struct { - db store.RawDB dir string mtx sync.Mutex @@ -33,7 +35,7 @@ type Store struct { } // NewStore creates a new snapshot store. -func NewStore(db store.RawDB, dir string) (*Store, error) { +func NewStore(dir string) (*Store, error) { if dir == "" { return nil, errors.Wrap(store.ErrLogic, "snapshot directory not given") } @@ -41,9 +43,12 @@ func NewStore(db store.RawDB, dir string) (*Store, error) { if err != nil { return nil, errors.Wrapf(err, "failed to create snapshot directory %q", dir) } + err = os.MkdirAll(filepath.Join(dir, "metadata"), 0o750) + if err != nil { + return nil, errors.Wrapf(err, "failed to create snapshot metadata directory %q", dir) + } return &Store{ - db: db, dir: dir, saving: make(map[uint64]bool), }, nil @@ -58,32 +63,25 @@ func (s *Store) Delete(height uint64, format uint32) error { return errors.Wrapf(store.ErrConflict, "snapshot for height %v format %v is currently being saved", height, format) } - b := s.db.NewBatch() - defer b.Close() - if err := b.Delete(encodeKey(height, format)); err != nil { - return errors.Wrapf(err, "failed to delete item in the batch") - } - if err := b.WriteSync(); err != nil { - return errors.Wrapf(err, "failed to delete snapshot for height %v format %v", - height, format) - } if err := os.RemoveAll(s.pathSnapshot(height, format)); err != nil { - return errors.Wrapf(err, "failed to delete snapshot chunks for height %v format %v", - height, format) + return errors.Wrapf(err, "failed to delete snapshot chunks for height %v format %v", height, format) + } + if err := os.RemoveAll(s.pathMetadata(height, format)); err != nil { + return errors.Wrapf(err, "failed to delete snapshot metadata for height %v format %v", height, format) } return nil } // Get fetches snapshot info from the database. func (s *Store) Get(height uint64, format uint32) (*types.Snapshot, error) { - bytes, err := s.db.Get(encodeKey(height, format)) + if _, err := os.Stat(s.pathMetadata(height, format)); os.IsNotExist(err) { + return nil, nil + } + bytes, err := os.ReadFile(s.pathMetadata(height, format)) if err != nil { return nil, errors.Wrapf(err, "failed to fetch snapshot metadata for height %v format %v", height, format) } - if bytes == nil { - return nil, nil - } snapshot := &types.Snapshot{} err = proto.Unmarshal(bytes, snapshot) if err != nil { @@ -96,44 +94,62 @@ func (s *Store) Get(height uint64, format uint32) (*types.Snapshot, error) { return snapshot, nil } -// Get fetches the latest snapshot from the database, if any. +// GetLatest fetches the latest snapshot from the database, if any. func (s *Store) GetLatest() (*types.Snapshot, error) { - iter, err := s.db.ReverseIterator(encodeKey(0, 0), encodeKey(uint64(math.MaxUint64), math.MaxUint32)) + metadata, err := os.ReadDir(s.pathMetadataDir()) if err != nil { - return nil, errors.Wrap(err, "failed to find latest snapshot") + return nil, errors.Wrap(err, "failed to list snapshot metadata") } - defer iter.Close() + if len(metadata) == 0 { + return nil, nil + } + // file system may not guarantee the order of the files, so we sort them lexically + sort.Slice(metadata, func(i, j int) bool { return metadata[i].Name() < metadata[j].Name() }) - var snapshot *types.Snapshot - if iter.Valid() { - snapshot = &types.Snapshot{} - err := proto.Unmarshal(iter.Value(), snapshot) - if err != nil { - return nil, errors.Wrap(err, "failed to decode latest snapshot") - } + path := filepath.Join(s.pathMetadataDir(), metadata[len(metadata)-1].Name()) + if err := s.validateMetadataPath(path); err != nil { + return nil, err + } + bz, err := os.ReadFile(path) + if err != nil { + return nil, errors.Wrapf(err, "failed to read latest snapshot metadata %s", path) } - err = iter.Error() - return snapshot, errors.Wrap(err, "failed to find latest snapshot") + + snapshot := &types.Snapshot{} + err = proto.Unmarshal(bz, snapshot) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode latest snapshot metadata %s", path) + } + return snapshot, nil } // List lists snapshots, in reverse order (newest first). func (s *Store) List() ([]*types.Snapshot, error) { - iter, err := s.db.ReverseIterator(encodeKey(0, 0), encodeKey(uint64(math.MaxUint64), math.MaxUint32)) + metadata, err := os.ReadDir(s.pathMetadataDir()) if err != nil { - return nil, errors.Wrap(err, "failed to list snapshots") + return nil, errors.Wrap(err, "failed to list snapshot metadata") } - defer iter.Close() + // file system may not guarantee the order of the files, so we sort them lexically + sort.Slice(metadata, func(i, j int) bool { return metadata[i].Name() < metadata[j].Name() }) - snapshots := make([]*types.Snapshot, 0) - for ; iter.Valid(); iter.Next() { + snapshots := make([]*types.Snapshot, len(metadata)) + for i, entry := range metadata { + path := filepath.Join(s.pathMetadataDir(), entry.Name()) + if err := s.validateMetadataPath(path); err != nil { + return nil, err + } + bz, err := os.ReadFile(path) + if err != nil { + return nil, errors.Wrapf(err, "failed to read snapshot metadata %s", entry.Name()) + } snapshot := &types.Snapshot{} - err := proto.Unmarshal(iter.Value(), snapshot) + err = proto.Unmarshal(bz, snapshot) if err != nil { - return nil, errors.Wrap(err, "failed to decode snapshot info") + return nil, errors.Wrapf(err, "failed to decode snapshot metadata %s", entry.Name()) } - snapshots = append(snapshots, snapshot) + snapshots[len(metadata)-1-i] = snapshot } - return snapshots, iter.Error() + return snapshots, nil } // Load loads a snapshot (both metadata and binary chunks). The chunks must be consumed and closed. @@ -188,25 +204,25 @@ func (s *Store) loadChunkFile(height uint64, format, chunk uint32) (io.ReadClose // Prune removes old snapshots. The given number of most recent heights (regardless of format) are retained. func (s *Store) Prune(retain uint32) (uint64, error) { - iter, err := s.db.ReverseIterator(encodeKey(0, 0), encodeKey(uint64(math.MaxUint64), math.MaxUint32)) + metadata, err := os.ReadDir(s.pathMetadataDir()) if err != nil { - return 0, errors.Wrap(err, "failed to prune snapshots") + return 0, errors.Wrap(err, "failed to list snapshot metadata") } - defer iter.Close() pruned := uint64(0) prunedHeights := make(map[uint64]bool) skip := make(map[uint64]bool) - for ; iter.Valid(); iter.Next() { - height, format, err := decodeKey(iter.Key()) + for i := len(metadata) - 1; i >= 0; i-- { + height, format, err := s.parseMetadataFilename(metadata[i].Name()) if err != nil { - return 0, errors.Wrap(err, "failed to prune snapshots") + return 0, err } + if skip[height] || uint32(len(skip)) < retain { skip[height] = true continue } - err = s.Delete(height, format) + err = s.Delete(height, uint32(format)) if err != nil { return 0, errors.Wrap(err, "failed to prune snapshots") } @@ -223,7 +239,7 @@ func (s *Store) Prune(retain uint32) (uint64, error) { } } } - return pruned, iter.Error() + return pruned, nil } // Save saves a snapshot to disk, returning it. @@ -249,37 +265,24 @@ func (s *Store) Save( s.mtx.Unlock() }() - exists, err := s.db.Has(encodeKey(height, format)) - if err != nil { - return nil, err - } - if exists { - return nil, errors.Wrapf(store.ErrConflict, - "snapshot already exists for height %v format %v", height, format) - } - snapshot := &types.Snapshot{ Height: height, Format: format, } - dirCreated := false + // create height directory or do nothing + if err := os.MkdirAll(s.pathHeight(height), 0o750); err != nil { + return nil, errors.Wrapf(err, "failed to create snapshot directory for height %v", height) + } + // create format directory or fail (if for example the format directory already exists) + if err := os.Mkdir(s.pathSnapshot(height, format), 0o750); err != nil { + return nil, errors.Wrapf(err, "failed to create snapshot directory for height %v format %v", height, format) + } + index := uint32(0) snapshotHasher := sha256.New() chunkHasher := sha256.New() for chunkBody := range chunks { - // Only create the snapshot directory on encountering the first chunk. - // If the directory disappears during chunk saving, - // the whole operation will fail anyway. - if !dirCreated { - dir := s.pathSnapshot(height, format) - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, errors.Wrapf(err, "failed to create snapshot directory %q", dir) - } - - dirCreated = true - } - if err := s.saveChunk(chunkBody, index, snapshot, chunkHasher, snapshotHasher); err != nil { return nil, err } @@ -332,13 +335,9 @@ func (s *Store) saveSnapshot(snapshot *types.Snapshot) error { if err != nil { return errors.Wrap(err, "failed to encode snapshot metadata") } - b := s.db.NewBatch() - defer b.Close() - if err := b.Set(encodeKey(snapshot.Height, snapshot.Format), value); err != nil { - return errors.Wrap(err, "failed to set snapshot in batch") - } - if err := b.WriteSync(); err != nil { - return errors.Wrap(err, "failed to store snapshot") + err = os.WriteFile(s.pathMetadata(snapshot.Height, snapshot.Format), value, 0o600) + if err != nil { + return errors.Wrap(err, "failed to write snapshot metadata") } return nil } @@ -353,13 +352,52 @@ func (s *Store) pathSnapshot(height uint64, format uint32) string { return filepath.Join(s.pathHeight(height), strconv.FormatUint(uint64(format), 10)) } +func (s *Store) pathMetadataDir() string { + return filepath.Join(s.dir, "metadata") +} + +// pathMetadata generates a snapshot metadata path. +func (s *Store) pathMetadata(height uint64, format uint32) string { + return filepath.Join(s.pathMetadataDir(), fmt.Sprintf("%020d-%08d", height, format)) +} + // PathChunk generates a snapshot chunk path. func (s *Store) PathChunk(height uint64, format, chunk uint32) string { return filepath.Join(s.pathSnapshot(height, format), strconv.FormatUint(uint64(chunk), 10)) } -// decodeKey decodes a snapshot key. -func decodeKey(k []byte) (uint64, uint32, error) { +func (s *Store) parseMetadataFilename(filename string) (height uint64, format uint32, err error) { + parts := strings.Split(filename, "-") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("invalid snapshot metadata filename %s", filename) + } + height, err = strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return 0, 0, errors.Wrapf(err, "invalid snapshot metadata filename %s", filename) + } + var f uint64 + f, err = strconv.ParseUint(parts[1], 10, 32) + if err != nil { + return 0, 0, errors.Wrapf(err, "invalid snapshot metadata filename %s", filename) + } + format = uint32(f) + if filename != filepath.Base(s.pathMetadata(height, uint32(format))) { + return 0, 0, fmt.Errorf("invalid snapshot metadata filename %s", filename) + } + return height, format, nil +} + +func (s *Store) validateMetadataPath(path string) error { + dir, f := filepath.Split(path) + if dir != fmt.Sprintf("%s/", s.pathMetadataDir()) { + return fmt.Errorf("invalid snapshot metadata path %s", path) + } + _, _, err := s.parseMetadataFilename(f) + return err +} + +// legacyV1DecodeKey decodes a legacy snapshot key used in a raw kv store. +func legacyV1DecodeKey(k []byte) (uint64, uint32, error) { if len(k) != 13 { return 0, 0, errors.Wrapf(store.ErrLogic, "invalid snapshot key with length %v", len(k)) } @@ -372,11 +410,29 @@ func decodeKey(k []byte) (uint64, uint32, error) { return height, format, nil } -// encodeKey encodes a snapshot key. -func encodeKey(height uint64, format uint32) []byte { +// legacyV1EncodeKey encodes a snapshot key for use in a raw kv store. +func legacyV1EncodeKey(height uint64, format uint32) []byte { k := make([]byte, 13) k[0] = keyPrefixSnapshot binary.BigEndian.PutUint64(k[1:], height) binary.BigEndian.PutUint32(k[9:], format) return k } + +func (s *Store) MigrateFromV1(db store.RawDB) error { + itr, err := db.Iterator(legacyV1EncodeKey(0, 0), legacyV1EncodeKey(math.MaxUint64, math.MaxUint32)) + if err != nil { + return err + } + defer itr.Close() + for ; itr.Valid(); itr.Next() { + height, format, err := legacyV1DecodeKey(itr.Key()) + if err != nil { + return err + } + if err := os.WriteFile(s.pathMetadata(height, format), itr.Value(), 0o600); err != nil { + return errors.Wrapf(err, "failed to write snapshot metadata %q", s.pathMetadata(height, format)) + } + } + return nil +} diff --git a/store/snapshots/store_test.go b/store/snapshots/store_test.go index 07f4d4a6d515..c6708ec8d73c 100644 --- a/store/snapshots/store_test.go +++ b/store/snapshots/store_test.go @@ -10,14 +10,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - dbm "cosmossdk.io/store/v2/db" "cosmossdk.io/store/v2/snapshots" "cosmossdk.io/store/v2/snapshots/types" ) func setupStore(t *testing.T) *snapshots.Store { t.Helper() - store, err := snapshots.NewStore(dbm.NewMemDB(), GetTempDir(t)) + store, err := snapshots.NewStore(GetTempDir(t)) require.NoError(t, err) _, err = store.Save(1, 1, makeChunks([][]byte{ @@ -42,13 +41,13 @@ func setupStore(t *testing.T) *snapshots.Store { func TestNewStore(t *testing.T) { tempdir := GetTempDir(t) - _, err := snapshots.NewStore(dbm.NewMemDB(), tempdir) + _, err := snapshots.NewStore(tempdir) require.NoError(t, err) } func TestNewStore_ErrNoDir(t *testing.T) { - _, err := snapshots.NewStore(dbm.NewMemDB(), "") + _, err := snapshots.NewStore("") require.Error(t, err) } From 7155a1cd5278879dd7a747e39888239959bf9083 Mon Sep 17 00:00:00 2001 From: Nikhil Vasan <97126437+nivasan1@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:25:33 -0500 Subject: [PATCH 2/4] Merge pull request from GHSA-95rx-m9m5-m94v * validate ExtendedCommit against LastCommit * test cases * lint --------- Co-authored-by: Marko --- baseapp/abci_test.go | 10 +- baseapp/abci_utils.go | 65 +++++++++-- baseapp/abci_utils_test.go | 222 +++++++++++++++++++++++++++++++++++-- 3 files changed, 280 insertions(+), 17 deletions(-) diff --git a/baseapp/abci_test.go b/baseapp/abci_test.go index a4f254260bea..3fd7d86bb647 100644 --- a/baseapp/abci_test.go +++ b/baseapp/abci_test.go @@ -1804,7 +1804,10 @@ func TestABCI_PrepareProposal_VoteExtensions(t *testing.T) { // set up baseapp prepareOpt := func(bapp *baseapp.BaseApp) { bapp.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { - err := baseapp.ValidateVoteExtensions(ctx, valStore, req.Height, bapp.ChainID(), req.LocalLastCommit) + ctx = ctx.WithBlockHeight(req.Height).WithChainID(bapp.ChainID()) + _, info := extendedCommitToLastCommit(req.LocalLastCommit) + ctx = ctx.WithCometInfo(info) + err := baseapp.ValidateVoteExtensions(ctx, valStore, req.LocalLastCommit) if err != nil { return nil, err } @@ -2111,7 +2114,10 @@ func TestBaseApp_VoteExtensions(t *testing.T) { app.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { txs := [][]byte{} - if err := baseapp.ValidateVoteExtensions(ctx, valStore, req.Height, app.ChainID(), req.LocalLastCommit); err != nil { + ctx = ctx.WithBlockHeight(req.Height).WithChainID(app.ChainID()) + _, info := extendedCommitToLastCommit(req.LocalLastCommit) + ctx = ctx.WithCometInfo(info) + if err := baseapp.ValidateVoteExtensions(ctx, valStore, req.LocalLastCommit); err != nil { return nil, err } // add all VE as txs (in a real scenario we would need to check signatures too) diff --git a/baseapp/abci_utils.go b/baseapp/abci_utils.go index f31446d540b7..450f4c9b235c 100644 --- a/baseapp/abci_utils.go +++ b/baseapp/abci_utils.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "slices" "github.com/cockroachdb/errors" abci "github.com/cometbft/cometbft/abci/types" @@ -14,6 +15,7 @@ import ( protoio "github.com/cosmos/gogoproto/io" "github.com/cosmos/gogoproto/proto" + "cosmossdk.io/core/comet" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/mempool" ) @@ -40,11 +42,19 @@ type ( func ValidateVoteExtensions( ctx sdk.Context, valStore ValidatorStore, - currentHeight int64, - chainID string, extCommit abci.ExtendedCommitInfo, ) error { + // Get values from context cp := ctx.ConsensusParams() + currentHeight := ctx.BlockHeight() + chainID := ctx.BlockHeader().ChainID + commitInfo := ctx.CometInfo().LastCommit + + // Check that both extCommit + commit are ordered in accordance with vp/address. + if err := validateExtendedCommitAgainstLastCommit(extCommit, commitInfo); err != nil { + return err + } + // Start checking vote extensions only **after** the vote extensions enable // height, because when `currentHeight == VoteExtensionsEnableHeight` // PrepareProposal doesn't get any vote extensions in its request. @@ -65,7 +75,6 @@ func ValidateVoteExtensions( sumVP int64 ) - cache := make(map[string]struct{}) for _, vote := range extCommit.Votes { totalVP += vote.Validator.Power @@ -90,12 +99,7 @@ func ValidateVoteExtensions( return fmt.Errorf("vote extensions enabled; received empty vote extension signature at height %d", currentHeight) } - // Ensure that the validator has not already submitted a vote extension. valConsAddr := sdk.ConsAddress(vote.Validator.Address) - if _, ok := cache[valConsAddr.String()]; ok { - return fmt.Errorf("duplicate validator; validator %s has already submitted a vote extension", valConsAddr.String()) - } - cache[valConsAddr.String()] = struct{}{} pubKeyProto, err := valStore.GetPubKeyByConsAddr(ctx, valConsAddr) if err != nil { @@ -141,6 +145,51 @@ func ValidateVoteExtensions( return nil } +// validateExtendedCommitAgainstLastCommit validates an ExtendedCommitInfo against a LastCommit. Specifically, +// it checks that the ExtendedCommit + LastCommit (for the same height), are consistent with each other + that +// they are ordered correctly (by voting power) in accordance with +// [comet](https://github.com/cometbft/cometbft/blob/4ce0277b35f31985bbf2c25d3806a184a4510010/types/validator_set.go#L784). +func validateExtendedCommitAgainstLastCommit(ec abci.ExtendedCommitInfo, lc comet.CommitInfo) error { + // check that the rounds are the same + if ec.Round != lc.Round { + return fmt.Errorf("extended commit round %d does not match last commit round %d", ec.Round, lc.Round) + } + + // check that the # of votes are the same + if len(ec.Votes) != len(lc.Votes) { + return fmt.Errorf("extended commit votes length %d does not match last commit votes length %d", len(ec.Votes), len(lc.Votes)) + } + + // check sort order of extended commit votes + if !slices.IsSortedFunc(ec.Votes, func(vote1, vote2 abci.ExtendedVoteInfo) int { + if vote1.Validator.Power == vote2.Validator.Power { + return bytes.Compare(vote1.Validator.Address, vote2.Validator.Address) // addresses sorted in ascending order (used to break vp conflicts) + } + return -int(vote1.Validator.Power - vote2.Validator.Power) // vp sorted in descending order + }) { + return fmt.Errorf("extended commit votes are not sorted by voting power") + } + + addressCache := make(map[string]struct{}, len(ec.Votes)) + // check consistency between LastCommit and ExtendedCommit + for i, vote := range ec.Votes { + // cache addresses to check for duplicates + if _, ok := addressCache[string(vote.Validator.Address)]; ok { + return fmt.Errorf("extended commit vote address %X is duplicated", vote.Validator.Address) + } + addressCache[string(vote.Validator.Address)] = struct{}{} + + if !bytes.Equal(vote.Validator.Address, lc.Votes[i].Validator.Address) { + return fmt.Errorf("extended commit vote address %X does not match last commit vote address %X", vote.Validator.Address, lc.Votes[i].Validator.Address) + } + if vote.Validator.Power != lc.Votes[i].Validator.Power { + return fmt.Errorf("extended commit vote power %d does not match last commit vote power %d", vote.Validator.Power, lc.Votes[i].Validator.Power) + } + } + + return nil +} + type ( // ProposalTxVerifier defines the interface that is implemented by BaseApp, // that any custom ABCI PrepareProposal and ProcessProposal handler can use diff --git a/baseapp/abci_utils_test.go b/baseapp/abci_utils_test.go index d0845c51040d..7157d3ac9cc3 100644 --- a/baseapp/abci_utils_test.go +++ b/baseapp/abci_utils_test.go @@ -2,6 +2,7 @@ package baseapp_test import ( "bytes" + "sort" "testing" abci "github.com/cometbft/cometbft/abci/types" @@ -19,6 +20,7 @@ import ( "cosmossdk.io/log" authtx "cosmossdk.io/x/auth/tx" + "cosmossdk.io/core/comet" "github.com/cosmos/cosmos-sdk/baseapp" baseapptestutil "github.com/cosmos/cosmos-sdk/baseapp/testutil" "github.com/cosmos/cosmos-sdk/baseapp/testutil/mock" @@ -64,6 +66,13 @@ func (t testValidator) toValidator(power int64) abci.Validator { } } +func (t testValidator) toSDKValidator(power int64) comet.Validator { + return comet.Validator{ + Address: t.consAddr.Bytes(), + Power: power, + } +} + type ABCIUtilsTestSuite struct { suite.Suite @@ -98,7 +107,9 @@ func NewABCIUtilsTestSuite(t *testing.T) *ABCIUtilsTestSuite { Abci: &cmtproto.ABCIParams{ VoteExtensionsEnableHeight: 2, }, - }) + }).WithBlockHeader(cmtproto.Header{ + ChainID: chainID, + }).WithLogger(log.NewTestLogger(t)) return s } @@ -128,6 +139,8 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsHappyPath() { extSig2, err := s.vals[2].privKey.Sign(bz) s.Require().NoError(err) + s.ctx = s.ctx.WithBlockHeight(3) // enable vote-extensions + llc := abci.ExtendedCommitInfo{ Round: 0, Votes: []abci.ExtendedVoteInfo{ @@ -151,8 +164,13 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsHappyPath() { }, }, } + + // order + convert to last commit + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + // expect-pass (votes of height 2 are included in next block) - s.Require().NoError(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, 3, chainID, llc)) + s.Require().NoError(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) } // check ValidateVoteExtensions works when a single node has submitted a BlockID_Absent @@ -174,6 +192,8 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsSingleVoteAbsent() { extSig2, err := s.vals[2].privKey.Sign(bz) s.Require().NoError(err) + s.ctx = s.ctx.WithBlockHeight(3) // vote-extensions are enabled + llc := abci.ExtendedCommitInfo{ Round: 0, Votes: []abci.ExtendedVoteInfo{ @@ -196,8 +216,12 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsSingleVoteAbsent() { }, }, } + + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + // expect-pass (votes of height 2 are included in next block) - s.Require().NoError(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, 3, chainID, llc)) + s.Require().NoError(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) } // check ValidateVoteExtensions works with duplicate votes @@ -223,15 +247,27 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsDuplicateVotes() { BlockIdFlag: cmtproto.BlockIDFlagCommit, } + ve2 := abci.ExtendedVoteInfo{ + Validator: s.vals[0].toValidator(334), // use diff voting-power to dupe + VoteExtension: ext, + ExtensionSignature: extSig0, + BlockIdFlag: cmtproto.BlockIDFlagCommit, + } + llc := abci.ExtendedCommitInfo{ Round: 0, Votes: []abci.ExtendedVoteInfo{ ve, - ve, + ve2, }, } + + s.ctx = s.ctx.WithBlockHeight(3) // vote-extensions are enabled + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + // expect fail (duplicate votes) - s.Require().Error(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, 3, chainID, llc)) + s.Require().Error(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) } // check ValidateVoteExtensions works when a single node has submitted a BlockID_Nil @@ -275,8 +311,15 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsSingleVoteNil() { }, }, } + + s.ctx = s.ctx.WithBlockHeight(3) // vote-extensions are enabled + + // create last commit + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + // expect-pass (votes of height 2 are included in next block) - s.Require().NoError(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, 3, chainID, llc)) + s.Require().NoError(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) } // check ValidateVoteExtensions works when two nodes have submitted a BlockID_Nil / BlockID_Absent @@ -317,8 +360,115 @@ func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsTwoVotesNilAbsent() { }, } + s.ctx = s.ctx.WithBlockHeight(3) // vote-extensions are enabled + + // create last commit + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + // expect-pass (votes of height 2 are included in next block) - s.Require().Error(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, 3, chainID, llc)) + s.Require().Error(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) +} + +func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsIncorrectVotingPower() { + ext := []byte("vote-extension") + cve := cmtproto.CanonicalVoteExtension{ + Extension: ext, + Height: 2, + Round: int64(0), + ChainId: chainID, + } + + bz, err := marshalDelimitedFn(&cve) + s.Require().NoError(err) + + extSig0, err := s.vals[0].privKey.Sign(bz) + s.Require().NoError(err) + + llc := abci.ExtendedCommitInfo{ + Round: 0, + Votes: []abci.ExtendedVoteInfo{ + // validator of power >2/3 is missing, so commit-info should not be valid + { + Validator: s.vals[0].toValidator(333), + BlockIdFlag: cmtproto.BlockIDFlagCommit, + VoteExtension: ext, + ExtensionSignature: extSig0, + }, + { + Validator: s.vals[1].toValidator(333), + BlockIdFlag: cmtproto.BlockIDFlagNil, + }, + { + Validator: s.vals[2].toValidator(334), + VoteExtension: ext, + BlockIdFlag: cmtproto.BlockIDFlagAbsent, + }, + }, + } + + s.ctx = s.ctx.WithBlockHeight(3) // vote-extensions are enabled + + // create last commit + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + + // modify voting powers to differ from the last-commit + llc.Votes[0].Validator.Power = 335 + llc.Votes[2].Validator.Power = 332 + + // expect-pass (votes of height 2 are included in next block) + s.Require().Error(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) +} + +func (s *ABCIUtilsTestSuite) TestValidateVoteExtensionsIncorrecOrder() { + ext := []byte("vote-extension") + cve := cmtproto.CanonicalVoteExtension{ + Extension: ext, + Height: 2, + Round: int64(0), + ChainId: chainID, + } + + bz, err := marshalDelimitedFn(&cve) + s.Require().NoError(err) + + extSig0, err := s.vals[0].privKey.Sign(bz) + s.Require().NoError(err) + + llc := abci.ExtendedCommitInfo{ + Round: 0, + Votes: []abci.ExtendedVoteInfo{ + // validator of power >2/3 is missing, so commit-info should not be valid + { + Validator: s.vals[0].toValidator(333), + BlockIdFlag: cmtproto.BlockIDFlagCommit, + VoteExtension: ext, + ExtensionSignature: extSig0, + }, + { + Validator: s.vals[1].toValidator(333), + BlockIdFlag: cmtproto.BlockIDFlagNil, + }, + { + Validator: s.vals[2].toValidator(334), + VoteExtension: ext, + BlockIdFlag: cmtproto.BlockIDFlagAbsent, + }, + }, + } + + s.ctx = s.ctx.WithBlockHeight(3) // vote-extensions are enabled + + // create last commit + llc, info := extendedCommitToLastCommit(llc) + s.ctx = s.ctx.WithCometInfo(info) + + // modify voting powers to differ from the last-commit + llc.Votes[0], llc.Votes[2] = llc.Votes[2], llc.Votes[0] + + // expect-pass (votes of height 2 are included in next block) + s.Require().Error(baseapp.ValidateVoteExtensions(s.ctx, s.valStore, llc)) } func (s *ABCIUtilsTestSuite) TestDefaultProposalHandler_NoOpMempoolTxSelection() { @@ -603,3 +753,61 @@ func setTxSignatureWithSecret(t *testing.T, builder client.TxBuilder, signatures ) require.NoError(t, err) } + +func extendedCommitToLastCommit(ec abci.ExtendedCommitInfo) (abci.ExtendedCommitInfo, comet.Info) { + // sort the extended commit info + sort.Sort(extendedVoteInfos(ec.Votes)) + + // convert the extended commit info to last commit info + lastCommit := comet.CommitInfo{ + Round: ec.Round, + Votes: make([]comet.VoteInfo, len(ec.Votes)), + } + + for i, vote := range ec.Votes { + lastCommit.Votes[i] = comet.VoteInfo{ + Validator: comet.Validator{ + Address: vote.Validator.Address, + Power: vote.Validator.Power, + }, + } + } + + return ec, comet.Info{ + LastCommit: lastCommit, + } +} + +type voteInfos []comet.VoteInfo + +func (v voteInfos) Len() int { + return len(v) +} + +func (v voteInfos) Less(i, j int) bool { + if v[i].Validator.Power == v[j].Validator.Power { + return bytes.Compare(v[i].Validator.Address, v[j].Validator.Address) == -1 + } + return v[i].Validator.Power > v[j].Validator.Power +} + +func (v voteInfos) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +type extendedVoteInfos []abci.ExtendedVoteInfo + +func (v extendedVoteInfos) Len() int { + return len(v) +} + +func (v extendedVoteInfos) Less(i, j int) bool { + if v[i].Validator.Power == v[j].Validator.Power { + return bytes.Compare(v[i].Validator.Address, v[j].Validator.Address) == -1 + } + return v[i].Validator.Power > v[j].Validator.Power +} + +func (v extendedVoteInfos) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} From 69f03cd17f2264c8be576f1b71abb02322146bf6 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 1 Mar 2024 18:24:16 +0100 Subject: [PATCH 3/4] feat(x/gov): add custom fuction to calculate vote results and vp (#19592) --- docs/architecture/adr-069-gov-improvements.md | 18 ++++---- simapp/app.go | 2 +- tests/integration/gov/keeper/keeper_test.go | 2 +- x/gov/CHANGELOG.md | 2 + x/gov/depinject.go | 2 +- x/gov/keeper/common_test.go | 2 +- x/gov/keeper/config.go | 42 +++++++++++++++++++ x/gov/keeper/deposit.go | 6 +-- x/gov/keeper/export_test.go | 2 +- x/gov/keeper/keeper.go | 7 ++-- x/gov/keeper/msg_server.go | 4 +- x/gov/keeper/proposal.go | 2 +- x/gov/keeper/tally.go | 9 +++- x/gov/types/config.go | 20 --------- 14 files changed, 73 insertions(+), 47 deletions(-) create mode 100644 x/gov/keeper/config.go delete mode 100644 x/gov/types/config.go diff --git a/docs/architecture/adr-069-gov-improvements.md b/docs/architecture/adr-069-gov-improvements.md index 6d5c80fad288..af5b12645205 100644 --- a/docs/architecture/adr-069-gov-improvements.md +++ b/docs/architecture/adr-069-gov-improvements.md @@ -164,19 +164,15 @@ Due to the vote option change, each proposal can have the same tallying method. However, chains may want to change the tallying function (weighted vote per voting power) of `x/gov` for a different algorithm (using a quadratic function on the voter stake, for instance). -The custom tallying function can be passed to the `x/gov` keeper with the following interface: +The custom tallying function can be passed to the `x/gov` keeper config: ```go -type Tally interface{ - // to be decided - - // Calculate calculates the tally result - Calculate(proposal v1.Proposal, govKeeper GovKeeper, stakingKeeper StakingKeeper) govv1.TallyResult - // IsAccepted returns true if the proposal passes/is accepted - IsAccepted() bool - // BurnDeposit returns true if the proposal deposit should be burned - BurnDeposit() bool -} +type CalculateVoteResultsAndVotingPowerFn func( + ctx context.Context, + keeper Keeper, + proposalID uint64, + validators map[string]v1.ValidatorGovInfo, +) (totalVoterPower math.LegacyDec, results map[v1.VoteOption]math.LegacyDec, err error) ``` ## Consequences diff --git a/simapp/app.go b/simapp/app.go index 510af074c4aa..2c1e47b95bf5 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -375,7 +375,7 @@ func NewSimApp( // by granting the governance module the right to execute the message. // See: https://docs.cosmos.network/main/modules/gov#proposal-messages govRouter := govv1beta1.NewRouter() - govConfig := govtypes.DefaultConfig() + govConfig := govkeeper.DefaultConfig() /* Example of setting gov params: govConfig.MaxMetadataLen = 10000 diff --git a/tests/integration/gov/keeper/keeper_test.go b/tests/integration/gov/keeper/keeper_test.go index 39756b834f34..68561f4a7336 100644 --- a/tests/integration/gov/keeper/keeper_test.go +++ b/tests/integration/gov/keeper/keeper_test.go @@ -112,7 +112,7 @@ func initFixture(tb testing.TB) *fixture { stakingKeeper, poolKeeper, router, - types.DefaultConfig(), + keeper.DefaultConfig(), authority.String(), ) assert.NilError(tb, govKeeper.ProposalID.Set(newCtx, 1)) diff --git a/x/gov/CHANGELOG.md b/x/gov/CHANGELOG.md index cc844666388d..0524fd23ff8b 100644 --- a/x/gov/CHANGELOG.md +++ b/x/gov/CHANGELOG.md @@ -27,6 +27,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* [#19592](https://github.com/cosmos/cosmos-sdk/pull/19592) Add custom tally function. * [#19304](https://github.com/cosmos/cosmos-sdk/pull/19304) Add `MsgSudoExec` for allowing executing any message as a sudo. * [#19101](https://github.com/cosmos/cosmos-sdk/pull/19101) Add message based params configuration. * [#18532](https://github.com/cosmos/cosmos-sdk/pull/18532) Add SPAM vote to proposals. @@ -59,6 +60,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### API Breaking Changes +* [#19592](https://github.com/cosmos/cosmos-sdk/pull/19592) `types.Config` and `types.DefaultConfig` have been moved to the keeper package in order to support the custom tallying function. * [#19349](https://github.com/cosmos/cosmos-sdk/pull/19349) Simplify state management in `x/gov`. Note `k.VotingPeriodProposals` and `k.SetProposal` are no longer needed and have been removed. * [#18532](https://github.com/cosmos/cosmos-sdk/pull/18532) All functions that were taking an expedited bool parameter now take a `ProposalType` parameter instead. * [#17496](https://github.com/cosmos/cosmos-sdk/pull/17496) in `x/gov/types/v1beta1/vote.go` `NewVote` was removed, constructing the struct is required for this type. diff --git a/x/gov/depinject.go b/x/gov/depinject.go index 1b99aeccc770..3776ddf46025 100644 --- a/x/gov/depinject.go +++ b/x/gov/depinject.go @@ -60,7 +60,7 @@ type ModuleOutputs struct { } func ProvideModule(in ModuleInputs) ModuleOutputs { - defaultConfig := govtypes.DefaultConfig() + defaultConfig := keeper.DefaultConfig() if in.Config.MaxTitleLen != 0 { defaultConfig.MaxTitleLen = in.Config.MaxTitleLen } diff --git a/x/gov/keeper/common_test.go b/x/gov/keeper/common_test.go index be8d44ea20c9..fb78f6fca0ca 100644 --- a/x/gov/keeper/common_test.go +++ b/x/gov/keeper/common_test.go @@ -130,7 +130,7 @@ func setupGovKeeper(t *testing.T, expectations ...func(sdk.Context, mocks)) ( // Gov keeper initializations - govKeeper := keeper.NewKeeper(encCfg.Codec, storeService, m.acctKeeper, m.bankKeeper, m.stakingKeeper, m.poolKeeper, baseApp.MsgServiceRouter(), types.DefaultConfig(), govAcct.String()) + govKeeper := keeper.NewKeeper(encCfg.Codec, storeService, m.acctKeeper, m.bankKeeper, m.stakingKeeper, m.poolKeeper, baseApp.MsgServiceRouter(), keeper.DefaultConfig(), govAcct.String()) require.NoError(t, govKeeper.ProposalID.Set(ctx, 1)) govRouter := v1beta1.NewRouter() // Also register legacy gov handlers to test them too. govRouter.AddRoute(types.RouterKey, v1beta1.ProposalHandler) diff --git a/x/gov/keeper/config.go b/x/gov/keeper/config.go new file mode 100644 index 000000000000..5e8f3a7d0c6a --- /dev/null +++ b/x/gov/keeper/config.go @@ -0,0 +1,42 @@ +package keeper + +import ( + "context" + + "cosmossdk.io/math" + v1 "cosmossdk.io/x/gov/types/v1" +) + +// CalculateVoteResultsAndVotingPowerFn is a function signature for calculating vote results and voting power +// It can be overridden to customize the voting power calculation for proposals +// It gets the proposal tallied and the validators governance infos (bonded tokens, voting power, etc.) +// It must return the total voting power and the results of the vote +type CalculateVoteResultsAndVotingPowerFn func( + ctx context.Context, + keeper Keeper, + proposalID uint64, + validators map[string]v1.ValidatorGovInfo, +) (totalVoterPower math.LegacyDec, results map[v1.VoteOption]math.LegacyDec, err error) + +// Config is a config struct used for initializing the gov module to avoid using globals. +type Config struct { + // MaxTitleLen defines the amount of characters that can be used for proposal title + MaxTitleLen uint64 + // MaxMetadataLen defines the amount of characters that can be used for proposal metadata + MaxMetadataLen uint64 + // MaxSummaryLen defines the amount of characters that can be used for proposal summary + MaxSummaryLen uint64 + // CalculateVoteResultsAndVotingPowerFn is a function signature for calculating vote results and voting power + // Keeping it nil will use the default implementation + CalculateVoteResultsAndVotingPowerFn CalculateVoteResultsAndVotingPowerFn +} + +// DefaultConfig returns the default config for gov. +func DefaultConfig() Config { + return Config{ + MaxTitleLen: 255, + MaxMetadataLen: 255, + MaxSummaryLen: 10200, + CalculateVoteResultsAndVotingPowerFn: nil, + } +} diff --git a/x/gov/keeper/deposit.go b/x/gov/keeper/deposit.go index 34512ae253bf..ece9eac79059 100644 --- a/x/gov/keeper/deposit.go +++ b/x/gov/keeper/deposit.go @@ -98,7 +98,7 @@ func (k Keeper) AddDeposit(ctx context.Context, proposalID uint64, depositorAddr } // the deposit must only contain valid denoms (listed in the min deposit param) - if err := k.validateDepositDenom(ctx, params, depositAmount); err != nil { + if err := k.validateDepositDenom(params, depositAmount); err != nil { return false, err } @@ -280,7 +280,7 @@ func (k Keeper) ChargeDeposit(ctx context.Context, proposalID uint64, destAddres // validateInitialDeposit validates if initial deposit is greater than or equal to the minimum // required at the time of proposal submission. This threshold amount is determined by // the deposit parameters. Returns nil on success, error otherwise. -func (k Keeper) validateInitialDeposit(ctx context.Context, params v1.Params, initialDeposit sdk.Coins, proposalType v1.ProposalType) error { +func (k Keeper) validateInitialDeposit(params v1.Params, initialDeposit sdk.Coins, proposalType v1.ProposalType) error { if !initialDeposit.IsValid() || initialDeposit.IsAnyNegative() { return errors.Wrap(sdkerrors.ErrInvalidCoins, initialDeposit.String()) } @@ -311,7 +311,7 @@ func (k Keeper) validateInitialDeposit(ctx context.Context, params v1.Params, in } // validateDepositDenom validates if the deposit denom is accepted by the governance module. -func (k Keeper) validateDepositDenom(ctx context.Context, params v1.Params, depositAmount sdk.Coins) error { +func (k Keeper) validateDepositDenom(params v1.Params, depositAmount sdk.Coins) error { denoms := []string{} acceptedDenoms := make(map[string]bool, len(params.MinDeposit)) for _, coin := range params.MinDeposit { diff --git a/x/gov/keeper/export_test.go b/x/gov/keeper/export_test.go index 1421e96781d1..8294a34d95b7 100644 --- a/x/gov/keeper/export_test.go +++ b/x/gov/keeper/export_test.go @@ -14,5 +14,5 @@ func (k Keeper) ValidateInitialDeposit(ctx sdk.Context, initialDeposit sdk.Coins return err } - return k.validateInitialDeposit(ctx, params, initialDeposit, proposalType) + return k.validateInitialDeposit(params, initialDeposit, proposalType) } diff --git a/x/gov/keeper/keeper.go b/x/gov/keeper/keeper.go index 6a82442a401c..6ffa959c71c1 100644 --- a/x/gov/keeper/keeper.go +++ b/x/gov/keeper/keeper.go @@ -42,7 +42,8 @@ type Keeper struct { // Msg server router router baseapp.MessageRouter - config types.Config + // Config represent extra module configuration + config Config // the address capable of executing a MsgUpdateParams message. Typically, this // should be the x/gov module account. @@ -88,7 +89,7 @@ func (k Keeper) GetAuthority() string { func NewKeeper( cdc codec.Codec, storeService corestoretypes.KVStoreService, authKeeper types.AccountKeeper, bankKeeper types.BankKeeper, sk types.StakingKeeper, pk types.PoolKeeper, - router baseapp.MessageRouter, config types.Config, authority string, + router baseapp.MessageRouter, config Config, authority string, ) *Keeper { // ensure governance module account is set if addr := authKeeper.GetModuleAddress(types.ModuleName); addr == nil { @@ -99,7 +100,7 @@ func NewKeeper( panic(fmt.Sprintf("invalid authority address: %s", authority)) } - defaultConfig := types.DefaultConfig() + defaultConfig := DefaultConfig() // If MaxMetadataLen not set by app developer, set to default value. if config.MaxTitleLen == 0 { config.MaxTitleLen = defaultConfig.MaxTitleLen diff --git a/x/gov/keeper/msg_server.go b/x/gov/keeper/msg_server.go index 9fd9b8b6c86f..e6cc63a2b8f6 100644 --- a/x/gov/keeper/msg_server.go +++ b/x/gov/keeper/msg_server.go @@ -76,11 +76,11 @@ func (k msgServer) SubmitProposal(goCtx context.Context, msg *v1.MsgSubmitPropos if msg.Expedited { // checking for backward compatibility msg.ProposalType = v1.ProposalType_PROPOSAL_TYPE_EXPEDITED } - if err := k.validateInitialDeposit(ctx, params, msg.GetInitialDeposit(), msg.ProposalType); err != nil { + if err := k.validateInitialDeposit(params, msg.GetInitialDeposit(), msg.ProposalType); err != nil { return nil, err } - if err := k.validateDepositDenom(ctx, params, msg.GetInitialDeposit()); err != nil { + if err := k.validateDepositDenom(params, msg.GetInitialDeposit()); err != nil { return nil, err } diff --git a/x/gov/keeper/proposal.go b/x/gov/keeper/proposal.go index 73d4566fa5a0..8e7801f8f520 100644 --- a/x/gov/keeper/proposal.go +++ b/x/gov/keeper/proposal.go @@ -264,7 +264,7 @@ func (k Keeper) ActivateVotingPeriod(ctx context.Context, proposal v1.Proposal) customMessageParams, err := k.MessageBasedParams.Get(ctx, sdk.MsgTypeURL(proposal.Messages[0])) if err == nil { votingPeriod = customMessageParams.VotingPeriod - } else if err != nil && !errors.Is(err, collections.ErrNotFound) { + } else if !errors.Is(err, collections.ErrNotFound) { return err } } diff --git a/x/gov/keeper/tally.go b/x/gov/keeper/tally.go index bf45e796e9c1..8f87ec3570d2 100644 --- a/x/gov/keeper/tally.go +++ b/x/gov/keeper/tally.go @@ -18,7 +18,11 @@ func (k Keeper) Tally(ctx context.Context, proposal v1.Proposal) (passes, burnDe return false, false, v1.TallyResult{}, err } - totalVoterPower, results, err := k.calculateVoteResultsAndVotingPower(ctx, proposal.Id, validators) + if k.config.CalculateVoteResultsAndVotingPowerFn == nil { + k.config.CalculateVoteResultsAndVotingPowerFn = defaultCalculateVoteResultsAndVotingPower + } + + totalVoterPower, results, err := k.config.CalculateVoteResultsAndVotingPowerFn(ctx, k, proposal.Id, validators) if err != nil { return false, false, v1.TallyResult{}, err } @@ -232,8 +236,9 @@ func (k Keeper) getCurrentValidators(ctx context.Context) (map[string]v1.Validat // calculateVoteResultsAndVotingPower iterate over all votes, tally up the voting power of each validator // and returns the votes results from voters -func (k Keeper) calculateVoteResultsAndVotingPower( +func defaultCalculateVoteResultsAndVotingPower( ctx context.Context, + k Keeper, proposalID uint64, validators map[string]v1.ValidatorGovInfo, ) (math.LegacyDec, map[v1.VoteOption]math.LegacyDec, error) { diff --git a/x/gov/types/config.go b/x/gov/types/config.go deleted file mode 100644 index 919d54e5b9ad..000000000000 --- a/x/gov/types/config.go +++ /dev/null @@ -1,20 +0,0 @@ -package types - -// Config is a config struct used for initializing the gov module to avoid using globals. -type Config struct { - // MaxTitleLen defines the amount of characters that can be used for proposal title - MaxTitleLen uint64 - // MaxMetadataLen defines the amount of characters that can be used for proposal metadata. - MaxMetadataLen uint64 - // MaxSummaryLen defines the amount of characters that can be used for proposal summary - MaxSummaryLen uint64 -} - -// DefaultConfig returns the default config for gov. -func DefaultConfig() Config { - return Config{ - MaxTitleLen: 255, - MaxMetadataLen: 255, - MaxSummaryLen: 10200, - } -} From 7628592c6a1fbefdff0afa71e89f08b147a02977 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 1 Mar 2024 19:05:20 +0100 Subject: [PATCH 4/4] chore(core): bring changes from serverv2 (#19617) --- core/CHANGELOG.md | 11 +++ core/appmodule/environment.go | 17 +--- core/appmodule/module.go | 41 +++------ core/appmodule/v2/appmodule.go | 92 ++++++++++++++++++++ core/appmodule/v2/environment.go | 22 +++++ core/appmodule/v2/genesis.go | 17 ++++ core/appmodule/v2/handlers.go | 142 +++++++++++++++++++++++++++++++ core/appmodule/v2/message.go | 21 +++++ core/appmodule/v2/migrations.go | 41 +++++++++ core/event/event.go | 29 +++++++ core/event/service.go | 9 -- core/gas/meter.go | 17 +++- core/go.mod | 3 + core/go.sum | 5 ++ core/store/database.go | 23 +++++ core/transaction/transaction.go | 32 +++++++ 16 files changed, 470 insertions(+), 52 deletions(-) create mode 100644 core/appmodule/v2/appmodule.go create mode 100644 core/appmodule/v2/environment.go create mode 100644 core/appmodule/v2/genesis.go create mode 100644 core/appmodule/v2/handlers.go create mode 100644 core/appmodule/v2/message.go create mode 100644 core/appmodule/v2/migrations.go create mode 100644 core/event/event.go create mode 100644 core/store/database.go create mode 100644 core/transaction/transaction.go diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 27ee7b8be825..8dec182fa0c3 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -42,6 +42,17 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#18457](https://github.com/cosmos/cosmos-sdk/pull/18457) Add branch.ExecuteWithGasLimit. * [#19041](https://github.com/cosmos/cosmos-sdk/pull/19041) Add `appmodule.Environment` interface to fetch different services * [#19370](https://github.com/cosmos/cosmos-sdk/pull/19370) Add `appmodule.Migrations` interface to handle migrations +* [#19617](https://github.com/cosmos/cosmos-sdk/pull/19617) Add DataBaseService to store non-consensus data in a database + * Create V2 appmodule with v2 api for runtime/v2 + * Introduce `Transaction.Tx` for use in runtime/v2 + * Introduce `HasUpdateValidators` interface and `ValidatorUpdate` struct for validator updates + * Introduce `HasTxValidation` interface for modules to register tx validation handlers + * `HasGenesis` interface for modules to register import, export, validation and default genesis handlers. The new api works with `proto.Message` + * Add `PreMsghandler`and `PostMsgHandler` for pre and post message hooks + * Add `MsgHandler` as an alternative to grpc handlers + * Provide separate `MigrationRegistrar` instead of grouping with `RegisterServices` + +### Improvements ### API Breaking Changes diff --git a/core/appmodule/environment.go b/core/appmodule/environment.go index 17ab778191c0..93778e9e9bbe 100644 --- a/core/appmodule/environment.go +++ b/core/appmodule/environment.go @@ -1,21 +1,8 @@ package appmodule import ( - "cosmossdk.io/core/branch" - "cosmossdk.io/core/event" - "cosmossdk.io/core/gas" - "cosmossdk.io/core/header" - "cosmossdk.io/core/store" - "cosmossdk.io/log" + appmodule "cosmossdk.io/core/appmodule/v2" ) // Environment is used to get all services to their respective module -type Environment struct { - BranchService branch.Service - EventService event.Service - GasService gas.Service - HeaderService header.Service - KVStoreService store.KVStoreService - MemStoreService store.MemoryStoreService - Logger log.Logger -} +type Environment = appmodule.Environment diff --git a/core/appmodule/module.go b/core/appmodule/module.go index ac30c246fde8..7f352a28e7b6 100644 --- a/core/appmodule/module.go +++ b/core/appmodule/module.go @@ -5,20 +5,27 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/runtime/protoiface" + + appmodule "cosmossdk.io/core/appmodule/v2" ) // AppModule is a tag interface for app module implementations to use as a basis // for extension interfaces. It provides no functionality itself, but is the // type that all valid app modules should provide so that they can be identified // by other modules (usually via depinject) as app modules. -type AppModule interface { - // IsAppModule is a dummy method to tag a struct as implementing an AppModule. - IsAppModule() +type AppModule = appmodule.AppModule + +// HasMigrations is the extension interface that modules should implement to register migrations. +type HasMigrations interface { + AppModule - // IsOnePerModuleType is a dummy method to help depinject resolve modules. - IsOnePerModuleType() + // RegisterMigrations registers the module's migrations with the app's migrator. + RegisterMigrations(MigrationRegistrar) error } +// HasConsensusVersion is the interface for declaring a module consensus version. +type HasConsensusVersion = appmodule.HasConsensusVersion + // HasServices is the extension interface that modules should implement to register // implementations of services defined in .proto files. type HasServices interface { @@ -39,14 +46,6 @@ type HasServices interface { RegisterServices(grpc.ServiceRegistrar) error } -// HasMigrations is the extension interface that modules should implement to register migrations. -type HasMigrations interface { - AppModule - - // RegisterMigrations registers the module's migrations with the app's migrator. - RegisterMigrations(MigrationRegistrar) error -} - // ResponsePreBlock represents the response from the PreBlock method. // It can modify consensus parameters in storage and signal the caller through the return value. // When it returns ConsensusParamsChanged=true, the caller must refresh the consensus parameter in the finalize context. @@ -65,23 +64,11 @@ type HasPreBlocker interface { // HasBeginBlocker is the extension interface that modules should implement to run // custom logic before transaction processing in a block. -type HasBeginBlocker interface { - AppModule - - // BeginBlock is a method that will be run before transactions are processed in - // a block. - BeginBlock(context.Context) error -} +type HasBeginBlocker = appmodule.HasBeginBlocker // HasEndBlocker is the extension interface that modules should implement to run // custom logic after transaction processing in a block. -type HasEndBlocker interface { - AppModule - - // EndBlock is a method that will be run after transactions are processed in - // a block. - EndBlock(context.Context) error -} +type HasEndBlocker = appmodule.HasEndBlocker // MsgHandlerRouter is implemented by the runtime provider. type MsgHandlerRouter interface { diff --git a/core/appmodule/v2/appmodule.go b/core/appmodule/v2/appmodule.go new file mode 100644 index 000000000000..eb44c9f513b8 --- /dev/null +++ b/core/appmodule/v2/appmodule.go @@ -0,0 +1,92 @@ +package appmodule + +import ( + "context" + + "cosmossdk.io/core/transaction" +) + +// AppModule is a tag interface for app module implementations to use as a basis +// for extension interfaces. It provides no functionality itself, but is the +// type that all valid app modules should provide so that they can be identified +// by other modules (usually via depinject) as app modules. +type AppModule interface { + // IsAppModule is a dummy method to tag a struct as implementing an AppModule. + IsAppModule() + + // IsOnePerModuleType is a dummy method to help depinject resolve modules. + IsOnePerModuleType() +} + +// HasBeginBlocker is the extension interface that modules should implement to run +// custom logic before transaction processing in a block. +type HasBeginBlocker interface { + AppModule + + // BeginBlock is a method that will be run before transactions are processed in + // a block. + BeginBlock(context.Context) error +} + +// HasEndBlocker is the extension interface that modules should implement to run +// custom logic after transaction processing in a block. +type HasEndBlocker interface { + AppModule + + // EndBlock is a method that will be run after transactions are processed in + // a block. + EndBlock(context.Context) error +} + +// HasTxValidation is the extension interface that modules should implement to run +// custom logic for validating transactions. +// It was previously known as AnteHandler/Decorator. +type HasTxValidation[T transaction.Tx] interface { + AppModule + + // TxValidator is a method that will be run on each transaction. + // If an error is returned: + // ,---. + // / | + // / | + // You shall not pass! / | + // / | + // \ ___,' | + // < -' : + // `-.__..--'``-,_\_ + // |o/ ` :,.)_`> + // :/ ` ||/) + // (_.).__,-` |\ + // /( `.`` `| : + // \'`-.) ` ; ; + // | ` /-< + // | ` / `. + // ,-_-..____ /| ` :__..-'\ + // /,'-.__\\ ``-./ :` ; \ + // `\ `\ `\\ \ : ( ` / , `. \ + // \` \ \\ | | ` : : .\ \ + // \ `\_ )) : ; | | ): : + // (`-.-'\ || |\ \ ` ; ; | | + // \-_ `;;._ ( ` / /_ | | + // `-.-.// ,'`-._\__/_,' ; | + // \:: : / ` , / | + // || | ( ,' / / | + // || ,' / | + TxValidator(ctx context.Context, tx T) error +} + +// HasUpdateValidators is an extension interface that contains information about the AppModule and UpdateValidators. +// It can be seen as the alternative of the Cosmos SDK' HasABCIEndBlocker. +// Both are still supported. +type HasUpdateValidators interface { + AppModule + + UpdateValidators(ctx context.Context) ([]ValidatorUpdate, error) +} + +// ValidatorUpdate defines a validator update. +type ValidatorUpdate struct { + PubKey []byte + PubKeyType string + Power int64 // updated power of the validtor +} diff --git a/core/appmodule/v2/environment.go b/core/appmodule/v2/environment.go new file mode 100644 index 000000000000..9badbddc2f73 --- /dev/null +++ b/core/appmodule/v2/environment.go @@ -0,0 +1,22 @@ +package appmodule + +import ( + "cosmossdk.io/core/branch" + "cosmossdk.io/core/event" + "cosmossdk.io/core/gas" + "cosmossdk.io/core/header" + "cosmossdk.io/core/store" + "cosmossdk.io/log" +) + +// Environment is used to get all services to their respective module +type Environment struct { + BranchService branch.Service + EventService event.Service + GasService gas.Service + HeaderService header.Service + KVStoreService store.KVStoreService + MemStoreService store.MemoryStoreService + DataBaseService store.DatabaseService + Logger log.Logger +} diff --git a/core/appmodule/v2/genesis.go b/core/appmodule/v2/genesis.go new file mode 100644 index 000000000000..d457ac89d8c3 --- /dev/null +++ b/core/appmodule/v2/genesis.go @@ -0,0 +1,17 @@ +package appmodule + +import ( + "context" + "encoding/json" +) + +// HasGenesis defines a custom genesis handling API implementation. +// WARNING: this API is meant as a short-term solution to allow for the +// migration of existing modules to the new app module API. It is intended to be replaced by collections +type HasGenesis interface { + AppModule + DefaultGenesis() Message + ValidateGenesis(data json.RawMessage) error + InitGenesis(ctx context.Context, data json.RawMessage) error + ExportGenesis(ctx context.Context) (json.RawMessage, error) +} diff --git a/core/appmodule/v2/handlers.go b/core/appmodule/v2/handlers.go new file mode 100644 index 000000000000..7bfc46bf24cf --- /dev/null +++ b/core/appmodule/v2/handlers.go @@ -0,0 +1,142 @@ +package appmodule + +import ( + "context" + "fmt" +) + +type ( + // PreMsgHandler is a handler that is executed before Handler. If it errors the execution reverts. + PreMsgHandler = func(ctx context.Context, msg Message) error + // Handler handles the state transition of the provided message. + Handler = func(ctx context.Context, msg Message) (msgResp Message, err error) + // PostMsgHandler runs after Handler, only if Handler does not error. If PostMsgHandler errors + // then the execution is reverted. + PostMsgHandler = func(ctx context.Context, msg, msgResp Message) error +) + +// RegisterHandler is a helper function that modules can use to not lose type safety when registering handlers to the +// QueryRouter or MsgRouter. Example usage: +// ```go +// +// func (k Keeper) QueryBalance(ctx context.Context, req *types.QueryBalanceRequest) (*types.QueryBalanceResponse, error) { +// ... query logic ... +// } +// +// func (m Module) RegisterQueryHandlers(router appmodule.QueryRouter) { +// appmodule.RegisterHandler(router, keeper.QueryBalance) +// } +// +// ``` +func RegisterHandler[R interface{ Register(string, Handler) }, Req, Resp Message]( + router R, + handler func(ctx context.Context, msg Req) (msgResp Resp, err error), +) { + untypedHandler := func(ctx context.Context, m Message) (Message, error) { + typed, ok := m.(Req) + if !ok { + return nil, fmt.Errorf("unexpected type %T, wanted: %T", m, *new(Req)) + } + return handler(ctx, typed) + } + router.Register(messageName[Req](), untypedHandler) +} + +// RegisterPreHandler is a helper function that modules can use to not lose type safety when registering PreMsgHandler to the +// PreMsgRouter. Example usage: +// ```go +// +// func (k Keeper) BeforeSend(ctx context.Context, req *types.MsgSend) (*types.QueryBalanceResponse, error) { +// ... before send logic ... +// } +// +// func (m Module) RegisterPreMsgHandlers(router appmodule.PreMsgRouter) { +// appmodule.RegisterPreHandler(router, keeper.BeforeSend) +// } +// +// ``` +func RegisterPreHandler[Req Message]( + router PreMsgRouter, + handler func(ctx context.Context, msg Req) error, +) { + untypedHandler := func(ctx context.Context, m Message) error { + typed, ok := m.(Req) + if !ok { + return fmt.Errorf("unexpected type %T, wanted: %T", m, *new(Req)) + } + return handler(ctx, typed) + } + router.Register(messageName[Req](), untypedHandler) +} + +// RegisterPostHandler is a helper function that modules can use to not lose type safety when registering handlers to the +// PostMsgRouter. Example usage: +// ```go +// +// func (k Keeper) AfterSend(ctx context.Context, req *types.MsgSend, resp *types.MsgSendResponse) error { +// ... query logic ... +// } +// +// func (m Module) RegisterPostMsgHandlers(router appmodule.PostMsgRouter) { +// appmodule.RegisterPostHandler(router, keeper.AfterSend) +// } +// +// ``` +func RegisterPostHandler[Req, Resp Message]( + router PostMsgRouter, + handler func(ctx context.Context, msg Req, msgResp Resp) error, +) { + untypedHandler := func(ctx context.Context, m, mResp Message) error { + typed, ok := m.(Req) + if !ok { + return fmt.Errorf("unexpected type %T, wanted: %T", m, *new(Req)) + } + typedResp, ok := mResp.(Resp) + if !ok { + return fmt.Errorf("unexpected type %T, wanted: %T", m, *new(Resp)) + } + return handler(ctx, typed, typedResp) + } + router.Register(messageName[Req](), untypedHandler) +} + +// msg handler + +type PreMsgRouter interface { + // Register will register a specific message handler hooking into the message with + // the provided name. + Register(msgName string, handler PreMsgHandler) + // RegisterGlobal will register a global message handler hooking into any message + // being executed. + RegisterGlobal(handler PreMsgHandler) +} + +type HasPreMsgHandlers interface { + RegisterPreMsgHandlers(router PreMsgRouter) +} + +type MsgRouter interface { + Register(msgName string, handler Handler) +} + +type HasMsgHandlers interface { + RegisterMsgHandlers(router MsgRouter) +} + +type PostMsgRouter interface { + // Register will register a specific message handler hooking after the execution of message with + // the provided name. + Register(msgName string, handler PostMsgHandler) + // RegisterGlobal will register a global message handler hooking after the execution of any message. + RegisterGlobal(handler PreMsgHandler) +} + +// query handler + +type QueryRouter interface { + Register(queryName string, handler Handler) +} + +type HasQueryHandlers interface { + RegisterQueryHandlers(router QueryRouter) +} diff --git a/core/appmodule/v2/message.go b/core/appmodule/v2/message.go new file mode 100644 index 000000000000..8a8753c9195e --- /dev/null +++ b/core/appmodule/v2/message.go @@ -0,0 +1,21 @@ +package appmodule + +import ( + gogoproto "github.com/cosmos/gogoproto/proto" + protov2 "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/runtime/protoiface" +) + +// Message aliases protoiface.MessageV1 for convenience. +type Message = protoiface.MessageV1 + +func messageName[M Message]() string { + switch m := any(*new(M)).(type) { + case protov2.Message: + return string(m.ProtoReflect().Descriptor().FullName()) + case gogoproto.Message: + return gogoproto.MessageName(m) + default: + panic("unknown message type") + } +} diff --git a/core/appmodule/v2/migrations.go b/core/appmodule/v2/migrations.go new file mode 100644 index 000000000000..794fb4a28cc8 --- /dev/null +++ b/core/appmodule/v2/migrations.go @@ -0,0 +1,41 @@ +package appmodule + +import "context" + +// HasConsensusVersion is the interface for declaring a module consensus version. +type HasConsensusVersion interface { + // ConsensusVersion is a sequence number for state-breaking change of the + // module. It should be incremented on each consensus-breaking change + // introduced by the module. To avoid wrong/empty versions, the initial version + // should be set to 1. + ConsensusVersion() uint64 +} + +// HasMigrations is implemented by a module which upgrades or has upgraded +// to a new consensus version. +type HasMigrations interface { + AppModule + HasConsensusVersion + + // RegisterMigrations registers the module's migrations with the app's migrator. + RegisterMigrations(MigrationRegistrar) error +} + +type MigrationRegistrar interface { + // Register registers an in-place store migration for a module. The + // handler is a migration script to perform in-place migrations from version + // `fromVersion` to version `fromVersion+1`. + // + // EACH TIME a module's ConsensusVersion increments, a new migration MUST + // be registered using this function. If a migration handler is missing for + // a particular function, the upgrade logic (see RunMigrations function) + // will panic. If the ConsensusVersion bump does not introduce any store + // changes, then a no-op function must be registered here. + Register(moduleName string, fromVersion uint64, handler MigrationHandler) error +} + +// MigrationHandler is the migration function that each module registers. +type MigrationHandler func(context.Context) error + +// VersionMap is a map of moduleName -> version +type VersionMap map[string]uint64 diff --git a/core/event/event.go b/core/event/event.go new file mode 100644 index 000000000000..4304e6e2b4af --- /dev/null +++ b/core/event/event.go @@ -0,0 +1,29 @@ +package event + +// Attribute is a kv-pair event attribute. +type Attribute struct { + Key, Value string +} + +func NewAttribute(key, value string) Attribute { + return Attribute{Key: key, Value: value} +} + +// Events represents a list of events. +type Events struct { + Events []Event +} + +func NewEvents(events ...Event) Events { + return Events{Events: events} +} + +// Event defines how an event will emitted +type Event struct { + Type string + Attributes []Attribute +} + +func NewEvent(ty string, attrs ...Attribute) Event { + return Event{Type: ty, Attributes: attrs} +} diff --git a/core/event/service.go b/core/event/service.go index 941f142db5f9..6668c1d5bfa8 100644 --- a/core/event/service.go +++ b/core/event/service.go @@ -36,12 +36,3 @@ type Manager interface { // not a state-machine breaking change. EmitNonConsensus(event protoiface.MessageV1) error } - -// KVEventAttribute is a kv-pair event attribute. -type Attribute struct { - Key, Value string -} - -func NewAttribute(key, value string) Attribute { - return Attribute{Key: key, Value: value} -} diff --git a/core/gas/meter.go b/core/gas/meter.go index 0774fd2a7733..9495201aa4c0 100644 --- a/core/gas/meter.go +++ b/core/gas/meter.go @@ -1,10 +1,25 @@ // Package gas provides a basic API for app modules to track gas usage. package gas -import "context" +import ( + "context" + "errors" + "math" +) +// ErrOutOfGas must be used by GasMeter implementers to signal +// that the state transition consumed all the allowed computational +// gas. +var ErrOutOfGas = errors.New("out of gas") + +// Gas defines type alias of uint64 for gas consumption. Gas is used +// to measure computational overhead when executing state transitions, +// it might be related to storage access and not only. type Gas = uint64 +// NoGasLimit signals that no gas limit must be applied. +const NoGasLimit Gas = math.MaxUint64 + // Service represents a gas service which can retrieve and set a gas meter in a context. // gas.Service is a core API type that should be provided by the runtime module being used to // build an app via depinject. diff --git a/core/go.mod b/core/go.mod index e7bf2f319d5e..e732129de93a 100644 --- a/core/go.mod +++ b/core/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( cosmossdk.io/log v1.3.1 + github.com/cosmos/gogoproto v1.4.11 github.com/stretchr/testify v1.8.4 google.golang.org/grpc v1.62.0 google.golang.org/protobuf v1.32.0 @@ -12,6 +13,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -19,6 +21,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/zerolog v1.32.0 // indirect + golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/core/go.sum b/core/go.sum index 105975b5b061..57133f587985 100644 --- a/core/go.sum +++ b/core/go.sum @@ -1,6 +1,8 @@ cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/gogoproto v1.4.11 h1:LZcMHrx4FjUgrqQSWeaGC1v/TeuVFqSLa43CC6aWR2g= +github.com/cosmos/gogoproto v1.4.11/go.mod h1:/g39Mh8m17X8Q/GDEs5zYTSNaNnInBSohtaxzQnYq1Y= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -10,6 +12,7 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -36,6 +39,8 @@ github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= +golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/core/store/database.go b/core/store/database.go new file mode 100644 index 000000000000..551727a6342b --- /dev/null +++ b/core/store/database.go @@ -0,0 +1,23 @@ +package store + +// Database provides access to the underlying database for CRUD operations of non-consensus data. +// WARNING: using this api will make your module unprovable for fraud and validity proofs +type DatabaseService interface { + GetDatabase() NonConsensusStore +} + +// NonConsensusStore is a simple key-value store that is used to store non-consensus data. +// Note the non-consensus data is not committed to the blockchain and does not allow iteration +type NonConsensusStore interface { + // Get returns nil iff key doesn't exist. Errors on nil key. + Get(key []byte) ([]byte, error) + + // Has checks if a key exists. Errors on nil key. + Has(key []byte) (bool, error) + + // Set sets the key. Errors on nil key or value. + Set(key, value []byte) error + + // Delete deletes the key. Errors on nil key. + Delete(key []byte) error +} diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go new file mode 100644 index 000000000000..be1e2960ad94 --- /dev/null +++ b/core/transaction/transaction.go @@ -0,0 +1,32 @@ +package transaction + +import ( + "google.golang.org/protobuf/proto" +) + +type ( + Type = proto.Message + Identity = []byte +) + +// Codec defines the TX codec, which converts a TX from bytes to its concrete representation. +type Codec[T Tx] interface { + // Decode decodes the tx bytes into a DecodedTx, containing + // both concrete and bytes representation of the tx. + Decode([]byte) (T, error) +} + +type Tx interface { + // Hash returns the unique identifier for the Tx. + Hash() [32]byte // TODO evaluate if 32 bytes is the right size & benchmark overhead of hashing instead of using identifier + // GetMessages returns the list of state transitions of the Tx. + GetMessages() []Type + // GetSenders returns the tx state transition sender. + GetSenders() []Identity // TODO reduce this to a single identity if accepted + // GetGasLimit returns the gas limit of the tx. Must return math.MaxUint64 for infinite gas + // txs. + GetGasLimit() uint64 + // Bytes returns the encoded version of this tx. Note: this is ideally cached + // from the first instance of the decoding of the tx. + Bytes() []byte +}