From 0a00d83664b48d8f94cdb85d552bf838a0632080 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Fri, 3 Jan 2025 16:02:47 -0500 Subject: [PATCH 1/3] init --- api/dependencies.go | 8 +- api/jsonrpc/server.go | 18 +- api/state/server.go | 10 +- chain/builder.go | 60 ++++--- chain/chain.go | 17 ++ chain/chaintest/balance_handler.go | 8 +- chain/default.go | 45 +++++ chain/dependencies.go | 26 +++ chain/option.go | 16 ++ chain/pre_executor.go | 23 ++- chain/processor.go | 65 +++++--- .../morpheusvm/storage/balance_handler.go | 2 +- examples/morpheusvm/storage/storage.go | 12 -- examples/morpheusvm/tests/transfer.go | 57 +++++++ examples/morpheusvm/vm/server.go | 7 +- examples/morpheusvm/vm/vm.go | 3 +- extension/tieredstorage/errors.go | 12 ++ extension/tieredstorage/option.go | 53 ++++++ .../tieredstorage/transaction_manager.go | 155 ++++++++++++++++++ .../tieredstorage/transaction_manager_test.go | 102 ++++++++++++ fees/dimension.go | 12 ++ internal/fees/manager.go | 24 ++- state/shim/no_op.go | 30 ++++ state/shim/shim.go | 23 +++ state/translated.go | 76 +++++++++ state/tstate/recorder_test.go | 16 +- state/tstate/tstate.go | 14 ++ state/tstate/tstate_test.go | 54 +++--- state/tstate/tstate_view.go | 10 +- vm/defaultvm/vm.go | 3 +- vm/option.go | 15 ++ vm/read_state.go | 45 +++++ vm/vm.go | 19 ++- 33 files changed, 909 insertions(+), 131 deletions(-) create mode 100644 chain/default.go create mode 100644 chain/option.go create mode 100644 extension/tieredstorage/errors.go create mode 100644 extension/tieredstorage/option.go create mode 100644 extension/tieredstorage/transaction_manager.go create mode 100644 extension/tieredstorage/transaction_manager_test.go create mode 100644 state/shim/no_op.go create mode 100644 state/shim/shim.go create mode 100644 state/translated.go create mode 100644 vm/read_state.go diff --git a/api/dependencies.go b/api/dependencies.go index 2da403f55c..9046c9eefd 100644 --- a/api/dependencies.go +++ b/api/dependencies.go @@ -41,7 +41,13 @@ type VM interface { context.Context, ) (map[ids.NodeID]*validators.GetValidatorOutput, map[string]struct{}) GetVerifyAuth() bool - ReadState(ctx context.Context, keys [][]byte) ([][]byte, []error) + // Access to Raw State + ReadState(ctx context.Context, keys [][]byte) (state.Immutable, error) ImmutableState(ctx context.Context) (state.Immutable, error) BalanceHandler() chain.BalanceHandler } + +type Chain interface { + // Access to executable state + Get() +} diff --git a/api/jsonrpc/server.go b/api/jsonrpc/server.go index 2d059bc564..f2c2c8d065 100644 --- a/api/jsonrpc/server.go +++ b/api/jsonrpc/server.go @@ -209,20 +209,20 @@ func (j *JSONRPCServer) ExecuteActions( storageKeysToRead = append(storageKeysToRead, []byte(key)) } - values, errs := j.vm.ReadState(ctx, storageKeysToRead) - for _, err := range errs { + im, err := j.vm.ReadState(ctx, storageKeysToRead) + if err != nil { + return fmt.Errorf("failed to read state: %w", err) + } + + for _, k := range storageKeysToRead { + v, err := im.GetValue(ctx, k) if err != nil && !errors.Is(err, database.ErrNotFound) { return fmt.Errorf("failed to read state: %w", err) } - } - for i, value := range values { - if value == nil { - continue - } - storage[string(storageKeysToRead[i])] = value + storage[string(k)] = v } - tsv := ts.NewView(stateKeysWithPermissions, storage) + tsv := ts.NewView(stateKeysWithPermissions, tstate.ImmutableScopeStorage(storage)) output, err := action.Execute( ctx, diff --git a/api/state/server.go b/api/state/server.go index 0a537f9526..edac658eff 100644 --- a/api/state/server.go +++ b/api/state/server.go @@ -51,9 +51,13 @@ func (s *JSONRPCStateServer) ReadState(req *http.Request, args *ReadStateRequest ctx, span := s.stateReader.Tracer().Start(req.Context(), "Server.ReadState") defer span.End() - var errs []error - res.Values, errs = s.stateReader.ReadState(ctx, args.Keys) - for _, err := range errs { + im, err := s.stateReader.ReadState(ctx, args.Keys) + if err != nil { + return err + } + for _, k := range args.Keys { + v, err := im.GetValue(ctx, k) + res.Values = append(res.Values, v) res.Errors = append(res.Errors, err.Error()) } return nil diff --git a/chain/builder.go b/chain/builder.go index b42ba00caf..302fe3a3f5 100644 --- a/chain/builder.go +++ b/chain/builder.go @@ -61,15 +61,16 @@ func HandlePreExecute(log logging.Logger, err error) bool { } type Builder struct { - tracer trace.Tracer - ruleFactory RuleFactory - log logging.Logger - metadataManager MetadataManager - balanceHandler BalanceHandler - mempool Mempool - validityWindow ValidityWindow - metrics *chainMetrics - config Config + tracer trace.Tracer + ruleFactory RuleFactory + log logging.Logger + metadataManager MetadataManager + transactionManagerFactory TransactionManagerFactory + balanceHandler BalanceHandler + mempool Mempool + validityWindow ValidityWindow + metrics *chainMetrics + config Config } func NewBuilder( @@ -77,6 +78,7 @@ func NewBuilder( ruleFactory RuleFactory, log logging.Logger, metadataManager MetadataManager, + transactionManagerFactory TransactionManagerFactory, balanceHandler BalanceHandler, mempool Mempool, validityWindow ValidityWindow, @@ -84,15 +86,16 @@ func NewBuilder( config Config, ) *Builder { return &Builder{ - tracer: tracer, - ruleFactory: ruleFactory, - log: log, - metadataManager: metadataManager, - balanceHandler: balanceHandler, - mempool: mempool, - validityWindow: validityWindow, - metrics: metrics, - config: config, + tracer: tracer, + ruleFactory: ruleFactory, + log: log, + metadataManager: metadataManager, + transactionManagerFactory: transactionManagerFactory, + balanceHandler: balanceHandler, + mempool: mempool, + validityWindow: validityWindow, + metrics: metrics, + config: config, } } @@ -289,8 +292,14 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent }() } + tm := c.transactionManagerFactory() + state, err := tm.ExecutableState(storage, height) + if err != nil { + return err + } + // Execute block - tsv := ts.NewView(stateKeys, storage) + tsv := ts.NewView(stateKeys, state) if err := tx.PreExecute(ctx, feeManager, c.balanceHandler, r, tsv, nextTime); err != nil { // We don't need to rollback [tsv] here because it will never // be committed. @@ -318,6 +327,10 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent blockLock.Lock() defer blockLock.Unlock() + if err := tm.AfterTX(ctx, tx, result, tsv, c.balanceHandler, feeManager, true); err != nil { + return err + } + // Ensure block isn't too big if ok, dimension := feeManager.Consume(result.Units, maxUnits); !ok { c.log.Debug( @@ -397,6 +410,11 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent c.metrics.emptyBlockBuilt.Inc() } + tm := c.transactionManagerFactory() + if err := tm.RawState(ts, height); err != nil { + return nil, nil, nil, err + } + // Update chain metadata heightKey := HeightKey(c.metadataManager.HeightPrefix()) heightKeyStr := string(heightKey) @@ -408,11 +426,11 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent keys.Add(heightKeyStr, state.Write) keys.Add(timestampKeyStr, state.Write) keys.Add(feeKeyStr, state.Write) - tsv := ts.NewView(keys, map[string][]byte{ + tsv := ts.NewView(keys, tstate.ImmutableScopeStorage(map[string][]byte{ heightKeyStr: binary.BigEndian.AppendUint64(nil, parent.Hght), timestampKeyStr: binary.BigEndian.AppendUint64(nil, uint64(parent.Tmstmp)), feeKeyStr: parentFeeManager.Bytes(), - }) + })) if err := tsv.Insert(ctx, heightKey, binary.BigEndian.AppendUint64(nil, height)); err != nil { return nil, nil, nil, fmt.Errorf("%w: unable to insert height", err) } diff --git a/chain/chain.go b/chain/chain.go index ea0b3002e2..1c0fe15653 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -40,17 +40,23 @@ func NewChain( authVM AuthVM, validityWindow ValidityWindow, config Config, + ops []Option, ) (*Chain, error) { metrics, err := newMetrics(registerer) if err != nil { return nil, err } + + options := &Options{} + applyOptions(options, ops) + return &Chain{ builder: NewBuilder( tracer, ruleFactory, logger, metadataManager, + options.TransactionManagerFactory, balanceHandler, mempool, validityWindow, @@ -64,6 +70,7 @@ func NewChain( authVerifiers, authVM, metadataManager, + options.TransactionManagerFactory, balanceHandler, validityWindow, metrics, @@ -78,6 +85,7 @@ func NewChain( ruleFactory, validityWindow, metadataManager, + options.TransactionManagerFactory, balanceHandler, ), blockParser: NewBlockParser(tracer, parser), @@ -120,3 +128,12 @@ func (c *Chain) PreExecute( func (c *Chain) ParseBlock(ctx context.Context, bytes []byte) (*ExecutionBlock, error) { return c.blockParser.ParseBlock(ctx, bytes) } + +func applyOptions(options *Options, ops []Option) { + for _, op := range ops { + op(options) + } + if options.TransactionManagerFactory == nil { + options.TransactionManagerFactory = NewDefaultTransactionManager + } +} diff --git a/chain/chaintest/balance_handler.go b/chain/chaintest/balance_handler.go index 27f57eab0d..7778a7951c 100644 --- a/chain/chaintest/balance_handler.go +++ b/chain/chaintest/balance_handler.go @@ -75,7 +75,7 @@ func TestBalanceHandler(t *testing.T, ctx context.Context, bf func() chain.Balan ts := tstate.New(1) tsv := ts.NewView( bh.SponsorStateKeys(addrOne), - ms.Storage, + tstate.ImmutableScopeStorage(ms.Storage), ) r.NoError(bh.Deduct(ctx, addrOne, tsv, 1)) @@ -95,7 +95,7 @@ func TestBalanceHandler(t *testing.T, ctx context.Context, bf func() chain.Balan ts := tstate.New(1) tsv := ts.NewView( bh.SponsorStateKeys(addrOne), - ms.Storage, + tstate.ImmutableScopeStorage(ms.Storage), ) r.Error(bh.Deduct(ctx, addrOne, tsv, 2)) @@ -115,7 +115,7 @@ func TestBalanceHandler(t *testing.T, ctx context.Context, bf func() chain.Balan ts := tstate.New(1) tsv := ts.NewView( bh.SponsorStateKeys(addrOne), - ms.Storage, + tstate.ImmutableScopeStorage(ms.Storage), ) r.NoError(bh.CanDeduct(ctx, addrOne, tsv, 1)) @@ -135,7 +135,7 @@ func TestBalanceHandler(t *testing.T, ctx context.Context, bf func() chain.Balan ts := tstate.New(1) tsv := ts.NewView( bh.SponsorStateKeys(addrOne), - ms.Storage, + tstate.ImmutableScopeStorage(ms.Storage), ) r.Error(bh.CanDeduct(ctx, addrOne, tsv, 2)) diff --git a/chain/default.go b/chain/default.go new file mode 100644 index 0000000000..3cea48a7fa --- /dev/null +++ b/chain/default.go @@ -0,0 +1,45 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +import ( + "context" + + "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" + + internalfees "github.com/ava-labs/hypersdk/internal/fees" +) + +var ( + _ Hooks = (*DefaultTransactionHooks)(nil) + _ TransactionManager = (*DefaultTransactionManager)(nil) +) + +type DefaultTransactionHooks struct{} + +func (*DefaultTransactionHooks) AfterTX( + _ context.Context, + _ *Transaction, + _ *Result, + _ state.Mutable, + _ BalanceHandler, + _ *internalfees.Manager, + _ bool, +) error { + return nil +} + +type DefaultTransactionManager struct { + shim.NoOp + DefaultTransactionHooks +} + +func NewDefaultTransactionManager() TransactionManager { + return &DefaultTransactionManager{} +} + +func DefaultTransactionManagerFactory() TransactionManagerFactory { + return NewDefaultTransactionManager +} diff --git a/chain/dependencies.go b/chain/dependencies.go index 82d5613351..1218d7d28f 100644 --- a/chain/dependencies.go +++ b/chain/dependencies.go @@ -11,6 +11,9 @@ import ( "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/fees" "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" + + internalfees "github.com/ava-labs/hypersdk/internal/fees" ) type Parser interface { @@ -204,3 +207,26 @@ type AuthFactory interface { MaxUnits() (bandwidth uint64, compute uint64) Address() codec.Address } + +// Hooks can be used to change the fees and compute units of a +// transaction +// This is called after transaction execution, but before the end of block execution +type Hooks interface { + // AfterTX is called post-transaction execution + AfterTX( + ctx context.Context, + tx *Transaction, + result *Result, + mu state.Mutable, + bh BalanceHandler, + fm *internalfees.Manager, + isBuilder bool, + ) error +} + +type TransactionManager interface { + shim.Execution + Hooks +} + +type TransactionManagerFactory func() TransactionManager diff --git a/chain/option.go b/chain/option.go new file mode 100644 index 0000000000..216c1f1521 --- /dev/null +++ b/chain/option.go @@ -0,0 +1,16 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +type Options struct { + TransactionManagerFactory TransactionManagerFactory +} + +type Option func(*Options) + +func WithTransactionManagerFactory(factory TransactionManagerFactory) Option { + return func(opts *Options) { + opts.TransactionManagerFactory = factory + } +} diff --git a/chain/pre_executor.go b/chain/pre_executor.go index 617f317100..9e094cc59a 100644 --- a/chain/pre_executor.go +++ b/chain/pre_executor.go @@ -13,23 +13,26 @@ import ( ) type PreExecutor struct { - ruleFactory RuleFactory - validityWindow ValidityWindow - metadataManager MetadataManager - balanceHandler BalanceHandler + ruleFactory RuleFactory + validityWindow ValidityWindow + metadataManager MetadataManager + transactionManagerFactory TransactionManagerFactory + balanceHandler BalanceHandler } func NewPreExecutor( ruleFactory RuleFactory, validityWindow ValidityWindow, metadataManager MetadataManager, + transactionManagerFactory TransactionManagerFactory, balanceHandler BalanceHandler, ) *PreExecutor { return &PreExecutor{ - ruleFactory: ruleFactory, - validityWindow: validityWindow, - metadataManager: metadataManager, - balanceHandler: balanceHandler, + ruleFactory: ruleFactory, + validityWindow: validityWindow, + metadataManager: metadataManager, + transactionManagerFactory: transactionManagerFactory, + balanceHandler: balanceHandler, } } @@ -78,6 +81,8 @@ func (p *PreExecutor) PreExecute( } } + tm := p.transactionManagerFactory() + // PreExecute does not make any changes to state // // This may fail if the state we are utilizing is invalidated (if a trie @@ -87,7 +92,7 @@ func (p *PreExecutor) PreExecute( // Note, [PreExecute] ensures that the pending transaction does not have // an expiry time further ahead than [ValidityWindow]. This ensures anything // added to the [Mempool] is immediately executable. - if err := tx.PreExecute(ctx, nextFeeManager, p.balanceHandler, r, view, now); err != nil { + if err := tx.PreExecute(ctx, nextFeeManager, p.balanceHandler, r, tm.ImmutableView(view), now); err != nil { return err } return nil diff --git a/chain/processor.go b/chain/processor.go index 25772ebf63..024d943aca 100644 --- a/chain/processor.go +++ b/chain/processor.go @@ -74,16 +74,17 @@ func (b *ExecutionBlock) Txs() []*Transaction { } type Processor struct { - tracer trace.Tracer - log logging.Logger - ruleFactory RuleFactory - authVerificationWorkers workers.Workers - authVM AuthVM - metadataManager MetadataManager - balanceHandler BalanceHandler - validityWindow ValidityWindow - metrics *chainMetrics - config Config + tracer trace.Tracer + log logging.Logger + ruleFactory RuleFactory + authVerificationWorkers workers.Workers + authVM AuthVM + metadataManager MetadataManager + transactionManagerFactory TransactionManagerFactory + balanceHandler BalanceHandler + validityWindow ValidityWindow + metrics *chainMetrics + config Config } func NewProcessor( @@ -93,22 +94,24 @@ func NewProcessor( authVerificationWorkers workers.Workers, authVM AuthVM, metadataManager MetadataManager, + transactionManagerFactory TransactionManagerFactory, balanceHandler BalanceHandler, validityWindow ValidityWindow, metrics *chainMetrics, config Config, ) *Processor { return &Processor{ - tracer: tracer, - log: log, - ruleFactory: ruleFactory, - authVerificationWorkers: authVerificationWorkers, - authVM: authVM, - metadataManager: metadataManager, - balanceHandler: balanceHandler, - validityWindow: validityWindow, - metrics: metrics, - config: config, + tracer: tracer, + log: log, + ruleFactory: ruleFactory, + authVerificationWorkers: authVerificationWorkers, + authVM: authVM, + metadataManager: metadataManager, + transactionManagerFactory: transactionManagerFactory, + balanceHandler: balanceHandler, + validityWindow: validityWindow, + metrics: metrics, + config: config, } } @@ -192,6 +195,11 @@ func (p *Processor) Execute( return nil, nil, err } + tm := p.transactionManagerFactory() + if err := tm.RawState(ts, b.Height()); err != nil { + return nil, nil, err + } + // Update chain metadata heightKeyStr := string(heightKey) timestampKeyStr := string(timestampKey) @@ -201,11 +209,11 @@ func (p *Processor) Execute( keys.Add(heightKeyStr, state.Write) keys.Add(timestampKeyStr, state.Write) keys.Add(feeKeyStr, state.Write) - tsv := ts.NewView(keys, map[string][]byte{ + tsv := ts.NewView(keys, tstate.ImmutableScopeStorage(map[string][]byte{ heightKeyStr: parentHeightRaw, timestampKeyStr: parentTimestampRaw, feeKeyStr: parentFeeManager.Bytes(), - }) + })) if err := tsv.Insert(ctx, heightKey, binary.BigEndian.AppendUint64(nil, b.Hght)); err != nil { return nil, nil, err } @@ -347,11 +355,17 @@ func (p *Processor) executeTxs( return err } + tm := p.transactionManagerFactory() + state, err := tm.ExecutableState(tstate.ImmutableScopeStorage(storage), b.Height()) + if err != nil { + return err + } + // Execute transaction // // It is critical we explicitly set the scope before each transaction is // processed - tsv := ts.NewView(stateKeys, storage) + tsv := ts.NewView(stateKeys, state) // Ensure we have enough funds to pay fees if err := tx.PreExecute(ctx, feeManager, p.balanceHandler, r, tsv, t); err != nil { @@ -362,6 +376,11 @@ func (p *Processor) executeTxs( if err != nil { return err } + + if err := tm.AfterTX(ctx, tx, result, tsv, p.balanceHandler, feeManager, false); err != nil { + return err + } + results[i] = result // Commit results to parent [TState] diff --git a/examples/morpheusvm/storage/balance_handler.go b/examples/morpheusvm/storage/balance_handler.go index 4fee2ef681..0c84ae89f5 100644 --- a/examples/morpheusvm/storage/balance_handler.go +++ b/examples/morpheusvm/storage/balance_handler.go @@ -17,7 +17,7 @@ type BalanceHandler struct{} func (*BalanceHandler) SponsorStateKeys(addr codec.Address) state.Keys { return state.Keys{ - string(BalanceKey(addr)): state.Read | state.Write, + string(BalanceKey(addr)): state.All, } } diff --git a/examples/morpheusvm/storage/storage.go b/examples/morpheusvm/storage/storage.go index 1ff48b0009..f7bbac0266 100644 --- a/examples/morpheusvm/storage/storage.go +++ b/examples/morpheusvm/storage/storage.go @@ -62,18 +62,6 @@ func getBalance( return k, bal, exists, err } -// Used to serve RPC queries -func GetBalanceFromState( - ctx context.Context, - f ReadState, - addr codec.Address, -) (uint64, error) { - k := BalanceKey(addr) - values, errs := f(ctx, [][]byte{k}) - bal, _, err := innerGetBalance(values[0], errs[0]) - return bal, err -} - func innerGetBalance( v []byte, err error, diff --git a/examples/morpheusvm/tests/transfer.go b/examples/morpheusvm/tests/transfer.go index 11d3387437..4b6921b201 100644 --- a/examples/morpheusvm/tests/transfer.go +++ b/examples/morpheusvm/tests/transfer.go @@ -9,10 +9,12 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/hypersdk/api/indexer" "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/crypto/ed25519" "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" + "github.com/ava-labs/hypersdk/fees" "github.com/ava-labs/hypersdk/tests/registry" tworkload "github.com/ava-labs/hypersdk/tests/workload" @@ -44,3 +46,58 @@ var _ = registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.F require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) }) + +var _ = registry.Register(TestsRegistry, "Read From Memory Refund", func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork) { + require := require.New(t) + other, err := ed25519.GeneratePrivateKey() + require.NoError(err) + toAddress := auth.NewED25519Address(other.PublicKey()) + + // We first warm up the state we want to test + authFactory := tn.Configuration().AuthFactories()[0] + tx, err := tn.GenerateTx(context.Background(), []chain.Action{&actions.Transfer{ + To: toAddress, + Value: 1, + Memo: []byte("warming up keys"), + }}, + authFactory, + ) + require.NoError(err) + + timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) + defer timeoutCtxFnc() + + // This call implicitly enforces that the state is warmed up by having the + // VM produce + accept a new block + require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) + + indexerCli := indexer.NewClient(tn.URIs()[0]) + resp, success, err := indexerCli.GetTx(timeoutCtx, tx.GetID()) + require.True(success) + require.NoError(err) + coldFee := resp.Fee + coldUnits := resp.Units + + // If TX successful, state is now warmed up + // Now we can test the refund + tx, err = tn.GenerateTx(context.Background(), []chain.Action{&actions.Transfer{ + To: toAddress, + Value: 1, + Memo: []byte("testing refunds"), + }}, + authFactory, + ) + require.NoError(err) + + require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) + + queryCtx, queryCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) + defer queryCtxFnc() + + resp, success, err = indexerCli.GetTx(queryCtx, tx.GetID()) + require.NoError(err) + require.True(success) + + require.Less(resp.Fee, coldFee) + require.Less(resp.Units[fees.StorageRead], coldUnits[fees.StorageRead]) +}) diff --git a/examples/morpheusvm/vm/server.go b/examples/morpheusvm/vm/server.go index 85e2e3e94b..682e1486e9 100644 --- a/examples/morpheusvm/vm/server.go +++ b/examples/morpheusvm/vm/server.go @@ -56,7 +56,12 @@ func (j *JSONRPCServer) Balance(req *http.Request, args *BalanceArgs, reply *Bal ctx, span := j.vm.Tracer().Start(req.Context(), "Server.Balance") defer span.End() - balance, err := storage.GetBalanceFromState(ctx, j.vm.ReadState, args.Address) + im, err := j.vm.ReadState(ctx, [][]byte{storage.BalanceKey(args.Address)}) + if err != nil { + return err + } + + balance, err := storage.GetBalance(ctx, im, args.Address) if err != nil { return err } diff --git a/examples/morpheusvm/vm/vm.go b/examples/morpheusvm/vm/vm.go index 2d0a296b4c..71a43f42ce 100644 --- a/examples/morpheusvm/vm/vm.go +++ b/examples/morpheusvm/vm/vm.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" "github.com/ava-labs/hypersdk/examples/morpheusvm/storage" + "github.com/ava-labs/hypersdk/extension/tieredstorage" "github.com/ava-labs/hypersdk/genesis" "github.com/ava-labs/hypersdk/state/metadata" "github.com/ava-labs/hypersdk/vm" @@ -57,7 +58,7 @@ func init() { // NewWithOptions returns a VM with the specified options func New(options ...vm.Option) (*vm.VM, error) { - options = append(options, With()) // Add MorpheusVM API + options = append(options, With(), tieredstorage.With()) // Add MorpheusVM API return defaultvm.New( consts.Version, genesis.DefaultGenesisFactory{}, diff --git a/extension/tieredstorage/errors.go b/extension/tieredstorage/errors.go new file mode 100644 index 0000000000..9bf8fd8716 --- /dev/null +++ b/extension/tieredstorage/errors.go @@ -0,0 +1,12 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tieredstorage + +import "errors" + +var ( + errFailedToParseMaxChunks = errors.New("failed to parse max chunks") + errValueTooShortForSuffix = errors.New("value is too short to contain suffix") + errFailedToRefund = errors.New("failed to refund units consumed") +) diff --git a/extension/tieredstorage/option.go b/extension/tieredstorage/option.go new file mode 100644 index 0000000000..956bf4651e --- /dev/null +++ b/extension/tieredstorage/option.go @@ -0,0 +1,53 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tieredstorage + +import ( + "github.com/ava-labs/hypersdk/api" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/vm" +) + +const Namespace = "tieredStorageTransactionManager" + +var _ chain.TransactionManager = (*TieredStorageTransactionManager)(nil) + +type Config struct { + Epsilon uint64 `json:"epsilon"` + StorageReadKeyRefund uint64 `json:"storageReadKeyRefund"` + StorageReadValueRefund uint64 `json:"storageReadValueRefund"` +} + +func NewDefaultConfig() Config { + return Config{ + Epsilon: 100, + StorageReadKeyRefund: 1, + StorageReadValueRefund: 1, + } +} + +func With() vm.Option { + return vm.NewOption(Namespace, NewDefaultConfig(), OptionFunc) +} + +func OptionFunc(_ api.VM, config Config) (vm.Opt, error) { + return vm.NewOpt( + vm.WithChainOptions( + chain.WithTransactionManagerFactory( + NewTieredStorageTransactionManager(config), + ), + ), + vm.WithExecutionShim( + NewTieredStorageTransactionManager(config)(), + ), + ), nil +} + +func NewTieredStorageTransactionManager(config Config) chain.TransactionManagerFactory { + return func() chain.TransactionManager { + return &TieredStorageTransactionManager{ + config: config, + } + } +} diff --git a/extension/tieredstorage/transaction_manager.go b/extension/tieredstorage/transaction_manager.go new file mode 100644 index 0000000000..08c03f2d31 --- /dev/null +++ b/extension/tieredstorage/transaction_manager.go @@ -0,0 +1,155 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tieredstorage + +import ( + "context" + "encoding/binary" + + "github.com/ava-labs/avalanchego/utils/maybe" + + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/consts" + "github.com/ava-labs/hypersdk/fees" + "github.com/ava-labs/hypersdk/internal/math" + "github.com/ava-labs/hypersdk/keys" + "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/tstate" + + safemath "github.com/ava-labs/avalanchego/utils/math" + internalfees "github.com/ava-labs/hypersdk/internal/fees" +) + +var _ chain.TransactionManager = (*TieredStorageTransactionManager)(nil) + +type TieredStorageTransactionManager struct { + config Config + hotKeys map[string]uint16 +} + +func newTieredStorageTransactionManager(config Config) *TieredStorageTransactionManager { + return &TieredStorageTransactionManager{ + config: config, + } +} + +// MutableView implements chain.TransactionManager. +func (*TieredStorageTransactionManager) MutableView(mu state.Mutable, blockHeight uint64) state.Mutable { + return state.NewTranslatedMutable(mu, blockHeight) +} + +func (*TieredStorageTransactionManager) ImmutableView(im state.Immutable) state.Immutable { + return state.NewTranslatedImmutable(im) +} + +// AfterTX issues fee refunds and, if the validator is calling, unit refunds +func (t *TieredStorageTransactionManager) AfterTX( + ctx context.Context, + tx *chain.Transaction, + result *chain.Result, + mu state.Mutable, + bh chain.BalanceHandler, + fm *internalfees.Manager, + isBuilder bool, +) error { + // We deduct fees regardless of whether the builder or processor is calling + readsRefundOp := math.NewUint64Operator(0) + for _, v := range t.hotKeys { + readsRefundOp.Add(t.config.StorageReadKeyRefund) + readsRefundOp.MulAdd(uint64(v), t.config.StorageReadValueRefund) + } + readRefundUnits, err := readsRefundOp.Value() + if err != nil { + return err + } + + refundDims := fees.Dimensions{0, 0, readRefundUnits, 0, 0} + refundFee, err := fm.Fee(refundDims) + if err != nil { + return err + } + if err := bh.AddBalance(ctx, tx.Auth.Sponsor(), mu, refundFee); err != nil { + return err + } + + if !isBuilder { + if err := t.refundUnitsConsumed(fm, refundDims); err != nil { + return err + } + } + + newUnits, err := fees.Sub(result.Units, refundDims) + if err != nil { + return err + } + + result.Units = newUnits + result.Fee -= refundFee + + return nil +} + +// ExecutableState implements chain.TransactionManager. +func (t *TieredStorageTransactionManager) ExecutableState( + mp map[string][]byte, + blockHeight uint64, +) (state.Immutable, error) { + unsuffixedStorage := make(map[string][]byte) + hotKeys := make(map[string]uint16) + + for k, v := range mp { + if len(v) < consts.Uint64Len { + return nil, errValueTooShortForSuffix + } + + lastTouched := binary.BigEndian.Uint64(v[len(v)-consts.Uint64Len:]) + + memoryThreshold, err := safemath.Sub(blockHeight, t.config.Epsilon) + if lastTouched >= memoryThreshold || err == safemath.ErrUnderflow { + maxChunks, ok := keys.MaxChunks([]byte(k)) + if !ok { + return nil, errFailedToParseMaxChunks + } + hotKeys[k] = maxChunks + } + + unsuffixedStorage[k] = v[:len(v)-consts.Uint64Len] + } + + t.hotKeys = hotKeys + return tstate.ImmutableScopeStorage(unsuffixedStorage), nil +} + +func (*TieredStorageTransactionManager) RawState(ts *tstate.TState, blockHeight uint64) error { + pendingChangedKeys := ts.ChangedKeys() + for k := range pendingChangedKeys { + if pendingChangedKeys[k].HasValue() { + pendingChangedKeys[k] = maybe.Some( + binary.BigEndian.AppendUint64( + pendingChangedKeys[k].Value(), + blockHeight, + ), + ) + } + } + + ts.SetChangedKeys(pendingChangedKeys) + return nil +} + +func (*TieredStorageTransactionManager) refundUnitsConsumed( + fm *internalfees.Manager, + refundDims fees.Dimensions, +) error { + ok, _ := fm.Refund(refundDims) + if !ok { + return errFailedToRefund + } + return nil +} + +func (t *TieredStorageTransactionManager) IsHotKey(key string) bool { + _, ok := t.hotKeys[key] + return ok +} diff --git a/extension/tieredstorage/transaction_manager_test.go b/extension/tieredstorage/transaction_manager_test.go new file mode 100644 index 0000000000..72f09d8c1d --- /dev/null +++ b/extension/tieredstorage/transaction_manager_test.go @@ -0,0 +1,102 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tieredstorage + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/consts" +) + +var ( + testKey = "testKey" + // BLk height is 0 + testVal = binary.BigEndian.AppendUint64([]byte("testVal"), 0) +) + +func TestTransactionManagerHotKeys(t *testing.T) { + tests := []struct { + name string + rawState map[string][]byte + epsilon uint64 + blockHeight uint64 + key string + isHotKey bool + }{ + { + name: "empty state has no hot keys", + rawState: map[string][]byte{}, + }, + { + name: "state with hot key", + rawState: map[string][]byte{testKey: testVal}, + epsilon: 1, + blockHeight: 1, + key: testKey, + isHotKey: true, + }, + { + name: "state with hot key and no underflow", + rawState: map[string][]byte{testKey: testVal}, + epsilon: 2, + blockHeight: 1, + key: testKey, + isHotKey: true, + }, + { + name: "state with cold key", + rawState: map[string][]byte{testKey: testVal}, + epsilon: 2, + blockHeight: 3, + key: testKey, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + tm := newTieredStorageTransactionManager(Config{Epsilon: tt.epsilon}) + + _, err := tm.ExecutableState(tt.rawState, tt.blockHeight) + r.NoError(err) + + r.Equal(tt.isHotKey, tm.IsHotKey(tt.key)) + }) + } +} + +func TestTransactionManagerShim(t *testing.T) { + tests := []struct { + name string + rawState map[string][]byte + err error + }{ + { + name: "rawDB with prefixes", + rawState: map[string][]byte{ + testKey: testVal, + }, + }, + { + name: "rawDB with no prefixes", + rawState: map[string][]byte{ + testKey: testVal[:len(testVal)-consts.Uint64Len], + }, + err: errValueTooShortForSuffix, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + tm := newTieredStorageTransactionManager(Config{Epsilon: 1}) + + _, err := tm.ExecutableState(tt.rawState, 1) + r.Equal(tt.err, err) + }) + } +} diff --git a/fees/dimension.go b/fees/dimension.go index 8e85f26e1a..c12cf73dbf 100644 --- a/fees/dimension.go +++ b/fees/dimension.go @@ -48,6 +48,18 @@ func Add(a, b Dimensions) (Dimensions, error) { return d, nil } +func Sub(a, b Dimensions) (Dimensions, error) { + d := Dimensions{} + for i := Dimension(0); i < FeeDimensions; i++ { + v, err := math.Sub(a[i], b[i]) + if err != nil { + return Dimensions{}, err + } + d[i] = v + } + return d, nil +} + func MulSum(a, b Dimensions) (uint64, error) { val := uint64(0) for i := Dimension(0); i < FeeDimensions; i++ { diff --git a/internal/fees/manager.go b/internal/fees/manager.go index ee8781a779..4dd9baf862 100644 --- a/internal/fees/manager.go +++ b/internal/fees/manager.go @@ -68,7 +68,7 @@ func (f *Manager) LastConsumed(d fees.Dimension) uint64 { } func (f *Manager) lastConsumed(d fees.Dimension) uint64 { - start := consts.IntLen + dimensionStateLen*d + consts.Uint64Len + window.WindowSliceSize + start := consts.Int64Len + dimensionStateLen*d + consts.Uint64Len + window.WindowSliceSize return binary.BigEndian.Uint64(f.raw[start : start+consts.Uint64Len]) } @@ -155,6 +155,28 @@ func (f *Manager) Consume(d fees.Dimensions, l fees.Dimensions) (bool, fees.Dime return true, 0 } +func (f *Manager) Refund(d fees.Dimensions) (bool, fees.Dimension) { + f.l.Lock() + defer f.l.Unlock() + + // Ensure we can refund (don't want partial update of values) + for i := fees.Dimension(0); i < fees.FeeDimensions; i++ { + if _, err := math.Sub(f.lastConsumed(i), d[i]); err != nil { + return false, i + } + } + + // Commit to refund + for i := fees.Dimension(0); i < fees.FeeDimensions; i++ { + consumed, err := math.Sub(f.lastConsumed(i), d[i]) + if err != nil { + return false, i + } + f.setLastConsumed(i, consumed) + } + return true, 0 +} + func (f *Manager) Bytes() []byte { f.l.RLock() defer f.l.RUnlock() diff --git a/state/shim/no_op.go b/state/shim/no_op.go new file mode 100644 index 0000000000..402ea8c0ab --- /dev/null +++ b/state/shim/no_op.go @@ -0,0 +1,30 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package shim + +import ( + "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/tstate" +) + +type NoOp struct{} + +func (*NoOp) MutableView(mu state.Mutable, _ uint64) state.Mutable { + return mu +} + +func (*NoOp) ImmutableView(im state.Immutable) state.Immutable { + return im +} + +func (*NoOp) RawState(*tstate.TState, uint64) error { + return nil +} + +func (*NoOp) ExecutableState( + mp map[string][]byte, + _ uint64, +) (state.Immutable, error) { + return tstate.ImmutableScopeStorage(mp), nil +} diff --git a/state/shim/shim.go b/state/shim/shim.go new file mode 100644 index 0000000000..4a7c58cf43 --- /dev/null +++ b/state/shim/shim.go @@ -0,0 +1,23 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package shim + +import ( + "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/tstate" +) + +// Execution is responsible for translating between raw state and execution state. +// This is useful for cases like managing suffix values +type Execution interface { + // ExecutableState converts a kv-store to a state which is suitable for TStateView + ExecutableState(map[string][]byte, uint64) (state.Immutable, error) + // MutableView is used for genesis initialization + MutableView(state.Mutable, uint64) state.Mutable + // ImmutableView is used for VM transaction pre-execution + ImmutableView(state.Immutable) state.Immutable + // ExecutableState converts the state changes of a block into a format + // suitable for DB storage + RawState(*tstate.TState, uint64) error +} diff --git a/state/translated.go b/state/translated.go new file mode 100644 index 0000000000..a1fcf6e349 --- /dev/null +++ b/state/translated.go @@ -0,0 +1,76 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "context" + "encoding/binary" + "errors" + + "github.com/ava-labs/avalanchego/database" + + "github.com/ava-labs/hypersdk/consts" +) + +var ( + _ Immutable = (*TranslatedImmutable)(nil) + _ Mutable = (*TranslatedMutable)(nil) + + ErrValueTooShortForSuffix = errors.New("value too short for suffix") +) + +type TranslatedImmutable struct { + im Immutable +} + +func NewTranslatedImmutable(im Immutable) TranslatedImmutable { + return TranslatedImmutable{im: im} +} + +// GetValue reads from a suffix-based key-value store. +func (t TranslatedImmutable) GetValue(ctx context.Context, key []byte) (value []byte, err error) { + return innerGetValue(t.im.GetValue(ctx, key)) +} + +type TranslatedMutable struct { + mu Mutable + suffix uint64 +} + +func NewTranslatedMutable(mu Mutable, suffix uint64) TranslatedMutable { + return TranslatedMutable{ + mu: mu, + suffix: suffix, + } +} + +// GetValue reads from a suffix-based key-value store. +// The value is returned with the suffix removed +func (t TranslatedMutable) GetValue(ctx context.Context, key []byte) (value []byte, err error) { + return innerGetValue(t.mu.GetValue(ctx, key)) +} + +// GetValue writes to a suffix-based key-value store. +// The suffix associated with t is appended to the value before writing. +func (t TranslatedMutable) Insert(ctx context.Context, key []byte, value []byte) error { + value = binary.BigEndian.AppendUint64(value, t.suffix) + return t.mu.Insert(ctx, key, value) +} + +func (t TranslatedMutable) Remove(ctx context.Context, key []byte) error { + return t.mu.Remove(ctx, key) +} + +func innerGetValue(v []byte, err error) ([]byte, error) { + if err == database.ErrNotFound { + return v, err + } + if err != nil { + return nil, err + } + if len(v) < consts.Uint64Len { + return nil, ErrValueTooShortForSuffix + } + return v[:len(v)-consts.Uint64Len], nil +} diff --git a/state/tstate/recorder_test.go b/state/tstate/recorder_test.go index e541cdef39..db17a2850e 100644 --- a/state/tstate/recorder_test.go +++ b/state/tstate/recorder_test.go @@ -54,7 +54,7 @@ func createKeysValues(keys [][]byte) map[string][]byte { return out } -func (i immutableScopeStorage) duplicate() immutableScopeStorage { +func (i ImmutableScopeStorage) duplicate() ImmutableScopeStorage { other := make(map[string][]byte, len(i)) for k, v := range i { other[k] = v @@ -84,7 +84,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source existingKeyValue := createKeysValues(existingKeys) // create a long living recorder. - recorder := NewRecorder(immutableScopeStorage(existingKeyValue).duplicate()) + recorder := NewRecorder(ImmutableScopeStorage(existingKeyValue).duplicate()) for opIdx := byte(0); opIdx < operationCount; opIdx++ { opType := r.Intn(fuzzerOpCount) @@ -95,7 +95,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source // validate operation agaist TStateView stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, immutableScopeStorage(existingKeyValue).duplicate()).Insert(context.Background(), key, []byte{1, 2, 3})) + require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Insert(context.Background(), key, []byte{1, 2, 3})) existingKeyValue[string(key)] = []byte{1, 2, 3} case fuzzerOpInsertNonExistingKey: // insert non existing key @@ -107,7 +107,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source // validate operation agaist TStateView stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, immutableScopeStorage(existingKeyValue).duplicate()).Insert(context.Background(), key, []byte{1, 2, 3, 4})) + require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Insert(context.Background(), key, []byte{1, 2, 3, 4})) // since we've modified the recorder state, we need to update our own. existingKeys = append(existingKeys, key) @@ -118,7 +118,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source // validate operation agaist TStateView stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, immutableScopeStorage(existingKeyValue).duplicate()).Remove(context.Background(), existingKeys[keyIdx])) + require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Remove(context.Background(), existingKeys[keyIdx])) // since we've modified the recorder state, we need to update our own. delete(existingKeyValue, string(existingKeys[keyIdx])) @@ -129,7 +129,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source // validate operation agaist TStateView stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, immutableScopeStorage(existingKeyValue).duplicate()).Remove(context.Background(), nonExistingKeys[keyIdx])) + require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Remove(context.Background(), nonExistingKeys[keyIdx])) case fuzzerOpGetExistingKey: // get value of existing key keyIdx := r.Intn(len(existingKeys)) recorderValue, err := recorder.GetValue(context.Background(), existingKeys[keyIdx]) @@ -137,7 +137,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source // validate operation agaist TStateView stateKeys := recorder.GetStateKeys() - stateValue, err := New(0).NewView(stateKeys, immutableScopeStorage(existingKeyValue)).GetValue(context.Background(), existingKeys[keyIdx]) + stateValue, err := New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue)).GetValue(context.Background(), existingKeys[keyIdx]) require.NoError(err) // both the recorder and the stateview should return the same value. @@ -149,7 +149,7 @@ func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source // validate operation agaist TStateView stateKeys := recorder.GetStateKeys() - _, err = New(0).NewView(stateKeys, immutableScopeStorage(existingKeyValue)).GetValue(context.Background(), nonExistingKeys[keyIdx]) + _, err = New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue)).GetValue(context.Background(), nonExistingKeys[keyIdx]) require.ErrorIs(err, database.ErrNotFound) } } diff --git a/state/tstate/tstate.go b/state/tstate/tstate.go index a2577db122..289cddc300 100644 --- a/state/tstate/tstate.go +++ b/state/tstate/tstate.go @@ -80,3 +80,17 @@ func (ts *TState) ExportMerkleDBView( return view.NewView(ctx, merkledb.ViewChanges{MapOps: ts.changedKeys, ConsumeBytes: true}) } + +func (ts *TState) ChangedKeys() map[string]maybe.Maybe[[]byte] { + ts.l.RLock() + defer ts.l.RUnlock() + + return ts.changedKeys +} + +func (ts *TState) SetChangedKeys(mp map[string]maybe.Maybe[[]byte]) { + ts.l.Lock() + defer ts.l.Unlock() + + ts.changedKeys = mp +} diff --git a/state/tstate/tstate_test.go b/state/tstate/tstate_test.go index 5c03a55da3..3cf6561c63 100644 --- a/state/tstate/tstate_test.go +++ b/state/tstate/tstate_test.go @@ -66,7 +66,7 @@ func TestScope(t *testing.T) { ts := New(10) // No Scope - tsv := ts.NewView(state.Keys{}, map[string][]byte{}) + tsv := ts.NewView(state.Keys{}, ImmutableScopeStorage(map[string][]byte{})) val, err := tsv.GetValue(ctx, testKey) require.ErrorIs(ErrInvalidKeyOrPermission, err) require.Nil(val) @@ -82,7 +82,7 @@ func TestGetValue(t *testing.T) { ts := New(10) // Set Scope - tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, map[string][]byte{string(testKey): testVal}) + tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{string(testKey): testVal})) val, err := tsv.GetValue(ctx, testKey) require.NoError(err, "unable to get value") require.Equal(testVal, val, "value was not saved correctly") @@ -94,12 +94,12 @@ func TestDeleteCommitGet(t *testing.T) { ts := New(10) // Delete value - tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, map[string][]byte{string(testKey): testVal}) + tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{string(testKey): testVal})) require.NoError(tsv.Remove(ctx, testKey)) tsv.Commit() // Check deleted - tsv = ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, map[string][]byte{string(testKey): testVal}) + tsv = ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{string(testKey): testVal})) val, err := tsv.GetValue(ctx, testKey) require.ErrorIs(err, database.ErrNotFound) require.Nil(val) @@ -111,7 +111,7 @@ func TestGetValueNoStorage(t *testing.T) { ts := New(10) // SetScope but dont add to storage - tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, map[string][]byte{}) + tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{})) _, err := tsv.GetValue(ctx, testKey) require.ErrorIs(database.ErrNotFound, err, "data should not exist") } @@ -122,7 +122,7 @@ func TestInsertNew(t *testing.T) { ts := New(10) // SetScope - tsv := ts.NewView(state.Keys{string(testKey): state.All}, map[string][]byte{}) + tsv := ts.NewView(state.Keys{string(testKey): state.All}, ImmutableScopeStorage(map[string][]byte{})) // Insert key require.NoError(tsv.Insert(ctx, testKey, testVal)) @@ -143,7 +143,7 @@ func TestInsertInvalid(t *testing.T) { // SetScope key := binary.BigEndian.AppendUint16([]byte("hello"), 0) - tsv := ts.NewView(state.Keys{string(key): state.Read | state.Write}, map[string][]byte{}) + tsv := ts.NewView(state.Keys{string(key): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{})) // Insert key err := tsv.Insert(ctx, key, []byte("cool")) @@ -160,7 +160,7 @@ func TestInsertUpdate(t *testing.T) { ts := New(10) // SetScope and add - tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, map[string][]byte{string(testKey): testVal}) + tsv := ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{string(testKey): testVal})) require.Equal(0, ts.OpIndex()) // Insert key @@ -176,7 +176,7 @@ func TestInsertUpdate(t *testing.T) { // Check value after commit tsv.Commit() - tsv = ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, map[string][]byte{string(testKey): testVal}) + tsv = ts.NewView(state.Keys{string(testKey): state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{string(testKey): testVal})) val, err = tsv.GetValue(ctx, testKey) require.NoError(err) require.Equal(newVal, val, "value was not committed correctly") @@ -188,7 +188,7 @@ func TestInsertRemoveInsert(t *testing.T) { ts := New(10) // SetScope and add - tsv := ts.NewView(state.Keys{key2str: state.All}, map[string][]byte{}) + tsv := ts.NewView(state.Keys{key2str: state.All}, ImmutableScopeStorage(map[string][]byte{})) require.Equal(0, ts.OpIndex()) // Insert key for first time @@ -260,7 +260,7 @@ func TestModifyRemoveInsert(t *testing.T) { ts := New(10) // SetScope and add - tsv := ts.NewView(state.Keys{key2str: state.All}, map[string][]byte{key2str: testVal}) + tsv := ts.NewView(state.Keys{key2str: state.All}, ImmutableScopeStorage(map[string][]byte{key2str: testVal})) require.Equal(0, ts.OpIndex()) // Modify existing key @@ -314,7 +314,7 @@ func TestModifyRevert(t *testing.T) { ts := New(10) // SetScope and add - tsv := ts.NewView(state.Keys{key2str: state.Read | state.Write}, map[string][]byte{key2str: testVal}) + tsv := ts.NewView(state.Keys{key2str: state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{key2str: testVal})) require.Equal(0, ts.OpIndex()) // Modify existing key @@ -354,7 +354,7 @@ func TestModifyModify(t *testing.T) { ts := New(10) // SetScope and add - tsv := ts.NewView(state.Keys{key2str: state.Read | state.Write}, map[string][]byte{key2str: testVal}) + tsv := ts.NewView(state.Keys{key2str: state.Read | state.Write}, ImmutableScopeStorage(map[string][]byte{key2str: testVal})) require.Equal(0, ts.OpIndex()) // Modify existing key @@ -401,7 +401,7 @@ func TestRemoveInsertRollback(t *testing.T) { ctx := context.TODO() // Insert - tsv := ts.NewView(state.Keys{string(testKey): state.All}, map[string][]byte{}) + tsv := ts.NewView(state.Keys{string(testKey): state.All}, ImmutableScopeStorage(map[string][]byte{})) require.NoError(tsv.Insert(ctx, testKey, testVal)) v, err := tsv.GetValue(ctx, testKey) require.NoError(err) @@ -447,7 +447,7 @@ func TestRestoreInsert(t *testing.T) { vals := [][]byte{[]byte("val1"), []byte("val2"), []byte("val3")} // Store keys - tsv := ts.NewView(keySet, map[string][]byte{}) + tsv := ts.NewView(keySet, ImmutableScopeStorage(map[string][]byte{})) for i, key := range keys { require.NoError(tsv.Insert(ctx, key, vals[i])) } @@ -502,11 +502,11 @@ func TestRestoreDelete(t *testing.T) { key3str: state.Read | state.Write, } vals := [][]byte{[]byte("val1"), []byte("val2"), []byte("val3")} - tsv := ts.NewView(keySet, map[string][]byte{ + tsv := ts.NewView(keySet, ImmutableScopeStorage(map[string][]byte{ string(keys[0]): vals[0], string(keys[1]): vals[1], string(keys[2]): vals[2], - }) + })) // Check scope for i, key := range keys { @@ -562,7 +562,7 @@ func TestCreateView(t *testing.T) { vals := [][]byte{[]byte("val1"), []byte("val2"), []byte("val3")} // Add - tsv := ts.NewView(keySet, map[string][]byte{}) + tsv := ts.NewView(keySet, ImmutableScopeStorage(map[string][]byte{})) for i, key := range keys { require.NoError(tsv.Insert(ctx, key, vals[i]), "error inserting value") val, err := tsv.GetValue(ctx, key) @@ -579,7 +579,7 @@ func TestCreateView(t *testing.T) { require.Equal(writeMap, writes) // Test warm modification - tsvM := ts.NewView(keySet, map[string][]byte{}) + tsvM := ts.NewView(keySet, ImmutableScopeStorage(map[string][]byte{})) require.NoError(tsvM.Insert(ctx, keys[0], vals[2])) allocates, writes = tsvM.KeyOperations() require.Empty(allocates) @@ -598,11 +598,11 @@ func TestCreateView(t *testing.T) { // Remove ts = New(10) - tsv = ts.NewView(keySet, map[string][]byte{ + tsv = ts.NewView(keySet, ImmutableScopeStorage(map[string][]byte{ string(keys[0]): vals[0], string(keys[1]): vals[1], string(keys[2]): vals[2], - }) + })) for _, key := range keys { err := tsv.Remove(ctx, key) require.NoError(err, "error removing from ts") @@ -661,7 +661,7 @@ func TestGetValuePermissions(t *testing.T) { require := require.New(t) ctx := context.TODO() ts := New(10) - tsv := ts.NewView(state.Keys{tt.key: tt.permission}, map[string][]byte{tt.key: testVal}) + tsv := ts.NewView(state.Keys{tt.key: tt.permission}, ImmutableScopeStorage(map[string][]byte{tt.key: testVal})) _, err := tsv.GetValue(ctx, []byte(tt.key)) require.ErrorIs(err, tt.expectedErr) }) @@ -706,7 +706,7 @@ func TestInsertPermissions(t *testing.T) { require := require.New(t) ctx := context.TODO() ts := New(10) - tsv := ts.NewView(state.Keys{tt.key: tt.permission}, map[string][]byte{tt.key: testVal}) + tsv := ts.NewView(state.Keys{tt.key: tt.permission}, ImmutableScopeStorage(map[string][]byte{tt.key: testVal})) err := tsv.Insert(ctx, []byte(tt.key), []byte("val")) require.ErrorIs(err, tt.expectedErr) }) @@ -751,7 +751,7 @@ func TestDeletePermissions(t *testing.T) { require := require.New(t) ctx := context.TODO() ts := New(10) - tsv := ts.NewView(state.Keys{tt.key: tt.permission}, map[string][]byte{tt.key: testVal}) + tsv := ts.NewView(state.Keys{tt.key: tt.permission}, ImmutableScopeStorage(map[string][]byte{tt.key: testVal})) err := tsv.Remove(ctx, []byte(tt.key)) require.ErrorIs(err, tt.expectedErr) }) @@ -808,7 +808,7 @@ func TestUpdatingKeyPermission(t *testing.T) { ts := New(10) keys := state.Keys{tt.key: tt.permission1} - tsv := ts.NewView(keys, map[string][]byte{tt.key: testVal}) + tsv := ts.NewView(keys, ImmutableScopeStorage(map[string][]byte{tt.key: testVal})) // Check permissions perm := keys[tt.key] @@ -900,9 +900,9 @@ func TestInsertAllocate(t *testing.T) { keys := state.Keys{tt.key: tt.permission} var tsv *TStateView if tt.keyExists { - tsv = ts.NewView(keys, map[string][]byte{tt.key: testVal}) + tsv = ts.NewView(keys, ImmutableScopeStorage(map[string][]byte{tt.key: testVal})) } else { - tsv = ts.NewView(keys, map[string][]byte{}) + tsv = ts.NewView(keys, ImmutableScopeStorage(map[string][]byte{})) } // Try to update key diff --git a/state/tstate/tstate_view.go b/state/tstate/tstate_view.go index 06c4dd77aa..3861a74a3b 100644 --- a/state/tstate/tstate_view.go +++ b/state/tstate/tstate_view.go @@ -32,11 +32,11 @@ type op struct { pastWrites *uint16 } -// immutableScopeStorage implements [state.Immutable], allowing the map to be used +// ImmutableScopeStorage implements [state.Immutable], allowing the map to be used // as a read-only state source, and making it compatible with error-generating backing storage. -type immutableScopeStorage map[string][]byte +type ImmutableScopeStorage map[string][]byte -func (i immutableScopeStorage) GetValue(_ context.Context, key []byte) (value []byte, err error) { +func (i ImmutableScopeStorage) GetValue(_ context.Context, key []byte) (value []byte, err error) { if v, has := i[string(key)]; has { return v, nil } @@ -68,8 +68,8 @@ type TStateView struct { simulationMode bool } -func (ts *TState) NewView(scope state.Keys, storage map[string][]byte) *TStateView { - return newView(ts, scope, immutableScopeStorage(storage), false) +func (ts *TState) NewView(scope state.Keys, storage state.Immutable) *TStateView { + return newView(ts, scope, storage, false) } func (*TStateRecorder) newRecorderView(immutableState state.Immutable) *TStateView { diff --git a/vm/defaultvm/vm.go b/vm/defaultvm/vm.go index 598f188775..d853fcb30c 100644 --- a/vm/defaultvm/vm.go +++ b/vm/defaultvm/vm.go @@ -43,7 +43,8 @@ func New( authEngine map[uint8]vm.AuthEngine, options ...vm.Option, ) (*vm.VM, error) { - options = append(options, NewDefaultOptions()...) + // User-supplied options take precedence over default options + options = append(NewDefaultOptions(), options...) return vm.New( v, genesisFactory, diff --git a/vm/option.go b/vm/option.go index 24e7f91e2b..f4b8ed3167 100644 --- a/vm/option.go +++ b/vm/option.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/hypersdk/api" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/event" + "github.com/ava-labs/hypersdk/state/shim" ) type Options struct { @@ -17,6 +18,8 @@ type Options struct { gossiper bool blockSubscriptionFactories []event.SubscriptionFactory[*chain.ExecutedBlock] vmAPIHandlerFactories []api.HandlerFactory[api.VM] + executionShim shim.Execution + chainOptions []chain.Option } type optionFunc func(vm api.VM, configBytes []byte) (Opt, error) @@ -65,6 +68,12 @@ func WithGossiper() Opt { }) } +func WithExecutionShim(shim shim.Execution) Opt { + return newFuncOption(func(o *Options) { + o.executionShim = shim + }) +} + func WithManual() Option { return NewOption[struct{}]( "manual", @@ -90,6 +99,12 @@ func WithVMAPIs(apiHandlerFactories ...api.HandlerFactory[api.VM]) Opt { }) } +func WithChainOptions(chainOptions ...chain.Option) Opt { + return newFuncOption(func(o *Options) { + o.chainOptions = append(o.chainOptions, chainOptions...) + }) +} + type Opt interface { apply(*Options) } diff --git a/vm/read_state.go b/vm/read_state.go new file mode 100644 index 0000000000..ccdd4b5055 --- /dev/null +++ b/vm/read_state.go @@ -0,0 +1,45 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import ( + "context" + "errors" + + "github.com/ava-labs/hypersdk/state" +) + +var ( + _ state.Immutable = (*RState)(nil) + + errKeyNotFetched = errors.New("key not fetched") +) + +type RStateValue struct { + value []byte + err error +} + +type RState struct { + mp map[string]RStateValue +} + +func (r *RState) GetValue(_ context.Context, key []byte) (value []byte, err error) { + v, ok := r.mp[string(key)] + if !ok { + return nil, errKeyNotFetched + } + return v.value, v.err +} + +func NewRState(keys [][]byte, values [][]byte, errs []error) *RState { + mp := make(map[string]RStateValue, len(keys)) + for i, key := range keys { + mp[string(key)] = RStateValue{ + value: values[i], + err: errs[i], + } + } + return &RState{mp: mp} +} diff --git a/vm/vm.go b/vm/vm.go index 8a8dfb54b9..e62adc60d8 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -43,6 +43,7 @@ import ( "github.com/ava-labs/hypersdk/internal/validitywindow" "github.com/ava-labs/hypersdk/internal/workers" "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" "github.com/ava-labs/hypersdk/statesync" "github.com/ava-labs/hypersdk/storage" "github.com/ava-labs/hypersdk/utils" @@ -99,6 +100,7 @@ type VM struct { rawStateDB database.Database stateDB merkledb.MerkleDB vmDB database.Database + executionShim shim.Execution handlers map[string]http.Handler balanceHandler chain.BalanceHandler metadataManager chain.MetadataManager @@ -372,6 +374,7 @@ func (vm *VM) Initialize( vm, vm.chainTimeValidityWindow, vm.config.ChainConfig, + options.chainOptions, ) if err != nil { return err @@ -424,7 +427,7 @@ func (vm *VM) Initialize( snowCtx.Log.Info("initialized vm from last accepted", zap.Stringer("block", blk.ID())) } else { sps := state.NewSimpleMutable(vm.stateDB) - if err := vm.genesis.InitializeState(ctx, vm.tracer, sps, vm.balanceHandler); err != nil { + if err := vm.genesis.InitializeState(ctx, vm.tracer, vm.executionShim.MutableView(sps, 0), vm.balanceHandler); err != nil { snowCtx.Log.Error("could not set genesis state", zap.Error(err)) return err } @@ -561,6 +564,13 @@ func (vm *VM) Initialize( func (vm *VM) applyOptions(o *Options) error { vm.asyncAcceptedSubscriptionFactories = o.blockSubscriptionFactories vm.vmAPIHandlerFactories = o.vmAPIHandlerFactories + + if o.executionShim != nil { + vm.executionShim = o.executionShim + } else { + vm.executionShim = &shim.NoOp{} + } + if o.builder { vm.builder = builder.NewManual(vm.toEngine, vm.snowCtx.Log) } else { @@ -676,12 +686,13 @@ func (vm *VM) IsReady() bool { } } -func (vm *VM) ReadState(ctx context.Context, keys [][]byte) ([][]byte, []error) { +func (vm *VM) ReadState(ctx context.Context, keys [][]byte) (state.Immutable, error) { if !vm.IsReady() { - return utils.Repeat[[]byte](nil, len(keys)), utils.Repeat(ErrNotReady, len(keys)) + return nil, ErrNotReady } // Atomic read to ensure consistency - return vm.stateDB.GetValues(ctx, keys) + values, errs := vm.stateDB.GetValues(ctx, keys) + return vm.executionShim.ImmutableView(NewRState(keys, values, errs)), nil } func (vm *VM) SetState(ctx context.Context, state snow.State) error { From 25a971a1e7cca76c9556748240da094c6be72841 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Wed, 15 Jan 2025 09:39:59 -0500 Subject: [PATCH 2/3] nit --- chain/builder.go | 2 +- chain/processor.go | 4 +- state/shim/no_op.go | 2 +- state/tstate/recorder_test.go | 156 ---------------------------------- 4 files changed, 4 insertions(+), 160 deletions(-) delete mode 100644 state/tstate/recorder_test.go diff --git a/chain/builder.go b/chain/builder.go index 84fbefabae..872b52014a 100644 --- a/chain/builder.go +++ b/chain/builder.go @@ -301,7 +301,7 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent // Execute block tsv := ts.NewView( stateKeys, - state.ImmutableStorage(storage), + state, len(stateKeys), ) if err := tx.PreExecute(ctx, feeManager, c.balanceHandler, r, tsv, nextTime); err != nil { diff --git a/chain/processor.go b/chain/processor.go index 607d302068..b54dd7f7ef 100644 --- a/chain/processor.go +++ b/chain/processor.go @@ -360,7 +360,7 @@ func (p *Processor) executeTxs( } tm := p.transactionManagerFactory() - state, err := tm.ExecutableState(tstate.ImmutableScopeStorage(storage), b.Height()) + state, err := tm.ExecutableState(state.ImmutableStorage(storage), b.Height()) if err != nil { return err } @@ -371,7 +371,7 @@ func (p *Processor) executeTxs( // processed tsv := ts.NewView( stateKeys, - state.ImmutableStorage(storage), + state, len(stateKeys), ) diff --git a/state/shim/no_op.go b/state/shim/no_op.go index 402ea8c0ab..c1f0c1c075 100644 --- a/state/shim/no_op.go +++ b/state/shim/no_op.go @@ -26,5 +26,5 @@ func (*NoOp) ExecutableState( mp map[string][]byte, _ uint64, ) (state.Immutable, error) { - return tstate.ImmutableScopeStorage(mp), nil + return state.ImmutableStorage(mp), nil } diff --git a/state/tstate/recorder_test.go b/state/tstate/recorder_test.go deleted file mode 100644 index db17a2850e..0000000000 --- a/state/tstate/recorder_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. -package tstate - -import ( - "context" - "crypto/sha256" - "math/rand" - "testing" - - "github.com/ava-labs/avalanchego/database" - "github.com/stretchr/testify/require" -) - -const ( - fuzzerOpInsertExistingKey = iota - fuzzerOpInsertNonExistingKey - fuzzerOpRemoveExistingKey - fuzzerOpRemoveNonExistingKey - fuzzerOpGetExistingKey - fuzzerOpGetNonExistingKey - fuzzerOpCount -) - -func FuzzRecorderPermissionValidator(f *testing.F) { - for i := 0; i < 100; i++ { - f.Add(byte(i), int64(i)) - } - - f.Fuzz( - RecorderPermissionValidatorFuzzer, - ) -} - -func createKeys(numKeys int, keySize int, rand *rand.Rand) [][]byte { - keys := make([][]byte, numKeys) - for i := 0; i < numKeys; i++ { - randKey := make([]byte, keySize) - _, err := rand.Read(randKey) - if err != nil { - panic(err) - } - keys[i] = randKey - } - return keys -} - -func createKeysValues(keys [][]byte) map[string][]byte { - out := map[string][]byte{} - for i, key := range keys { - shaBytes := sha256.Sum256(key) - out[string(keys[i])] = shaBytes[:] - } - return out -} - -func (i ImmutableScopeStorage) duplicate() ImmutableScopeStorage { - other := make(map[string][]byte, len(i)) - for k, v := range i { - other[k] = v - } - return other -} - -// removeSliceElement removes the idx-th element from a slice without maintaining the original -// slice order. removeSliceElement modifies the backing array of the provided slice. -func removeSliceElement[K any](slice []K, idx int) []K { - if idx >= len(slice) { - return slice - } - // swap the element at index [idx] with the last element. - slice[idx], slice[len(slice)-1] = slice[len(slice)-1], slice[idx] - // resize the slice and return it. - return slice[:len(slice)-1] -} - -func RecorderPermissionValidatorFuzzer(t *testing.T, operationCount byte, source int64) { - require := require.New(t) - r := rand.New(rand.NewSource(source)) //nolint:gosec - // create a set of keys which would be used for testing. - // half of these keys would "exists", where the other won't. - existingKeys := createKeys(512, 32, r) - nonExistingKeys := createKeys(512, 32, r) - existingKeyValue := createKeysValues(existingKeys) - - // create a long living recorder. - recorder := NewRecorder(ImmutableScopeStorage(existingKeyValue).duplicate()) - - for opIdx := byte(0); opIdx < operationCount; opIdx++ { - opType := r.Intn(fuzzerOpCount) - switch opType { - case fuzzerOpInsertExistingKey: // insert existing key - key := existingKeys[r.Intn(len(existingKeys))] - require.NoError(recorder.Insert(context.Background(), key, []byte{1, 2, 3})) - - // validate operation agaist TStateView - stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Insert(context.Background(), key, []byte{1, 2, 3})) - - existingKeyValue[string(key)] = []byte{1, 2, 3} - case fuzzerOpInsertNonExistingKey: // insert non existing key - keyIdx := r.Intn(len(nonExistingKeys)) - key := nonExistingKeys[keyIdx] - nonExistingKeys = removeSliceElement(nonExistingKeys, keyIdx) - - require.NoError(recorder.Insert(context.Background(), key, []byte{1, 2, 3, 4})) - - // validate operation agaist TStateView - stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Insert(context.Background(), key, []byte{1, 2, 3, 4})) - - // since we've modified the recorder state, we need to update our own. - existingKeys = append(existingKeys, key) - existingKeyValue[string(key)] = []byte{1, 2, 3, 4} - case fuzzerOpRemoveExistingKey: // remove existing key - keyIdx := r.Intn(len(existingKeys)) - require.NoError(recorder.Remove(context.Background(), existingKeys[keyIdx])) - - // validate operation agaist TStateView - stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Remove(context.Background(), existingKeys[keyIdx])) - - // since we've modified the recorder state, we need to update our own. - delete(existingKeyValue, string(existingKeys[keyIdx])) - existingKeys = append(existingKeys[:keyIdx], existingKeys[keyIdx+1:]...) - case fuzzerOpRemoveNonExistingKey: // remove a non existing key - keyIdx := r.Intn(len(nonExistingKeys)) - require.NoError(recorder.Remove(context.Background(), nonExistingKeys[keyIdx])) - - // validate operation agaist TStateView - stateKeys := recorder.GetStateKeys() - require.NoError(New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue).duplicate()).Remove(context.Background(), nonExistingKeys[keyIdx])) - case fuzzerOpGetExistingKey: // get value of existing key - keyIdx := r.Intn(len(existingKeys)) - recorderValue, err := recorder.GetValue(context.Background(), existingKeys[keyIdx]) - require.NoError(err) - - // validate operation agaist TStateView - stateKeys := recorder.GetStateKeys() - stateValue, err := New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue)).GetValue(context.Background(), existingKeys[keyIdx]) - require.NoError(err) - - // both the recorder and the stateview should return the same value. - require.Equal(recorderValue, stateValue) - case fuzzerOpGetNonExistingKey: // get value of non existing key - keyIdx := r.Intn(len(nonExistingKeys)) - val, err := recorder.GetValue(context.Background(), nonExistingKeys[keyIdx]) - require.ErrorIs(err, database.ErrNotFound, "element was found with a value of %v, while it was supposed to be missing", val) - - // validate operation agaist TStateView - stateKeys := recorder.GetStateKeys() - _, err = New(0).NewView(stateKeys, ImmutableScopeStorage(existingKeyValue)).GetValue(context.Background(), nonExistingKeys[keyIdx]) - require.ErrorIs(err, database.ErrNotFound) - } - } -} From c7bf73866114d7c1b50d8f1ea9883bab15419007 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Wed, 15 Jan 2025 14:36:51 -0500 Subject: [PATCH 3/3] changes --- chain/builder.go | 69 ++++---- chain/chain.go | 24 ++- chain/default.go | 39 ++--- chain/dependencies.go | 34 ++-- chain/option.go | 53 +++++- chain/pre_executor.go | 34 ++-- chain/processor.go | 95 ++++++---- chain/result.go | 5 + extension/tieredstorage/components.go | 162 ++++++++++++++++++ extension/tieredstorage/option.go | 22 +-- .../tieredstorage/transaction_manager.go | 155 ----------------- .../tieredstorage/transaction_manager_test.go | 102 ----------- state/shim/no_op.go | 24 +-- state/shim/shim.go | 15 +- state/tstate/tstate.go | 7 - vm/vm.go | 8 +- 16 files changed, 405 insertions(+), 443 deletions(-) create mode 100644 extension/tieredstorage/components.go delete mode 100644 extension/tieredstorage/transaction_manager.go delete mode 100644 extension/tieredstorage/transaction_manager_test.go diff --git a/chain/builder.go b/chain/builder.go index 872b52014a..43776e1ca1 100644 --- a/chain/builder.go +++ b/chain/builder.go @@ -23,6 +23,7 @@ import ( "github.com/ava-labs/hypersdk/internal/fees" "github.com/ava-labs/hypersdk/keys" "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" "github.com/ava-labs/hypersdk/state/tstate" ) @@ -61,16 +62,19 @@ func HandlePreExecute(log logging.Logger, err error) bool { } type Builder struct { - tracer trace.Tracer - ruleFactory RuleFactory - log logging.Logger - metadataManager MetadataManager - transactionManagerFactory TransactionManagerFactory - balanceHandler BalanceHandler - mempool Mempool - validityWindow ValidityWindow - metrics *chainMetrics - config Config + tracer trace.Tracer + ruleFactory RuleFactory + log logging.Logger + metadataManager MetadataManager + balanceHandler BalanceHandler + mempool Mempool + validityWindow ValidityWindow + metrics *chainMetrics + executionShim shim.Execution + exportStateDiff ExportStateDiffFunc + resultModifierFunc ResultModifierFunc + refundFunc RefundFunc + config Config } func NewBuilder( @@ -78,24 +82,30 @@ func NewBuilder( ruleFactory RuleFactory, log logging.Logger, metadataManager MetadataManager, - transactionManagerFactory TransactionManagerFactory, balanceHandler BalanceHandler, mempool Mempool, validityWindow ValidityWindow, metrics *chainMetrics, + executionShim shim.Execution, + afterBlock ExportStateDiffFunc, + resultModifierFunc ResultModifierFunc, + refundFunc RefundFunc, config Config, ) *Builder { return &Builder{ - tracer: tracer, - ruleFactory: ruleFactory, - log: log, - metadataManager: metadataManager, - transactionManagerFactory: transactionManagerFactory, - balanceHandler: balanceHandler, - mempool: mempool, - validityWindow: validityWindow, - metrics: metrics, - config: config, + tracer: tracer, + ruleFactory: ruleFactory, + log: log, + metadataManager: metadataManager, + balanceHandler: balanceHandler, + mempool: mempool, + validityWindow: validityWindow, + metrics: metrics, + executionShim: executionShim, + exportStateDiff: afterBlock, + resultModifierFunc: resultModifierFunc, + refundFunc: refundFunc, + config: config, } } @@ -292,8 +302,7 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent }() } - tm := c.transactionManagerFactory() - state, err := tm.ExecutableState(storage, height) + state, err := c.executionShim.ImmutableView(ctx, stateKeys, state.ImmutableStorage(storage), height) if err != nil { return err } @@ -331,7 +340,12 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent blockLock.Lock() defer blockLock.Unlock() - if err := tm.AfterTX(ctx, tx, result, tsv, c.balanceHandler, feeManager, true); err != nil { + resultChanges, err := c.resultModifierFunc(state, result, feeManager) + if err != nil { + return err + } + + if err := c.refundFunc(ctx, resultChanges, c.balanceHandler, tx.Auth.Sponsor(), tsv); err != nil { return err } @@ -414,11 +428,6 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent c.metrics.emptyBlockBuilt.Inc() } - tm := c.transactionManagerFactory() - if err := tm.RawState(ts, height); err != nil { - return nil, nil, nil, err - } - // Update chain metadata heightKey := HeightKey(c.metadataManager.HeightPrefix()) heightKeyStr := string(heightKey) @@ -458,7 +467,7 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent } // Get view from [tstate] after writing all changed keys - view, err := ts.ExportMerkleDBView(ctx, c.tracer, parentView) + view, err := c.exportStateDiff(ctx, ts, parentView, c.metadataManager, height) if err != nil { return nil, nil, nil, err } diff --git a/chain/chain.go b/chain/chain.go index d5ba914b7b..bb416493f0 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -43,7 +43,7 @@ func NewChain( return nil, err } - options := &Options{} + options := NewDefaultOptions() applyOptions(options, ops) return &Chain{ @@ -52,11 +52,14 @@ func NewChain( ruleFactory, logger, metadataManager, - options.TransactionManagerFactory, balanceHandler, mempool, validityWindow, metrics, + options.executionShim, + options.exportStateDiffFunc, + options.resultModifierFunc, + options.refundFunc, config, ), processor: NewProcessor( @@ -66,10 +69,14 @@ func NewChain( authVerifiers, authVM, metadataManager, - options.TransactionManagerFactory, balanceHandler, validityWindow, metrics, + options.executionShim, + options.exportStateDiffFunc, + options.dimsModifierFunc, + options.resultModifierFunc, + options.refundFunc, config, ), accepter: NewAccepter( @@ -81,7 +88,7 @@ func NewChain( ruleFactory, validityWindow, metadataManager, - options.TransactionManagerFactory, + options.executionShim, balanceHandler, ), blockParser: NewBlockParser(tracer, parser), @@ -124,12 +131,3 @@ func (c *Chain) PreExecute( func (c *Chain) ParseBlock(ctx context.Context, bytes []byte) (*ExecutionBlock, error) { return c.blockParser.ParseBlock(ctx, bytes) } - -func applyOptions(options *Options, ops []Option) { - for _, op := range ops { - op(options) - } - if options.TransactionManagerFactory == nil { - options.TransactionManagerFactory = NewDefaultTransactionManager - } -} diff --git a/chain/default.go b/chain/default.go index 3cea48a7fa..af44c9e175 100644 --- a/chain/default.go +++ b/chain/default.go @@ -6,40 +6,29 @@ package chain import ( "context" + "github.com/ava-labs/avalanchego/x/merkledb" + + "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/state" - "github.com/ava-labs/hypersdk/state/shim" + "github.com/ava-labs/hypersdk/state/tstate" internalfees "github.com/ava-labs/hypersdk/internal/fees" ) -var ( - _ Hooks = (*DefaultTransactionHooks)(nil) - _ TransactionManager = (*DefaultTransactionManager)(nil) -) - -type DefaultTransactionHooks struct{} - -func (*DefaultTransactionHooks) AfterTX( - _ context.Context, - _ *Transaction, - _ *Result, - _ state.Mutable, - _ BalanceHandler, - _ *internalfees.Manager, - _ bool, -) error { - return nil +func DefaultExportStateDiff(ctx context.Context, ts *tstate.TState, view state.View, _ MetadataManager, _ uint64) (merkledb.View, error) { + return view.NewView(ctx, merkledb.ViewChanges{MapOps: ts.ChangedKeys(), ConsumeBytes: true}) } -type DefaultTransactionManager struct { - shim.NoOp - DefaultTransactionHooks +// This modifies the result passed in +// Futhermore, any changes that were made in Result should be documented in ResultChanges +func DefaultResultModifier(state.Immutable, *Result, *internalfees.Manager) (*ResultChanges, error) { + return nil, nil } -func NewDefaultTransactionManager() TransactionManager { - return &DefaultTransactionManager{} +func DefaultRefundFunc(context.Context, *ResultChanges, BalanceHandler, codec.Address, state.Mutable) error { + return nil } -func DefaultTransactionManagerFactory() TransactionManagerFactory { - return NewDefaultTransactionManager +func FeeManagerModifier(*internalfees.Manager, *ResultChanges) error { + return nil } diff --git a/chain/dependencies.go b/chain/dependencies.go index 4cc753e10b..4894b8f163 100644 --- a/chain/dependencies.go +++ b/chain/dependencies.go @@ -8,12 +8,13 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/x/merkledb" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/fees" "github.com/ava-labs/hypersdk/internal/validitywindow" "github.com/ava-labs/hypersdk/state" - "github.com/ava-labs/hypersdk/state/shim" + "github.com/ava-labs/hypersdk/state/tstate" internalfees "github.com/ava-labs/hypersdk/internal/fees" ) @@ -225,25 +226,16 @@ type ValidityWindow interface { ) (set.Bits, error) } -// Hooks can be used to change the fees and compute units of a -// transaction -// This is called after transaction execution, but before the end of block execution -type Hooks interface { - // AfterTX is called post-transaction execution - AfterTX( - ctx context.Context, - tx *Transaction, - result *Result, - mu state.Mutable, - bh BalanceHandler, - fm *internalfees.Manager, - isBuilder bool, - ) error -} +// ExportStateDiffFunc finalizes the state diff +type ExportStateDiffFunc func(context.Context, *tstate.TState, state.View, MetadataManager, uint64) (merkledb.View, error) -type TransactionManager interface { - shim.Execution - Hooks -} +// ResultModifierFunc modifies the result of a transaction +// Any changes should be reflected in ResultChanges +type ResultModifierFunc func(state.Immutable, *Result, *internalfees.Manager) (*ResultChanges, error) + +// RefundFunc refunds any fees to the transaction sponsor +type RefundFunc func(context.Context, *ResultChanges, BalanceHandler, codec.Address, state.Mutable) error -type TransactionManagerFactory func() TransactionManager +// FeeManagerModifierFunc can be used to modify the fee manager +// Any changes to a result's units should be reflected here +type FeeManagerModifierFunc func(*internalfees.Manager, *ResultChanges) error diff --git a/chain/option.go b/chain/option.go index 216c1f1521..61220c74e6 100644 --- a/chain/option.go +++ b/chain/option.go @@ -3,14 +3,61 @@ package chain +import "github.com/ava-labs/hypersdk/state/shim" + type Options struct { - TransactionManagerFactory TransactionManagerFactory + executionShim shim.Execution + // exportStateDiffFunc allows users to override the state diff at the end of block execution + exportStateDiffFunc ExportStateDiffFunc + refundFunc RefundFunc + dimsModifierFunc FeeManagerModifierFunc + resultModifierFunc ResultModifierFunc } type Option func(*Options) -func WithTransactionManagerFactory(factory TransactionManagerFactory) Option { +func NewDefaultOptions() *Options { + return &Options{ + executionShim: &shim.ExecutionNoOp{}, + exportStateDiffFunc: DefaultExportStateDiff, + refundFunc: DefaultRefundFunc, + dimsModifierFunc: FeeManagerModifier, + resultModifierFunc: DefaultResultModifier, + } +} + +func WithExecutionShim(shim shim.Execution) Option { + return func(opts *Options) { + opts.executionShim = shim + } +} + +func WithExportStateDiffFunc(exportStateDiff ExportStateDiffFunc) Option { + return func(opts *Options) { + opts.exportStateDiffFunc = exportStateDiff + } +} + +func WithRefundFunc(refund RefundFunc) Option { return func(opts *Options) { - opts.TransactionManagerFactory = factory + opts.refundFunc = refund + } +} + +func WithDimsModifierFunc(dimsModifier FeeManagerModifierFunc) Option { + return func(opts *Options) { + opts.dimsModifierFunc = dimsModifier + } +} + +func WithResultModifierFunc(resultModifier ResultModifierFunc) Option { + return func(opts *Options) { + opts.resultModifierFunc = resultModifier + } +} + +func applyOptions(option *Options, opts []Option) { + for _, o := range opts { + o(option) } } diff --git a/chain/pre_executor.go b/chain/pre_executor.go index 9e094cc59a..e223ba30f9 100644 --- a/chain/pre_executor.go +++ b/chain/pre_executor.go @@ -8,31 +8,32 @@ import ( "time" "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" internalfees "github.com/ava-labs/hypersdk/internal/fees" ) type PreExecutor struct { - ruleFactory RuleFactory - validityWindow ValidityWindow - metadataManager MetadataManager - transactionManagerFactory TransactionManagerFactory - balanceHandler BalanceHandler + ruleFactory RuleFactory + validityWindow ValidityWindow + metadataManager MetadataManager + executionShim shim.Execution + balanceHandler BalanceHandler } func NewPreExecutor( ruleFactory RuleFactory, validityWindow ValidityWindow, metadataManager MetadataManager, - transactionManagerFactory TransactionManagerFactory, + executionShim shim.Execution, balanceHandler BalanceHandler, ) *PreExecutor { return &PreExecutor{ - ruleFactory: ruleFactory, - validityWindow: validityWindow, - metadataManager: metadataManager, - transactionManagerFactory: transactionManagerFactory, - balanceHandler: balanceHandler, + ruleFactory: ruleFactory, + validityWindow: validityWindow, + metadataManager: metadataManager, + executionShim: executionShim, + balanceHandler: balanceHandler, } } @@ -69,7 +70,7 @@ func (p *PreExecutor) PreExecute( } // Ensure state keys are valid - _, err = tx.StateKeys(p.balanceHandler) + stateKeys, err := tx.StateKeys(p.balanceHandler) if err != nil { return err } @@ -81,8 +82,6 @@ func (p *PreExecutor) PreExecute( } } - tm := p.transactionManagerFactory() - // PreExecute does not make any changes to state // // This may fail if the state we are utilizing is invalidated (if a trie @@ -92,7 +91,12 @@ func (p *PreExecutor) PreExecute( // Note, [PreExecute] ensures that the pending transaction does not have // an expiry time further ahead than [ValidityWindow]. This ensures anything // added to the [Mempool] is immediately executable. - if err := tx.PreExecute(ctx, nextFeeManager, p.balanceHandler, r, tm.ImmutableView(view), now); err != nil { + executionView, err := p.executionShim.ImmutableView(ctx, stateKeys, view, parentBlk.Height()+1) + if err != nil { + return err + } + + if err := tx.PreExecute(ctx, nextFeeManager, p.balanceHandler, r, executionView, now); err != nil { return err } return nil diff --git a/chain/processor.go b/chain/processor.go index b54dd7f7ef..e0982e8bee 100644 --- a/chain/processor.go +++ b/chain/processor.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/hypersdk/internal/fetcher" "github.com/ava-labs/hypersdk/internal/workers" "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" "github.com/ava-labs/hypersdk/state/tstate" ) @@ -74,17 +75,21 @@ func (b *ExecutionBlock) Txs() []*Transaction { } type Processor struct { - tracer trace.Tracer - log logging.Logger - ruleFactory RuleFactory - authVerificationWorkers workers.Workers - authVM AuthVM - metadataManager MetadataManager - transactionManagerFactory TransactionManagerFactory - balanceHandler BalanceHandler - validityWindow ValidityWindow - metrics *chainMetrics - config Config + tracer trace.Tracer + log logging.Logger + ruleFactory RuleFactory + authVerificationWorkers workers.Workers + authVM AuthVM + metadataManager MetadataManager + balanceHandler BalanceHandler + validityWindow ValidityWindow + metrics *chainMetrics + executionShim shim.Execution + exportStateDiff ExportStateDiffFunc + dimsModifierFunc FeeManagerModifierFunc + resultModifierFunc ResultModifierFunc + refundFunc RefundFunc + config Config } func NewProcessor( @@ -94,24 +99,32 @@ func NewProcessor( authVerificationWorkers workers.Workers, authVM AuthVM, metadataManager MetadataManager, - transactionManagerFactory TransactionManagerFactory, balanceHandler BalanceHandler, validityWindow ValidityWindow, metrics *chainMetrics, + executionShim shim.Execution, + afterBlock ExportStateDiffFunc, + dimsModifierFunc FeeManagerModifierFunc, + resultModifierFunc ResultModifierFunc, + refundFunc RefundFunc, config Config, ) *Processor { return &Processor{ - tracer: tracer, - log: log, - ruleFactory: ruleFactory, - authVerificationWorkers: authVerificationWorkers, - authVM: authVM, - metadataManager: metadataManager, - transactionManagerFactory: transactionManagerFactory, - balanceHandler: balanceHandler, - validityWindow: validityWindow, - metrics: metrics, - config: config, + tracer: tracer, + log: log, + ruleFactory: ruleFactory, + authVerificationWorkers: authVerificationWorkers, + authVM: authVM, + metadataManager: metadataManager, + balanceHandler: balanceHandler, + validityWindow: validityWindow, + metrics: metrics, + executionShim: executionShim, + exportStateDiff: afterBlock, + dimsModifierFunc: dimsModifierFunc, + resultModifierFunc: resultModifierFunc, + refundFunc: refundFunc, + config: config, } } @@ -195,11 +208,6 @@ func (p *Processor) Execute( return nil, nil, err } - tm := p.transactionManagerFactory() - if err := tm.RawState(ts, b.Height()); err != nil { - return nil, nil, err - } - // Update chain metadata heightKeyStr := string(heightKey) timestampKeyStr := string(timestampKey) @@ -265,7 +273,7 @@ func (p *Processor) Execute( // Get view from [tstate] after processing all state transitions p.metrics.stateChanges.Add(float64(ts.PendingChanges())) p.metrics.stateOperations.Add(float64(ts.OpIndex())) - view, err := ts.ExportMerkleDBView(ctx, p.tracer, parentView) + view, err := p.exportStateDiff(ctx, ts, parentView, p.metadataManager, b.Height()) if err != nil { return nil, nil, err } @@ -359,12 +367,25 @@ func (p *Processor) executeTxs( return err } - tm := p.transactionManagerFactory() - state, err := tm.ExecutableState(state.ImmutableStorage(storage), b.Height()) + state, err := p.executionShim.ImmutableView( + ctx, + stateKeys, + state.ImmutableStorage(storage), + b.Height(), + ) if err != nil { return err } + // Ideally, this function converts storage into a state.Immutable + // type + // However, what we can do is return a struct which impls + // state.Immutable AND also keep track of the hot keys + // This way, we can then pass in executionState into another + // function (similar to afterTX), typecast to T, and then get the + // hot keys from there + // executionState := executionStateFunc(storage) + // Execute transaction // // It is critical we explicitly set the scope before each transaction is @@ -385,7 +406,17 @@ func (p *Processor) executeTxs( return err } - if err := tm.AfterTX(ctx, tx, result, tsv, p.balanceHandler, feeManager, false); err != nil { + resultChanges, err := p.resultModifierFunc(state, result, feeManager) + if err != nil { + return err + } + + if err := p.refundFunc(ctx, resultChanges, p.balanceHandler, tx.Auth.Sponsor(), tsv); err != nil { + return err + } + + // This refunds the dims from the feeManager (only called in the Processor) + if err := p.dimsModifierFunc(feeManager, resultChanges); err != nil { return err } diff --git a/chain/result.go b/chain/result.go index 1295e69ee0..26a69aa0e3 100644 --- a/chain/result.go +++ b/chain/result.go @@ -138,3 +138,8 @@ func UnmarshalResults(src []byte) ([]*Result, error) { } return results, nil } + +type ResultChanges struct { + DimsDiff fees.Dimensions + FeeDiff uint64 +} diff --git a/extension/tieredstorage/components.go b/extension/tieredstorage/components.go new file mode 100644 index 0000000000..16d05afd97 --- /dev/null +++ b/extension/tieredstorage/components.go @@ -0,0 +1,162 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tieredstorage + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/utils/maybe" + "github.com/ava-labs/avalanchego/x/merkledb" + + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/consts" + "github.com/ava-labs/hypersdk/fees" + "github.com/ava-labs/hypersdk/internal/math" + "github.com/ava-labs/hypersdk/keys" + "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" + "github.com/ava-labs/hypersdk/state/tstate" + + safemath "github.com/ava-labs/avalanchego/utils/math" + internalfees "github.com/ava-labs/hypersdk/internal/fees" +) + +var ( + _ shim.Execution = (*Shim)(nil) + _ state.Immutable = (*innerShim)(nil) +) + +type Shim struct { + config Config +} + +func (s *Shim) ImmutableView(ctx context.Context, stateKeys state.Keys, im state.Immutable, blockHeight uint64) (state.Immutable, error) { + unsuffixedStorage := make(map[string][]byte) + hotKeys := make(map[string]uint16) + + for k := range stateKeys { + v, err := im.GetValue(ctx, []byte(k)) + if err != nil && err != database.ErrNotFound { + return nil, err + } else if err == database.ErrNotFound { + continue + } + + if len(v) < consts.Uint64Len { + return nil, errValueTooShortForSuffix + } + + lastTouched := binary.BigEndian.Uint64(v[len(v)-consts.Uint64Len:]) + memoryThreshold, err := safemath.Sub(blockHeight, s.config.Epsilon) + if lastTouched >= memoryThreshold || err == safemath.ErrUnderflow { + maxChunks, ok := keys.MaxChunks([]byte(k)) + if !ok { + return nil, errFailedToParseMaxChunks + } + hotKeys[k] = maxChunks + } + + unsuffixedStorage[k] = v[:len(v)-consts.Uint64Len] + } + + return &innerShim{ + hotKeys: hotKeys, + Immutable: state.ImmutableStorage(unsuffixedStorage), + config: s.config, + }, nil +} + +func (*Shim) MutableView(mu state.Mutable, blockHeight uint64) state.Mutable { + return state.NewTranslatedMutable(mu, blockHeight) +} + +type innerShim struct { + state.Immutable + hotKeys map[string]uint16 + config Config +} + +func ExportStateDiff(ctx context.Context, ts *tstate.TState, view state.View, m chain.MetadataManager, blockHeight uint64) (merkledb.View, error) { + changedKeys := ts.ChangedKeys() + for k := range changedKeys { + // Metadata should not be suffixed + if isMetadataKey(k, m) { + continue + } + + if changedKeys[k].HasValue() { + changedKeys[k] = maybe.Some( + binary.BigEndian.AppendUint64(changedKeys[k].Value(), blockHeight), + ) + } + } + + return view.NewView(ctx, merkledb.ViewChanges{MapOps: changedKeys, ConsumeBytes: true}) +} + +func ResultModifier(im state.Immutable, result *chain.Result, fm *internalfees.Manager) (*chain.ResultChanges, error) { + innerShim, ok := im.(*innerShim) + if !ok { + return nil, fmt.Errorf("expected innerShim but got %T", im) + } + + // Compute refund dims + readsRefundOp := math.NewUint64Operator(0) + for _, v := range innerShim.hotKeys { + readsRefundOp.Add(innerShim.config.StorageReadKeyRefund) + readsRefundOp.MulAdd(uint64(v), innerShim.config.StorageReadValueRefund) + } + readRefundUnits, err := readsRefundOp.Value() + if err != nil { + return nil, err + } + refundDims := fees.Dimensions{0, 0, readRefundUnits, 0, 0} + + // Compute refund fee + refundFee, err := fm.Fee(refundDims) + if err != nil { + return nil, err + } + + // Modify result + newDims, err := fees.Sub(result.Units, refundDims) + if err != nil { + return nil, err + } + result.Units = newDims + result.Fee -= refundFee + + return &chain.ResultChanges{ + DimsDiff: refundDims, + FeeDiff: refundFee, + }, nil +} + +func Refund(ctx context.Context, resultChanges *chain.ResultChanges, bh chain.BalanceHandler, sponsor codec.Address, mu state.Mutable) error { + return bh.AddBalance(ctx, sponsor, mu, resultChanges.FeeDiff) +} + +func FeeManagerModifier(fm *internalfees.Manager, resultChanges *chain.ResultChanges) error { + ok, _ := fm.Refund(resultChanges.DimsDiff) + if !ok { + return errFailedToRefund + } + return nil +} + +func isMetadataKey(k string, m chain.MetadataManager) bool { + if bytes.Equal([]byte(k), chain.HeightKey(m.HeightPrefix())) { //nolint:gocritic + return true + } else if bytes.Equal([]byte(k), chain.TimestampKey(m.TimestampPrefix())) { + return true + } else if bytes.Equal([]byte(k), chain.FeeKey(m.FeePrefix())) { + return true + } + return false +} diff --git a/extension/tieredstorage/option.go b/extension/tieredstorage/option.go index 956bf4651e..e9fbfd9b0d 100644 --- a/extension/tieredstorage/option.go +++ b/extension/tieredstorage/option.go @@ -9,9 +9,7 @@ import ( "github.com/ava-labs/hypersdk/vm" ) -const Namespace = "tieredStorageTransactionManager" - -var _ chain.TransactionManager = (*TieredStorageTransactionManager)(nil) +const Namespace = "tieredStorage" type Config struct { Epsilon uint64 `json:"epsilon"` @@ -34,20 +32,22 @@ func With() vm.Option { func OptionFunc(_ api.VM, config Config) (vm.Opt, error) { return vm.NewOpt( vm.WithChainOptions( - chain.WithTransactionManagerFactory( - NewTieredStorageTransactionManager(config), + chain.WithExecutionShim( + ExecutionShim(config), ), + chain.WithExportStateDiffFunc(ExportStateDiff), + chain.WithRefundFunc(Refund), + chain.WithDimsModifierFunc(FeeManagerModifier), + chain.WithResultModifierFunc(ResultModifier), ), vm.WithExecutionShim( - NewTieredStorageTransactionManager(config)(), + ExecutionShim(config), ), ), nil } -func NewTieredStorageTransactionManager(config Config) chain.TransactionManagerFactory { - return func() chain.TransactionManager { - return &TieredStorageTransactionManager{ - config: config, - } +func ExecutionShim(config Config) *Shim { + return &Shim{ + config: config, } } diff --git a/extension/tieredstorage/transaction_manager.go b/extension/tieredstorage/transaction_manager.go deleted file mode 100644 index 08c03f2d31..0000000000 --- a/extension/tieredstorage/transaction_manager.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package tieredstorage - -import ( - "context" - "encoding/binary" - - "github.com/ava-labs/avalanchego/utils/maybe" - - "github.com/ava-labs/hypersdk/chain" - "github.com/ava-labs/hypersdk/consts" - "github.com/ava-labs/hypersdk/fees" - "github.com/ava-labs/hypersdk/internal/math" - "github.com/ava-labs/hypersdk/keys" - "github.com/ava-labs/hypersdk/state" - "github.com/ava-labs/hypersdk/state/tstate" - - safemath "github.com/ava-labs/avalanchego/utils/math" - internalfees "github.com/ava-labs/hypersdk/internal/fees" -) - -var _ chain.TransactionManager = (*TieredStorageTransactionManager)(nil) - -type TieredStorageTransactionManager struct { - config Config - hotKeys map[string]uint16 -} - -func newTieredStorageTransactionManager(config Config) *TieredStorageTransactionManager { - return &TieredStorageTransactionManager{ - config: config, - } -} - -// MutableView implements chain.TransactionManager. -func (*TieredStorageTransactionManager) MutableView(mu state.Mutable, blockHeight uint64) state.Mutable { - return state.NewTranslatedMutable(mu, blockHeight) -} - -func (*TieredStorageTransactionManager) ImmutableView(im state.Immutable) state.Immutable { - return state.NewTranslatedImmutable(im) -} - -// AfterTX issues fee refunds and, if the validator is calling, unit refunds -func (t *TieredStorageTransactionManager) AfterTX( - ctx context.Context, - tx *chain.Transaction, - result *chain.Result, - mu state.Mutable, - bh chain.BalanceHandler, - fm *internalfees.Manager, - isBuilder bool, -) error { - // We deduct fees regardless of whether the builder or processor is calling - readsRefundOp := math.NewUint64Operator(0) - for _, v := range t.hotKeys { - readsRefundOp.Add(t.config.StorageReadKeyRefund) - readsRefundOp.MulAdd(uint64(v), t.config.StorageReadValueRefund) - } - readRefundUnits, err := readsRefundOp.Value() - if err != nil { - return err - } - - refundDims := fees.Dimensions{0, 0, readRefundUnits, 0, 0} - refundFee, err := fm.Fee(refundDims) - if err != nil { - return err - } - if err := bh.AddBalance(ctx, tx.Auth.Sponsor(), mu, refundFee); err != nil { - return err - } - - if !isBuilder { - if err := t.refundUnitsConsumed(fm, refundDims); err != nil { - return err - } - } - - newUnits, err := fees.Sub(result.Units, refundDims) - if err != nil { - return err - } - - result.Units = newUnits - result.Fee -= refundFee - - return nil -} - -// ExecutableState implements chain.TransactionManager. -func (t *TieredStorageTransactionManager) ExecutableState( - mp map[string][]byte, - blockHeight uint64, -) (state.Immutable, error) { - unsuffixedStorage := make(map[string][]byte) - hotKeys := make(map[string]uint16) - - for k, v := range mp { - if len(v) < consts.Uint64Len { - return nil, errValueTooShortForSuffix - } - - lastTouched := binary.BigEndian.Uint64(v[len(v)-consts.Uint64Len:]) - - memoryThreshold, err := safemath.Sub(blockHeight, t.config.Epsilon) - if lastTouched >= memoryThreshold || err == safemath.ErrUnderflow { - maxChunks, ok := keys.MaxChunks([]byte(k)) - if !ok { - return nil, errFailedToParseMaxChunks - } - hotKeys[k] = maxChunks - } - - unsuffixedStorage[k] = v[:len(v)-consts.Uint64Len] - } - - t.hotKeys = hotKeys - return tstate.ImmutableScopeStorage(unsuffixedStorage), nil -} - -func (*TieredStorageTransactionManager) RawState(ts *tstate.TState, blockHeight uint64) error { - pendingChangedKeys := ts.ChangedKeys() - for k := range pendingChangedKeys { - if pendingChangedKeys[k].HasValue() { - pendingChangedKeys[k] = maybe.Some( - binary.BigEndian.AppendUint64( - pendingChangedKeys[k].Value(), - blockHeight, - ), - ) - } - } - - ts.SetChangedKeys(pendingChangedKeys) - return nil -} - -func (*TieredStorageTransactionManager) refundUnitsConsumed( - fm *internalfees.Manager, - refundDims fees.Dimensions, -) error { - ok, _ := fm.Refund(refundDims) - if !ok { - return errFailedToRefund - } - return nil -} - -func (t *TieredStorageTransactionManager) IsHotKey(key string) bool { - _, ok := t.hotKeys[key] - return ok -} diff --git a/extension/tieredstorage/transaction_manager_test.go b/extension/tieredstorage/transaction_manager_test.go deleted file mode 100644 index 72f09d8c1d..0000000000 --- a/extension/tieredstorage/transaction_manager_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package tieredstorage - -import ( - "encoding/binary" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/hypersdk/consts" -) - -var ( - testKey = "testKey" - // BLk height is 0 - testVal = binary.BigEndian.AppendUint64([]byte("testVal"), 0) -) - -func TestTransactionManagerHotKeys(t *testing.T) { - tests := []struct { - name string - rawState map[string][]byte - epsilon uint64 - blockHeight uint64 - key string - isHotKey bool - }{ - { - name: "empty state has no hot keys", - rawState: map[string][]byte{}, - }, - { - name: "state with hot key", - rawState: map[string][]byte{testKey: testVal}, - epsilon: 1, - blockHeight: 1, - key: testKey, - isHotKey: true, - }, - { - name: "state with hot key and no underflow", - rawState: map[string][]byte{testKey: testVal}, - epsilon: 2, - blockHeight: 1, - key: testKey, - isHotKey: true, - }, - { - name: "state with cold key", - rawState: map[string][]byte{testKey: testVal}, - epsilon: 2, - blockHeight: 3, - key: testKey, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := require.New(t) - tm := newTieredStorageTransactionManager(Config{Epsilon: tt.epsilon}) - - _, err := tm.ExecutableState(tt.rawState, tt.blockHeight) - r.NoError(err) - - r.Equal(tt.isHotKey, tm.IsHotKey(tt.key)) - }) - } -} - -func TestTransactionManagerShim(t *testing.T) { - tests := []struct { - name string - rawState map[string][]byte - err error - }{ - { - name: "rawDB with prefixes", - rawState: map[string][]byte{ - testKey: testVal, - }, - }, - { - name: "rawDB with no prefixes", - rawState: map[string][]byte{ - testKey: testVal[:len(testVal)-consts.Uint64Len], - }, - err: errValueTooShortForSuffix, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := require.New(t) - tm := newTieredStorageTransactionManager(Config{Epsilon: 1}) - - _, err := tm.ExecutableState(tt.rawState, 1) - r.Equal(tt.err, err) - }) - } -} diff --git a/state/shim/no_op.go b/state/shim/no_op.go index c1f0c1c075..e3fcc11082 100644 --- a/state/shim/no_op.go +++ b/state/shim/no_op.go @@ -4,27 +4,19 @@ package shim import ( + "context" + "github.com/ava-labs/hypersdk/state" - "github.com/ava-labs/hypersdk/state/tstate" ) -type NoOp struct{} - -func (*NoOp) MutableView(mu state.Mutable, _ uint64) state.Mutable { - return mu -} +var _ Execution = (*ExecutionNoOp)(nil) -func (*NoOp) ImmutableView(im state.Immutable) state.Immutable { - return im -} +type ExecutionNoOp struct{} -func (*NoOp) RawState(*tstate.TState, uint64) error { - return nil +func (*ExecutionNoOp) ImmutableView(_ context.Context, _ state.Keys, im state.Immutable, _ uint64) (state.Immutable, error) { + return im, nil } -func (*NoOp) ExecutableState( - mp map[string][]byte, - _ uint64, -) (state.Immutable, error) { - return state.ImmutableStorage(mp), nil +func (*ExecutionNoOp) MutableView(mu state.Mutable, _ uint64) state.Mutable { + return mu } diff --git a/state/shim/shim.go b/state/shim/shim.go index 4a7c58cf43..57f8230668 100644 --- a/state/shim/shim.go +++ b/state/shim/shim.go @@ -4,20 +4,13 @@ package shim import ( + "context" + "github.com/ava-labs/hypersdk/state" - "github.com/ava-labs/hypersdk/state/tstate" ) -// Execution is responsible for translating between raw state and execution state. -// This is useful for cases like managing suffix values type Execution interface { - // ExecutableState converts a kv-store to a state which is suitable for TStateView - ExecutableState(map[string][]byte, uint64) (state.Immutable, error) - // MutableView is used for genesis initialization + ImmutableView(context.Context, state.Keys, state.Immutable, uint64) (state.Immutable, error) + // TODO: we should be able to get of this if we modify the genesis logic MutableView(state.Mutable, uint64) state.Mutable - // ImmutableView is used for VM transaction pre-execution - ImmutableView(state.Immutable) state.Immutable - // ExecutableState converts the state changes of a block into a format - // suitable for DB storage - RawState(*tstate.TState, uint64) error } diff --git a/state/tstate/tstate.go b/state/tstate/tstate.go index 289cddc300..baae0f703d 100644 --- a/state/tstate/tstate.go +++ b/state/tstate/tstate.go @@ -87,10 +87,3 @@ func (ts *TState) ChangedKeys() map[string]maybe.Maybe[[]byte] { return ts.changedKeys } - -func (ts *TState) SetChangedKeys(mp map[string]maybe.Maybe[[]byte]) { - ts.l.Lock() - defer ts.l.Unlock() - - ts.changedKeys = mp -} diff --git a/vm/vm.go b/vm/vm.go index 34aad85ad0..bbe979c4a1 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -568,7 +568,7 @@ func (vm *VM) applyOptions(o *Options) error { if o.executionShim != nil { vm.executionShim = o.executionShim } else { - vm.executionShim = &shim.NoOp{} + vm.executionShim = &shim.ExecutionNoOp{} } if o.builder { @@ -692,7 +692,11 @@ func (vm *VM) ReadState(ctx context.Context, keys [][]byte) (state.Immutable, er } // Atomic read to ensure consistency values, errs := vm.stateDB.GetValues(ctx, keys) - return vm.executionShim.ImmutableView(NewRState(keys, values, errs)), nil + stateKeys := state.Keys{} + for _, key := range keys { + stateKeys.Add(string(key), state.All) + } + return vm.executionShim.ImmutableView(ctx, stateKeys, NewRState(keys, values, errs), 0) } func (vm *VM) SetState(ctx context.Context, state snow.State) error {