diff --git a/store/gas.go b/store/gas.go new file mode 100644 index 0000000000..f7743486f0 --- /dev/null +++ b/store/gas.go @@ -0,0 +1,261 @@ +package store + +import ( + "fmt" + "math" +) + +// Gas defines type alias of uint64 for gas consumption. Gas is measured by the +// SDK for store operations such as Get and Set calls. In addition, callers have +// the ability to explicitly charge gas for costly operations such as signature +// verification. +type Gas uint64 + +// Gas consumption descriptors. +const ( + GasDescIterNextCostFlat = "IterNextFlat" + GasDescValuePerByte = "ValuePerByte" + GasDescWritePerByte = "WritePerByte" + GasDescReadPerByte = "ReadPerByte" + GasDescWriteCostFlat = "WriteFlat" + GasDescReadCostFlat = "ReadFlat" + GasDescHas = "Has" + GasDescDelete = "Delete" +) + +type ( + // ErrorNegativeGasConsumed defines an error thrown when the amount of gas refunded + // results in a negative gas consumed amount. + ErrorNegativeGasConsumed struct { + Descriptor string + } + + // ErrorOutOfGas defines an error thrown when an action results in out of gas. + ErrorOutOfGas struct { + Descriptor string + } + + // ErrorGasOverflow defines an error thrown when an action results gas consumption + // unsigned integer overflow. + ErrorGasOverflow struct { + Descriptor string + } +) + +func (e ErrorNegativeGasConsumed) Error() string { + return fmt.Sprintf("negative gas consumed: %s", e.Descriptor) +} + +func (e ErrorOutOfGas) Error() string { + return fmt.Sprintf("out of gas: %s", e.Descriptor) +} + +func (e ErrorGasOverflow) Error() string { + return fmt.Sprintf("gas overflow: %s", e.Descriptor) +} + +// GasMeter defines an interface for gas consumption tracking. +type GasMeter interface { + // GasConsumed returns the amount of gas consumed so far. + GasConsumed() Gas + // GasConsumedToLimit returns the gas limit if gas consumed is past the limit, + // otherwise it returns the consumed gas so far. + GasConsumedToLimit() Gas + // GasRemaining returns the gas left in the GasMeter. + GasRemaining() Gas + // Limit returns the gas limit (if any). + Limit() Gas + // ConsumeGas adds the given amount of gas to the gas consumed and should panic + // if it overflows the gas limit (if any). + ConsumeGas(amount Gas, descriptor string) + // RefundGas will deduct the given amount from the gas consumed so far. If the + // amount is greater than the gas consumed, the function should panic. + RefundGas(amount Gas, descriptor string) + // IsPastLimit returns if the gas consumed so far is past the limit (if any), + // otherwise it returns . + IsPastLimit() bool + // IsOutOfGas returns if the gas consumed so far is greater than or equal + // to gas limit (if any), otherwise it returns . + IsOutOfGas() bool + + fmt.Stringer +} + +// GasConfig defines gas cost for each operation on a KVStore. +type GasConfig struct { + // HasCost should reflect a fixed cost for a Has() call on a store. + HasCost Gas + // DeleteCost should reflect a fixed cost for a Delete() call on a store. + DeleteCost Gas + // ReadCostFlat should reflect a fixed cost for a Get() call on a store. + ReadCostFlat Gas + // ReadCostPerByte should reflect a fixed cost, per-byte on the key and value, + // for a Get() call on a store. Note, this cost can also be used on iteration + // seeks. + ReadCostPerByte Gas + // WriteCostFlat should reflect a fixed cost for a Set() call on a store. + WriteCostFlat Gas + // WriteCostPerByte should reflect a fixed cost, per-byte on the key and value, + // for a Set() call on a store. + WriteCostPerByte Gas + // IterNextCostFlat should reflect a fixed cost for each call to Next() on an + // iterator. + IterNextCostFlat Gas +} + +// DefaultGasConfig returns a default GasConfig for gas metering. +// +// Note, these values are essentially arbitrary. They are not based on any specific +// computation or measurements, but mainly reflect relative costs, i.e. writes +// should be more expensive than reads. +func DefaultGasConfig() GasConfig { + return GasConfig{ + HasCost: 1000, + ReadCostFlat: 1000, + ReadCostPerByte: 3, + DeleteCost: 1500, + WriteCostFlat: 2000, + WriteCostPerByte: 30, + IterNextCostFlat: 30, + } +} + +// defaultGasMeter defines a default implementation of a GasMeter. +type defaultGasMeter struct { + limit Gas + consumed Gas +} + +// NewGasMeter returns a reference to a GasMeter with the provided limit. +func NewGasMeter(limit Gas) GasMeter { + return &defaultGasMeter{ + limit: limit, + } +} + +func (gm *defaultGasMeter) GasConsumed() Gas { + return gm.consumed +} + +// NOTE: This behavior should only be called when recovering from a panic when +// BlockGasMeter consumes gas past the gas limit. +func (gm *defaultGasMeter) GasConsumedToLimit() Gas { + if gm.IsPastLimit() { + return gm.limit + } + + return gm.consumed +} + +func (gm *defaultGasMeter) GasRemaining() Gas { + if gm.IsPastLimit() { + return 0 + } + + return gm.limit - gm.consumed +} + +func (gm *defaultGasMeter) Limit() Gas { + return gm.limit +} + +func (gm *defaultGasMeter) ConsumeGas(amount Gas, descriptor string) { + newConsumed, overflow := addGasOverflow(gm.consumed, amount) + if overflow { + panic(ErrorGasOverflow{descriptor}) + } + + if newConsumed > gm.limit { + gm.consumed = math.MaxUint64 + panic(ErrorOutOfGas{descriptor}) + } + + gm.consumed = newConsumed +} + +func (gm *defaultGasMeter) RefundGas(amount Gas, descriptor string) { + if gm.consumed < amount { + panic(ErrorNegativeGasConsumed{Descriptor: descriptor}) + } + + gm.consumed -= amount +} + +func (gm *defaultGasMeter) IsPastLimit() bool { + return gm.consumed > gm.limit +} + +func (gm *defaultGasMeter) IsOutOfGas() bool { + return gm.consumed >= gm.limit +} + +func (gm *defaultGasMeter) String() string { + return fmt.Sprintf("%T{limit: %d, consumed: %d}", gm, gm.limit, gm.consumed) +} + +// infiniteGasMeter defines a GasMeter with an infinite gas limit. +type infiniteGasMeter struct { + consumed Gas +} + +// NewInfiniteGasMeter returns a reference to a GasMeter with an infinite gas limit. +func NewInfiniteGasMeter() GasMeter { + return &infiniteGasMeter{ + consumed: 0, + } +} + +func (gm *infiniteGasMeter) GasConsumed() Gas { + return gm.consumed +} + +func (gm *infiniteGasMeter) GasConsumedToLimit() Gas { + return gm.consumed +} + +func (*infiniteGasMeter) GasRemaining() Gas { + return math.MaxUint64 +} + +func (*infiniteGasMeter) Limit() Gas { + return math.MaxUint64 +} + +func (gm *infiniteGasMeter) ConsumeGas(amount Gas, descriptor string) { + var overflow bool + + gm.consumed, overflow = addGasOverflow(gm.consumed, amount) + if overflow { + panic(ErrorGasOverflow{descriptor}) + } +} + +func (gm *infiniteGasMeter) RefundGas(amount Gas, descriptor string) { + if gm.consumed < amount { + panic(ErrorNegativeGasConsumed{Descriptor: descriptor}) + } + + gm.consumed -= amount +} + +func (*infiniteGasMeter) IsPastLimit() bool { + return false +} + +func (*infiniteGasMeter) IsOutOfGas() bool { + return false +} + +func (gm *infiniteGasMeter) String() string { + return fmt.Sprintf("%T{consumed: %d}", gm, gm.consumed) +} + +// addGasOverflow performs the addition operation on two uint64 integers and +// returns a boolean on whether or not the result overflows. +func addGasOverflow(a, b Gas) (Gas, bool) { + if math.MaxUint64-a < b { + return 0, true + } + + return a + b, false +} diff --git a/store/kv/gas/iterator.go b/store/kv/gas/iterator.go new file mode 100644 index 0000000000..a8043863a6 --- /dev/null +++ b/store/kv/gas/iterator.go @@ -0,0 +1,63 @@ +package gas + +import "cosmossdk.io/store/v2" + +var _ store.Iterator = (*iterator)(nil) + +type iterator struct { + gasMeter store.GasMeter + gasConfig store.GasConfig + parent store.Iterator +} + +func newIterator(parent store.Iterator, gm store.GasMeter, gc store.GasConfig) store.Iterator { + return &iterator{ + parent: parent, + gasConfig: gc, + gasMeter: gm, + } +} + +func (itr *iterator) Domain() ([]byte, []byte) { + return itr.parent.Domain() +} + +func (itr *iterator) Valid() bool { + return itr.parent.Valid() +} + +func (itr *iterator) Key() []byte { + return itr.parent.Key() +} + +func (itr *iterator) Value() []byte { + return itr.parent.Value() +} + +func (itr *iterator) Next() bool { + itr.consumeGasSeek() + return itr.parent.Next() +} + +func (itr *iterator) Close() { + itr.parent.Close() +} + +func (itr *iterator) Error() error { + return itr.parent.Error() +} + +// consumeGasSeek consumes a fixed amount of gas for each iteration step and a +// variable gas cost based on the current key and value's length. This is called +// prior to the iterator's Next() call. +func (itr *iterator) consumeGasSeek() { + if itr.Valid() { + key := itr.Key() + value := itr.Value() + + itr.gasMeter.ConsumeGas(itr.gasConfig.ReadCostPerByte*store.Gas(len(key)), store.GasDescValuePerByte) + itr.gasMeter.ConsumeGas(itr.gasConfig.ReadCostPerByte*store.Gas(len(value)), store.GasDescValuePerByte) + } + + itr.gasMeter.ConsumeGas(itr.gasConfig.IterNextCostFlat, store.GasDescIterNextCostFlat) +} diff --git a/store/kv/gas/store.go b/store/kv/gas/store.go new file mode 100644 index 0000000000..8cd4af2226 --- /dev/null +++ b/store/kv/gas/store.go @@ -0,0 +1,89 @@ +package gas + +import ( + "fmt" + "io" + + "cosmossdk.io/store/v2" +) + +var _ store.BranchedKVStore = (*Store)(nil) + +type Store struct { + parent store.KVStore + gasMeter store.GasMeter + gasConfig store.GasConfig +} + +func New(p store.KVStore, gm store.GasMeter, gc store.GasConfig) store.BranchedKVStore { + return &Store{ + parent: p, + gasMeter: gm, + gasConfig: gc, + } +} + +func (s *Store) GetStoreKey() string { + return s.parent.GetStoreKey() +} + +func (s *Store) GetStoreType() store.StoreType { + return s.parent.GetStoreType() +} + +func (s *Store) Get(key []byte) []byte { + s.gasMeter.ConsumeGas(s.gasConfig.ReadCostFlat, store.GasDescReadCostFlat) + + value := s.parent.Get(key) + s.gasMeter.ConsumeGas(s.gasConfig.ReadCostPerByte*store.Gas(len(key)), store.GasDescReadPerByte) + s.gasMeter.ConsumeGas(s.gasConfig.ReadCostPerByte*store.Gas(len(value)), store.GasDescReadPerByte) + + return value +} + +func (s *Store) Has(key []byte) bool { + s.gasMeter.ConsumeGas(s.gasConfig.HasCost, store.GasDescHas) + return s.parent.Has(key) +} + +func (s *Store) Set(key, value []byte) { + s.gasMeter.ConsumeGas(s.gasConfig.WriteCostFlat, store.GasDescWriteCostFlat) + s.gasMeter.ConsumeGas(s.gasConfig.WriteCostPerByte*store.Gas(len(key)), store.GasDescWritePerByte) + s.gasMeter.ConsumeGas(s.gasConfig.WriteCostPerByte*store.Gas(len(value)), store.GasDescWritePerByte) + s.parent.Set(key, value) +} + +func (s *Store) Delete(key []byte) { + s.gasMeter.ConsumeGas(s.gasConfig.DeleteCost, store.GasDescDelete) + s.parent.Delete(key) +} + +func (s *Store) GetChangeset() *store.Changeset { + return s.parent.GetChangeset() +} + +func (s *Store) Reset() error { + return s.parent.Reset() +} + +func (s *Store) Write() { + if b, ok := s.parent.(store.BranchedKVStore); ok { + b.Write() + } +} + +func (s *Store) Branch() store.BranchedKVStore { + panic(fmt.Sprintf("cannot call Branch() on %T", s)) +} + +func (s *Store) BranchWithTrace(_ io.Writer, _ store.TraceContext) store.BranchedKVStore { + panic(fmt.Sprintf("cannot call BranchWithTrace() on %T", s)) +} + +func (s *Store) Iterator(start, end []byte) store.Iterator { + return newIterator(s.parent.Iterator(start, end), s.gasMeter, s.gasConfig) +} + +func (s *Store) ReverseIterator(start, end []byte) store.Iterator { + return newIterator(s.parent.ReverseIterator(start, end), s.gasMeter, s.gasConfig) +} diff --git a/store/kv/gas/store_test.go b/store/kv/gas/store_test.go new file mode 100644 index 0000000000..49616eba70 --- /dev/null +++ b/store/kv/gas/store_test.go @@ -0,0 +1,111 @@ +package gas_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/kv/gas" + "cosmossdk.io/store/v2/kv/mem" +) + +const ( + storeKey = "storeKey" + gasLimit = store.Gas(1_000_000) +) + +type StoreTestSuite struct { + suite.Suite + + parent store.KVStore + gasKVStore store.BranchedKVStore + gasMeter store.GasMeter +} + +func TestStorageTestSuite(t *testing.T) { + suite.Run(t, &StoreTestSuite{}) +} + +func (s *StoreTestSuite) SetupTest() { + s.parent = mem.New(storeKey) + s.gasMeter = store.NewGasMeter(gasLimit) + s.gasKVStore = gas.New(s.parent, s.gasMeter, store.DefaultGasConfig()) +} + +func (s *StoreTestSuite) TearDownTest() { + err := s.gasKVStore.Reset() + s.Require().NoError(err) +} + +func (s *StoreTestSuite) TestGetStoreKey() { + s.Require().Equal(s.parent.GetStoreKey(), s.gasKVStore.GetStoreKey()) +} + +func (s *StoreTestSuite) TestGetStoreType() { + s.Require().Equal(s.parent.GetStoreType(), s.gasKVStore.GetStoreType()) +} + +func (s *StoreTestSuite) TestGet() { + key, value := []byte("key"), []byte("value") + s.parent.Set(key, value) + + s.Require().Equal(value, s.gasKVStore.Get(key)) + s.Require().Equal(store.Gas(1024), s.gasMeter.GasConsumed()) +} + +func (s *StoreTestSuite) TestHas() { + key, value := []byte("key"), []byte("value") + s.parent.Set(key, value) + + s.Require().True(s.gasKVStore.Has(key)) + s.Require().Equal(store.Gas(1000), s.gasMeter.GasConsumed()) +} + +func (s *StoreTestSuite) TestSet() { + s.gasKVStore.Set([]byte("key"), []byte("value")) + s.Require().Equal(store.Gas(2240), s.gasMeter.GasConsumed()) +} + +func (s *StoreTestSuite) TestDelete() { + key, value := []byte("key"), []byte("value") + s.parent.Set(key, value) + + s.gasKVStore.Delete(key) + s.Require().Equal(store.Gas(1500), s.gasMeter.GasConsumed()) +} + +func (s *StoreTestSuite) TestIterator() { + for i := 0; i < 100; i++ { + key := fmt.Sprintf("key%03d", i) // key000, key001, ..., key099 + val := fmt.Sprintf("val%03d", i) // val000, val001, ..., val099 + s.parent.Set([]byte(key), []byte(val)) + } + + itr := s.gasKVStore.Iterator(nil, nil) + defer itr.Close() + + for ; itr.Valid(); itr.Next() { + _ = itr.Key() + _ = itr.Value() + } + s.Require().Equal(store.Gas(6600), s.gasMeter.GasConsumed()) +} + +func (s *StoreTestSuite) TestReverseIterator() { + for i := 0; i < 100; i++ { + key := fmt.Sprintf("key%03d", i) // key000, key001, ..., key099 + val := fmt.Sprintf("val%03d", i) // val000, val001, ..., val099 + s.parent.Set([]byte(key), []byte(val)) + } + + itr := s.gasKVStore.ReverseIterator(nil, nil) + defer itr.Close() + + for ; itr.Valid(); itr.Next() { + _ = itr.Key() + _ = itr.Value() + } + s.Require().Equal(store.Gas(6600), s.gasMeter.GasConsumed()) +} diff --git a/store/kv/mem/store.go b/store/kv/mem/store.go index eb008a9f4d..47b423a4e6 100644 --- a/store/kv/mem/store.go +++ b/store/kv/mem/store.go @@ -8,10 +8,8 @@ import ( "cosmossdk.io/store/v2" ) -const ( - // degree defines the approximate number of items and children per B-tree node. - degree = 32 -) +// degree defines the approximate number of items and children per B-tree node. +const degree = 32 var _ store.KVStore = (*Store)(nil) diff --git a/store/kv/trace/store.go b/store/kv/trace/store.go index 0bb0ff2d64..ed0d9a3e9b 100644 --- a/store/kv/trace/store.go +++ b/store/kv/trace/store.go @@ -3,6 +3,7 @@ package trace import ( "encoding/base64" "encoding/json" + "fmt" "io" "github.com/cockroachdb/errors" @@ -90,11 +91,11 @@ func (s *Store) Write() { } func (s *Store) Branch() store.BranchedKVStore { - panic("cannot call Branch() on tracekv.Store") + panic(fmt.Sprintf("cannot call Branch() on %T", s)) } func (s *Store) BranchWithTrace(_ io.Writer, _ store.TraceContext) store.BranchedKVStore { - panic("cannot call BranchWithTrace() on tracekv.Store") + panic(fmt.Sprintf("cannot call BranchWithTrace() on %T", s)) } func (s *Store) Iterator(start, end []byte) store.Iterator { diff --git a/store/storage/storage_bench_test.go b/store/storage/storage_bench_test.go index 59c4d1994a..3d2d4c06a3 100644 --- a/store/storage/storage_bench_test.go +++ b/store/storage/storage_bench_test.go @@ -10,11 +10,12 @@ import ( "sort" "testing" + "github.com/stretchr/testify/require" + "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/storage/pebbledb" "cosmossdk.io/store/v2/storage/rocksdb" "cosmossdk.io/store/v2/storage/sqlite" - "github.com/stretchr/testify/require" ) var (