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 6281c4ee3f..5dcece99cd 100644 --- a/api/jsonrpc/server.go +++ b/api/jsonrpc/server.go @@ -209,17 +209,17 @@ 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( 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 ddef83314a..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,15 +62,19 @@ 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 + balanceHandler BalanceHandler + mempool Mempool + validityWindow ValidityWindow + metrics *chainMetrics + executionShim shim.Execution + exportStateDiff ExportStateDiffFunc + resultModifierFunc ResultModifierFunc + refundFunc RefundFunc + config Config } func NewBuilder( @@ -81,18 +86,26 @@ func NewBuilder( 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, - 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, } } @@ -289,10 +302,15 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent }() } + state, err := c.executionShim.ImmutableView(ctx, stateKeys, state.ImmutableStorage(storage), height) + if err != nil { + return err + } + // 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 { @@ -322,6 +340,15 @@ func (c *Builder) BuildBlock(ctx context.Context, parentView state.View, parent blockLock.Lock() defer blockLock.Unlock() + 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 + } + // Ensure block isn't too big if ok, dimension := feeManager.Consume(result.Units, maxUnits); !ok { c.log.Debug( @@ -440,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 9c11eee7f8..bb416493f0 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -36,11 +36,16 @@ func NewChain( authVM AuthVM, validityWindow ValidityWindow, config Config, + ops []Option, ) (*Chain, error) { metrics, err := newMetrics(registerer) if err != nil { return nil, err } + + options := NewDefaultOptions() + applyOptions(options, ops) + return &Chain{ builder: NewBuilder( tracer, @@ -51,6 +56,10 @@ func NewChain( mempool, validityWindow, metrics, + options.executionShim, + options.exportStateDiffFunc, + options.resultModifierFunc, + options.refundFunc, config, ), processor: NewProcessor( @@ -63,6 +72,11 @@ func NewChain( balanceHandler, validityWindow, metrics, + options.executionShim, + options.exportStateDiffFunc, + options.dimsModifierFunc, + options.resultModifierFunc, + options.refundFunc, config, ), accepter: NewAccepter( @@ -74,6 +88,7 @@ func NewChain( ruleFactory, validityWindow, metadataManager, + options.executionShim, balanceHandler, ), blockParser: NewBlockParser(tracer, parser), diff --git a/chain/default.go b/chain/default.go new file mode 100644 index 0000000000..af44c9e175 --- /dev/null +++ b/chain/default.go @@ -0,0 +1,34 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +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/tstate" + + internalfees "github.com/ava-labs/hypersdk/internal/fees" +) + +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}) +} + +// 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 DefaultRefundFunc(context.Context, *ResultChanges, BalanceHandler, codec.Address, state.Mutable) error { + return nil +} + +func FeeManagerModifier(*internalfees.Manager, *ResultChanges) error { + return nil +} diff --git a/chain/dependencies.go b/chain/dependencies.go index 6aafbcc435..4894b8f163 100644 --- a/chain/dependencies.go +++ b/chain/dependencies.go @@ -8,11 +8,15 @@ 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/tstate" + + internalfees "github.com/ava-labs/hypersdk/internal/fees" ) type Parser interface { @@ -221,3 +225,17 @@ type ValidityWindow interface { oldestAllowed int64, ) (set.Bits, error) } + +// ExportStateDiffFunc finalizes the state diff +type ExportStateDiffFunc func(context.Context, *tstate.TState, state.View, MetadataManager, uint64) (merkledb.View, error) + +// 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 + +// 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 new file mode 100644 index 0000000000..61220c74e6 --- /dev/null +++ b/chain/option.go @@ -0,0 +1,63 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +import "github.com/ava-labs/hypersdk/state/shim" + +type Options struct { + 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 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.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 617f317100..e223ba30f9 100644 --- a/chain/pre_executor.go +++ b/chain/pre_executor.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ava-labs/hypersdk/state" + "github.com/ava-labs/hypersdk/state/shim" internalfees "github.com/ava-labs/hypersdk/internal/fees" ) @@ -16,6 +17,7 @@ type PreExecutor struct { ruleFactory RuleFactory validityWindow ValidityWindow metadataManager MetadataManager + executionShim shim.Execution balanceHandler BalanceHandler } @@ -23,12 +25,14 @@ func NewPreExecutor( ruleFactory RuleFactory, validityWindow ValidityWindow, metadataManager MetadataManager, + executionShim shim.Execution, balanceHandler BalanceHandler, ) *PreExecutor { return &PreExecutor{ ruleFactory: ruleFactory, validityWindow: validityWindow, metadataManager: metadataManager, + executionShim: executionShim, balanceHandler: balanceHandler, } } @@ -66,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 } @@ -87,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, 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 43f02fd183..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" ) @@ -83,6 +84,11 @@ type Processor struct { balanceHandler BalanceHandler validityWindow ValidityWindow metrics *chainMetrics + executionShim shim.Execution + exportStateDiff ExportStateDiffFunc + dimsModifierFunc FeeManagerModifierFunc + resultModifierFunc ResultModifierFunc + refundFunc RefundFunc config Config } @@ -96,6 +102,11 @@ func NewProcessor( balanceHandler BalanceHandler, validityWindow ValidityWindow, metrics *chainMetrics, + executionShim shim.Execution, + afterBlock ExportStateDiffFunc, + dimsModifierFunc FeeManagerModifierFunc, + resultModifierFunc ResultModifierFunc, + refundFunc RefundFunc, config Config, ) *Processor { return &Processor{ @@ -108,6 +119,11 @@ func NewProcessor( balanceHandler: balanceHandler, validityWindow: validityWindow, metrics: metrics, + executionShim: executionShim, + exportStateDiff: afterBlock, + dimsModifierFunc: dimsModifierFunc, + resultModifierFunc: resultModifierFunc, + refundFunc: refundFunc, config: config, } } @@ -257,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 } @@ -351,13 +367,32 @@ func (p *Processor) executeTxs( return err } + 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 // processed tsv := ts.NewView( stateKeys, - state.ImmutableStorage(storage), + state, len(stateKeys), ) @@ -370,6 +405,21 @@ func (p *Processor) executeTxs( if err != nil { return err } + + 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 + } + results[i] = result // Commit results to parent [TState] 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/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/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/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..e9fbfd9b0d --- /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 = "tieredStorage" + +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.WithExecutionShim( + ExecutionShim(config), + ), + chain.WithExportStateDiffFunc(ExportStateDiff), + chain.WithRefundFunc(Refund), + chain.WithDimsModifierFunc(FeeManagerModifier), + chain.WithResultModifierFunc(ResultModifier), + ), + vm.WithExecutionShim( + ExecutionShim(config), + ), + ), nil +} + +func ExecutionShim(config Config) *Shim { + return &Shim{ + config: config, + } +} 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 c110cdf92c..4ab4dbc17d 100644 --- a/internal/fees/manager.go +++ b/internal/fees/manager.go @@ -152,6 +152,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..e3fcc11082 --- /dev/null +++ b/state/shim/no_op.go @@ -0,0 +1,22 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package shim + +import ( + "context" + + "github.com/ava-labs/hypersdk/state" +) + +var _ Execution = (*ExecutionNoOp)(nil) + +type ExecutionNoOp struct{} + +func (*ExecutionNoOp) ImmutableView(_ context.Context, _ state.Keys, im state.Immutable, _ uint64) (state.Immutable, error) { + return im, nil +} + +func (*ExecutionNoOp) MutableView(mu state.Mutable, _ uint64) state.Mutable { + return mu +} diff --git a/state/shim/shim.go b/state/shim/shim.go new file mode 100644 index 0000000000..57f8230668 --- /dev/null +++ b/state/shim/shim.go @@ -0,0 +1,16 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package shim + +import ( + "context" + + "github.com/ava-labs/hypersdk/state" +) + +type Execution interface { + 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 +} 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/tstate.go b/state/tstate/tstate.go index a2577db122..baae0f703d 100644 --- a/state/tstate/tstate.go +++ b/state/tstate/tstate.go @@ -80,3 +80,10 @@ 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 +} 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 22c7d32748..bbe979c4a1 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.ExecutionNoOp{} + } + if o.builder { vm.builder = builder.NewManual(vm.toEngine, vm.snowCtx.Log) } else { @@ -676,12 +686,17 @@ 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) + 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 {