diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a446dc86c50..8b3ad2cbe13 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,17 +1,19 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 90 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - Preserve - - Idea -# Label to use when marking an issue as stale -staleLabel: Stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false +name: 'Mark and close stale issues' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 90 + days-before-issue-close: 7 + exempt-issue-labels: 'Preserve,Idea' + stale-issue-label: 'Stale' + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. diff --git a/cmd/execution_builder.go b/cmd/execution_builder.go index 3841f70f5f5..2f1795cd8e0 100644 --- a/cmd/execution_builder.go +++ b/cmd/execution_builder.go @@ -691,7 +691,7 @@ func (exeNode *ExecutionNode) LoadExecutionDataGetter(node *NodeConfig) error { return nil } -func openChunkDataPackDB(dbPath string, logger zerolog.Logger) (*badgerDB.DB, error) { +func OpenChunkDataPackDB(dbPath string, logger zerolog.Logger) (*badgerDB.DB, error) { log := sutil.NewLogger(logger) opts := badgerDB. @@ -722,17 +722,21 @@ func (exeNode *ExecutionNode) LoadExecutionState( error, ) { - chunkDataPackDB, err := openChunkDataPackDB(exeNode.exeConf.chunkDataPackDir, node.Logger) + chunkDataPackDB, err := storagepebble.OpenDefaultPebbleDB(exeNode.exeConf.chunkDataPackDir) if err != nil { - return nil, err + return nil, fmt.Errorf("could not open chunk data pack database: %w", err) } + exeNode.builder.ShutdownFunc(func() error { if err := chunkDataPackDB.Close(); err != nil { return fmt.Errorf("error closing chunk data pack database: %w", err) } return nil }) - chunkDataPacks := storage.NewChunkDataPacks(node.Metrics.Cache, chunkDataPackDB, node.Storage.Collections, exeNode.exeConf.chunkDataPackCacheSize) + // chunkDataPacks := storage.NewChunkDataPacks(node.Metrics.Cache, + // chunkDataPackDB, node.Storage.Collections, exeNode.exeConf.chunkDataPackCacheSize) + chunkDataPacks := storagepebble.NewChunkDataPacks(node.Metrics.Cache, + chunkDataPackDB, node.Storage.Collections, exeNode.exeConf.chunkDataPackCacheSize) // Needed for gRPC server, make sure to assign to main scoped vars exeNode.events = storage.NewEvents(node.Metrics.Cache, node.DB) diff --git a/cmd/scaffold.go b/cmd/scaffold.go index 734e8371b92..5db4e75c395 100644 --- a/cmd/scaffold.go +++ b/cmd/scaffold.go @@ -471,6 +471,11 @@ func (fnb *FlowNodeBuilder) EnqueueNetworkInit() { peerManagerFilters) }) + fnb.Module("epoch transition logger", func(node *NodeConfig) error { + node.ProtocolEvents.AddConsumer(events.NewEventLogger(node.Logger)) + return nil + }) + fnb.Module("network underlay dependency", func(node *NodeConfig) error { fnb.networkUnderlayDependable = module.NewProxiedReadyDoneAware() fnb.PeerManagerDependencies.Add(fnb.networkUnderlayDependable) diff --git a/cmd/util/cmd/diff-states/cmd.go b/cmd/util/cmd/diff-states/cmd.go index b26a759b7a4..414f7147d36 100644 --- a/cmd/util/cmd/diff-states/cmd.go +++ b/cmd/util/cmd/diff-states/cmd.go @@ -2,6 +2,7 @@ package diff_states import ( "bytes" + "context" "encoding/hex" "encoding/json" "errors" @@ -17,6 +18,7 @@ import ( "github.com/onflow/flow-go/cmd/util/ledger/util/registers" "github.com/onflow/flow-go/ledger" "github.com/onflow/flow-go/model/flow" + moduleUtil "github.com/onflow/flow-go/module/util" ) var ( @@ -28,6 +30,7 @@ var ( flagStateCommitment1 string flagStateCommitment2 string flagRaw bool + flagAlwaysDiffValues bool flagNWorker int flagChain string ) @@ -40,6 +43,13 @@ var Cmd = &cobra.Command{ const ReporterName = "state-diff" +type state uint8 + +const ( + oldState state = 1 + newState state = 2 +) + func init() { // Input 1 @@ -104,6 +114,13 @@ func init() { "Raw or value", ) + Cmd.Flags().BoolVar( + &flagAlwaysDiffValues, + "always-diff-values", + false, + "always diff on value level. useful when trying to test iteration, by diffing same state.", + ) + Cmd.Flags().IntVar( &flagNWorker, "n-worker", @@ -177,7 +194,10 @@ func run(*cobra.Command, []string) { } } - diff(registers1, registers2, chainID, rw) + err := diff(registers1, registers2, chainID, rw, flagNWorker) + if err != nil { + log.Warn().Err(err).Msgf("failed to diff registers") + } } func loadPayloads() (payloads1, payloads2 []*ledger.Payload) { @@ -266,108 +286,254 @@ func payloadsToRegisters(payloads1, payloads2 []*ledger.Payload) (registers1, re var accountsDiffer = errors.New("accounts differ") +func diffAccount( + owner string, + accountRegisters1 *registers.AccountRegisters, + accountRegisters2 *registers.AccountRegisters, + chainID flow.ChainID, + rw reporters.ReportWriter, +) (err error) { + + if accountRegisters1.Count() != accountRegisters2.Count() { + rw.Write(countDiff{ + Owner: owner, + State1: accountRegisters1.Count(), + State2: accountRegisters2.Count(), + }) + } + + diffValues := flagAlwaysDiffValues + + err = accountRegisters1.ForEach(func(owner, key string, value1 []byte) error { + var value2 []byte + value2, err = accountRegisters2.Get(owner, key) + if err != nil { + return err + } + + if !bytes.Equal(value1, value2) { + + if flagRaw { + rw.Write(rawDiff{ + Owner: owner, + Key: key, + Value1: value1, + Value2: value2, + }) + } else { + // stop on first difference in accounts + return accountsDiffer + } + } + + return nil + }) + if err != nil { + if flagRaw || !errors.Is(err, accountsDiffer) { + return err + } + + diffValues = true + } + + if diffValues { + address, err := common.BytesToAddress([]byte(owner)) + if err != nil { + return err + } + + migrations.NewCadenceValueDiffReporter( + address, + chainID, + rw, + true, + flagNWorker/2, + ).DiffStates( + accountRegisters1, + accountRegisters2, + migrations.AllStorageMapDomains, + ) + } + + return nil +} + func diff( registers1 *registers.ByAccount, registers2 *registers.ByAccount, chainID flow.ChainID, rw reporters.ReportWriter, -) { - log.Info().Msg("Diffing accounts") + nWorkers int, +) error { + log.Info().Msgf("Diffing %d accounts", registers1.AccountCount()) - err := registers1.ForEachAccount(func(accountRegisters1 *registers.AccountRegisters) (err error) { - owner := accountRegisters1.Owner() + if registers1.AccountCount() < nWorkers { + nWorkers = registers1.AccountCount() + } - if !registers2.HasAccountOwner(owner) { - rw.Write(accountMissing{ - Owner: owner, - State: 2, - }) + logAccount := moduleUtil.LogProgress( + log.Logger, + moduleUtil.DefaultLogProgressConfig( + "processing account group", + registers1.AccountCount(), + ), + ) - return nil - } + if nWorkers <= 1 { + foundAccountCountInRegisters2 := 0 - accountRegisters2 := registers2.AccountRegisters(owner) + _ = registers1.ForEachAccount(func(accountRegisters1 *registers.AccountRegisters) (err error) { + owner := accountRegisters1.Owner() - if accountRegisters1.Count() != accountRegisters2.Count() { - rw.Write(countDiff{ - Owner: owner, - State1: accountRegisters1.Count(), - State2: accountRegisters2.Count(), - }) - } + if !registers2.HasAccountOwner(owner) { + rw.Write(accountMissing{ + Owner: owner, + State: int(newState), + }) + + return nil + } - err = accountRegisters1.ForEach(func(owner, key string, value1 []byte) error { - var value2 []byte - value2, err = accountRegisters2.Get(owner, key) + foundAccountCountInRegisters2++ + + accountRegisters2 := registers2.AccountRegisters(owner) + + err = diffAccount( + owner, + accountRegisters1, + accountRegisters2, + chainID, + rw, + ) if err != nil { - return err + log.Warn().Err(err).Msgf("failed to diff account %x", []byte(owner)) } - if !bytes.Equal(value1, value2) { + logAccount(1) + + return nil + }) - if flagRaw { - rw.Write(rawDiff{ - Owner: owner, - Key: key, - Value1: value1, - Value2: value2, + if foundAccountCountInRegisters2 < registers2.AccountCount() { + _ = registers2.ForEachAccount(func(accountRegisters2 *registers.AccountRegisters) error { + owner := accountRegisters2.Owner() + if !registers1.HasAccountOwner(owner) { + rw.Write(accountMissing{ + Owner: owner, + State: int(oldState), }) - } else { - // stop on first difference in accounts - return accountsDiffer } - } + return nil + }) + } + + return nil + } + + type job struct { + owner string + accountRegisters1 *registers.AccountRegisters + accountRegisters2 *registers.AccountRegisters + } + + type result struct { + owner string + err error + } + + jobs := make(chan job, nWorkers) + + results := make(chan result, nWorkers) + + g, ctx := errgroup.WithContext(context.Background()) + // Launch goroutines to diff accounts + for i := 0; i < nWorkers; i++ { + g.Go(func() (err error) { + for job := range jobs { + err := diffAccount( + job.owner, + job.accountRegisters1, + job.accountRegisters2, + chainID, + rw, + ) + + select { + case results <- result{owner: job.owner, err: err}: + case <-ctx.Done(): + return ctx.Err() + } + } return nil }) - if err != nil { - if flagRaw || !errors.Is(err, accountsDiffer) { - return err - } + } - address, err := common.BytesToAddress([]byte(owner)) - if err != nil { - return err + // Launch goroutine to wait for workers and close result channel + go func() { + _ = g.Wait() + close(results) + }() + + // Launch goroutine to send account registers to jobs channel + go func() { + defer close(jobs) + + foundAccountCountInRegisters2 := 0 + + _ = registers1.ForEachAccount(func(accountRegisters1 *registers.AccountRegisters) (err error) { + owner := accountRegisters1.Owner() + if !registers2.HasAccountOwner(owner) { + rw.Write(accountMissing{ + Owner: owner, + State: int(newState), + }) + + return nil } - migrations.NewCadenceValueDiffReporter( - address, - chainID, - rw, - true, - flagNWorker, - ).DiffStates( - accountRegisters1, - accountRegisters2, - migrations.AllStorageMapDomains, - ) - } + foundAccountCountInRegisters2++ - return nil - }) - if err != nil { - log.Fatal().Err(err).Msg("failed to diff") - } + accountRegisters2 := registers2.AccountRegisters(owner) - err = registers2.ForEachAccount(func(accountRegisters2 *registers.AccountRegisters) (err error) { - owner := accountRegisters2.Owner() + jobs <- job{ + owner: owner, + accountRegisters1: accountRegisters1, + accountRegisters2: accountRegisters2, + } - if !registers1.HasAccountOwner(owner) { - rw.Write(accountMissing{ - Owner: owner, - State: 1, - }) return nil + }) + + if foundAccountCountInRegisters2 < registers2.AccountCount() { + _ = registers2.ForEachAccount(func(accountRegisters2 *registers.AccountRegisters) (err error) { + owner := accountRegisters2.Owner() + if !registers1.HasAccountOwner(owner) { + rw.Write(accountMissing{ + Owner: owner, + State: int(oldState), + }) + } + return nil + }) } + }() - return nil - }) - if err != nil { - log.Fatal().Err(err).Msg("failed to diff") + // Gather results + for result := range results { + logAccount(1) + if result.err != nil { + log.Warn().Err(result.err).Msgf("failed to diff account %x", []byte(result.owner)) + } } - log.Info().Msg("Finished diffing accounts") + log.Info().Msgf("Finished diffing accounts, waiting for goroutines...") + + if err := g.Wait(); err != nil { + return err + } + return nil } type rawDiff struct { diff --git a/cmd/util/cmd/diff-states/diff_states_test.go b/cmd/util/cmd/diff-states/diff_states_test.go index 01c238ca3ab..fe6041dec5d 100644 --- a/cmd/util/cmd/diff-states/diff_states_test.go +++ b/cmd/util/cmd/diff-states/diff_states_test.go @@ -1,6 +1,7 @@ package diff_states import ( + "encoding/json" "io/fs" "path/filepath" "strings" @@ -82,18 +83,14 @@ func TestDiffStates(t *testing.T) { report, err := io.ReadFile(reportPath) require.NoError(t, err) - assert.JSONEq( - t, - ` - [ - {"kind":"raw-diff", "owner":"0100000000000000", "key":"62", "value1":"03", "value2":"05"}, - {"kind":"account-missing", "owner":"0200000000000000", "state":2}, - {"kind":"account-missing", "owner":"0300000000000000", "state":1}, - {"kind":"account-missing", "owner":"0400000000000000", "state":1} - ] - `, - string(report), - ) + var msgs []any + err = json.Unmarshal(report, &msgs) + require.NoError(t, err) + assert.Equal(t, 4, len(msgs)) + assert.Containsf(t, string(report), `{"kind":"raw-diff","owner":"0100000000000000","key":"62","value1":"03","value2":"05"}`, "diff report contains raw-diff") + assert.Containsf(t, string(report), `{"kind":"account-missing","owner":"0200000000000000","state":2}`, "diff report contains account-missing for 0200000000000000") + assert.Containsf(t, string(report), `{"kind":"account-missing","owner":"0300000000000000","state":1}`, "diff report contains account-missing for 0300000000000000") + assert.Containsf(t, string(report), `{"kind":"account-missing","owner":"0400000000000000","state":1}`, "diff report contains account-missing for 0400000000000000") }) } diff --git a/cmd/util/cmd/execution-state-extract/cmd.go b/cmd/util/cmd/execution-state-extract/cmd.go index f9701e503c3..b8920226a4a 100644 --- a/cmd/util/cmd/execution-state-extract/cmd.go +++ b/cmd/util/cmd/execution-state-extract/cmd.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "runtime/pprof" "strings" "github.com/rs/zerolog/log" @@ -178,6 +179,20 @@ func init() { } func run(*cobra.Command, []string) { + if flagCPUProfile != "" { + f, err := os.Create(flagCPUProfile) + if err != nil { + log.Fatal().Err(err).Msg("could not create CPU profile") + } + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal().Err(err).Msg("could not start CPU profile") + } + + defer pprof.StopCPUProfile() + } + var stateCommitment flow.StateCommitment if len(flagBlockHash) > 0 && len(flagStateCommitment) > 0 { diff --git a/cmd/util/cmd/execution-state-extract/execution_state_extract.go b/cmd/util/cmd/execution-state-extract/execution_state_extract.go index 16617522108..cc5ee3d48c4 100644 --- a/cmd/util/cmd/execution-state-extract/execution_state_extract.go +++ b/cmd/util/cmd/execution-state-extract/execution_state_extract.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "os" - "runtime/pprof" syncAtomic "sync/atomic" "time" @@ -326,19 +325,6 @@ func newMigration( nWorker int, ) ledger.Migration { return func(payloads []*ledger.Payload) ([]*ledger.Payload, error) { - if flagCPUProfile != "" { - f, err := os.Create(flagCPUProfile) - if err != nil { - logger.Fatal().Err(err).Msg("could not create CPU profile") - } - - err = pprof.StartCPUProfile(f) - if err != nil { - logger.Fatal().Err(err).Msg("could not start CPU profile") - } - - defer pprof.StopCPUProfile() - } if len(migrations) == 0 { return payloads, nil diff --git a/cmd/util/ledger/migrations/cadence_value_diff.go b/cmd/util/ledger/migrations/cadence_value_diff.go index a12dcd7126a..904fd747887 100644 --- a/cmd/util/ledger/migrations/cadence_value_diff.go +++ b/cmd/util/ledger/migrations/cadence_value_diff.go @@ -2,9 +2,13 @@ package migrations import ( "fmt" + "time" + "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/interpreter" + "github.com/rs/zerolog/log" + "golang.org/x/sync/errgroup" "github.com/onflow/flow-go/cmd/util/ledger/reporters" "github.com/onflow/flow-go/cmd/util/ledger/util" @@ -72,6 +76,8 @@ type difference struct { NewValueStaticType string `json:",omitempty"` } +const minLargeAccountRegisterCount = 1_000_000 + type CadenceValueDiffReporter struct { address common.Address chainID flow.ChainID @@ -96,81 +102,130 @@ func NewCadenceValueDiffReporter( } } -func (dr *CadenceValueDiffReporter) newStorageRuntime( - registers registers.Registers, -) ( - *InterpreterMigrationRuntime, - error, -) { - // TODO: maybe make read-only again - runtimeInterface, err := NewInterpreterMigrationRuntime( - registers, - dr.chainID, - InterpreterMigrationRuntimeConfig{}, - ) - if err != nil { - return nil, err - } +func (dr *CadenceValueDiffReporter) DiffStates(oldRegs, newRegs registers.Registers, domains []string) { - return runtimeInterface, nil -} + oldStorage := newReadonlyStorage(oldRegs) -func (dr *CadenceValueDiffReporter) DiffStates(oldRegs, newRegs registers.Registers, domains []string) { + newStorage := newReadonlyStorage(newRegs) + + var loadAtreeStorageGroup errgroup.Group - // Create all the runtime components we need for comparing Cadence values. - oldRuntime, err := dr.newStorageRuntime(oldRegs) + loadAtreeStorageGroup.Go(func() (err error) { + return loadAtreeSlabsInStorage(oldStorage, oldRegs, dr.nWorkers) + }) + + err := loadAtreeSlabsInStorage(newStorage, newRegs, dr.nWorkers) if err != nil { dr.reportWriter.Write( diffError{ Address: dr.address.Hex(), Kind: diffErrorKindString[abortErrorKind], - Msg: fmt.Sprintf("failed to create runtime for old registers: %s", err), + Msg: fmt.Sprintf("failed to preload new atree registers: %s", err), }) return } - newRuntime, err := dr.newStorageRuntime(newRegs) - if err != nil { + // Wait for old registers to be loaded in storage. + if err := loadAtreeStorageGroup.Wait(); err != nil { dr.reportWriter.Write( diffError{ Address: dr.address.Hex(), Kind: diffErrorKindString[abortErrorKind], - Msg: fmt.Sprintf("failed to create runtime with new registers: %s", err), + Msg: fmt.Sprintf("failed to preload old atree registers: %s", err), }) return } - err = loadAtreeSlabsInStorage(oldRuntime.Storage, oldRegs, dr.nWorkers) + if oldRegs.Count() > minLargeAccountRegisterCount { + // Add concurrency to diff domains + var g errgroup.Group + + // NOTE: preload storage map in the same goroutine + for _, domain := range domains { + _ = oldStorage.GetStorageMap(dr.address, domain, false) + _ = newStorage.GetStorageMap(dr.address, domain, false) + } + + // Create goroutine to diff storage domain + g.Go(func() (err error) { + oldRuntime, err := newReadonlyStorageRuntimeWithStorage(oldStorage, oldRegs.Count()) + if err != nil { + return fmt.Errorf("failed to create runtime for old registers: %s", err) + } + + newRuntime, err := newReadonlyStorageRuntimeWithStorage(newStorage, newRegs.Count()) + if err != nil { + return fmt.Errorf("failed to create runtime for new registers: %s", err) + } + + dr.diffDomain(oldRuntime, newRuntime, common.PathDomainStorage.Identifier()) + return nil + }) + + // Create goroutine to diff other domains + g.Go(func() (err error) { + oldRuntime, err := newReadonlyStorageRuntimeWithStorage(oldStorage, oldRegs.Count()) + if err != nil { + return fmt.Errorf("failed to create runtime for old registers: %s", err) + } + + newRuntime, err := newReadonlyStorageRuntimeWithStorage(newStorage, oldRegs.Count()) + if err != nil { + return fmt.Errorf("failed to create runtime for new registers: %s", err) + } + + for _, domain := range domains { + if domain != common.PathDomainStorage.Identifier() { + dr.diffDomain(oldRuntime, newRuntime, domain) + } + } + return nil + }) + + err = g.Wait() + if err != nil { + dr.reportWriter.Write( + diffError{ + Address: dr.address.Hex(), + Kind: diffErrorKindString[abortErrorKind], + Msg: err.Error(), + }) + } + + return + } + + // Skip goroutine overhead for smaller accounts + oldRuntime, err := newReadonlyStorageRuntimeWithStorage(oldStorage, oldRegs.Count()) if err != nil { dr.reportWriter.Write( diffError{ Address: dr.address.Hex(), Kind: diffErrorKindString[abortErrorKind], - Msg: fmt.Sprintf("failed to preload old atree registers: %s", err), + Msg: fmt.Sprintf("failed to create runtime for old registers: %s", err), }) return } - err = loadAtreeSlabsInStorage(newRuntime.Storage, newRegs, dr.nWorkers) + newRuntime, err := newReadonlyStorageRuntimeWithStorage(newStorage, newRegs.Count()) if err != nil { dr.reportWriter.Write( diffError{ Address: dr.address.Hex(), Kind: diffErrorKindString[abortErrorKind], - Msg: fmt.Sprintf("failed to preload new atree registers: %s", err), + Msg: fmt.Sprintf("failed to create runtime with new registers: %s", err), }) return } - // Iterate through all domains and compare cadence values. for _, domain := range domains { - dr.diffStorageDomain(oldRuntime, newRuntime, domain) + dr.diffDomain(oldRuntime, newRuntime, domain) } } -func (dr *CadenceValueDiffReporter) diffStorageDomain( - oldRuntime *InterpreterMigrationRuntime, - newRuntime *InterpreterMigrationRuntime, +func (dr *CadenceValueDiffReporter) diffDomain( + oldRuntime *readonlyStorageRuntime, + newRuntime *readonlyStorageRuntime, domain string, ) { defer func() { @@ -264,8 +319,11 @@ func (dr *CadenceValueDiffReporter) diffStorageDomain( }) } - // Compare elements present in both storage maps - for _, key := range sharedKeys { + if len(sharedKeys) == 0 { + return + } + + getValues := func(key any) (interpreter.Value, interpreter.Value, *util.Trace, bool) { trace := util.NewTrace(fmt.Sprintf("%s[%v]", domain, key)) @@ -297,17 +355,27 @@ func (dr *CadenceValueDiffReporter) diffStorageDomain( key, ), }) - continue + return nil, nil, nil, false } oldValue := oldStorageMap.ReadValue(nil, mapKey) newValue := newStorageMap.ReadValue(nil, mapKey) + return oldValue, newValue, trace, true + } + + diffValues := func( + oldInterpreter *interpreter.Interpreter, + oldValue interpreter.Value, + newInterpreter *interpreter.Interpreter, + newValue interpreter.Value, + trace *util.Trace, + ) { hasDifference := dr.diffValues( - oldRuntime.Interpreter, + oldInterpreter, oldValue, - newRuntime.Interpreter, + newInterpreter, newValue, domain, trace, @@ -324,13 +392,139 @@ func (dr *CadenceValueDiffReporter) diffStorageDomain( Trace: trace.String(), OldValue: oldValue.String(), NewValue: newValue.String(), - OldValueStaticType: oldValue.StaticType(oldRuntime.Interpreter).String(), - NewValueStaticType: newValue.StaticType(newRuntime.Interpreter).String(), + OldValueStaticType: oldValue.StaticType(oldInterpreter).String(), + NewValueStaticType: newValue.StaticType(newInterpreter).String(), }) } } + } + + // Skip goroutine overhead for non-storage domain and small accounts. + if domain != common.PathDomainStorage.Identifier() || + oldRuntime.PayloadCount < minLargeAccountRegisterCount || + len(sharedKeys) == 1 { + + for _, key := range sharedKeys { + oldValue, newValue, trace, canDiff := getValues(key) + if canDiff { + diffValues( + oldRuntime.Interpreter, + oldValue, + newRuntime.Interpreter, + newValue, + trace, + ) + } + } + return + } + + startTime := time.Now() + + log.Info().Msgf( + "Diffing %x storage domain containing %d elements (%d payloads) ...", + dr.address[:], + len(sharedKeys), + oldRuntime.PayloadCount, + ) + + // Diffing storage domain in large account + + type job struct { + oldValue interpreter.Value + newValue interpreter.Value + trace *util.Trace + } + nWorkers := dr.nWorkers + if len(sharedKeys) < nWorkers { + nWorkers = len(sharedKeys) } + + jobs := make(chan job, nWorkers) + + var g errgroup.Group + + for i := 0; i < nWorkers; i++ { + + g.Go(func() error { + oldInterpreter, err := interpreter.NewInterpreter( + nil, + nil, + &interpreter.Config{ + Storage: oldRuntime.Storage, + }, + ) + if err != nil { + dr.reportWriter.Write( + diffError{ + Address: dr.address.Hex(), + Kind: diffErrorKindString[abortErrorKind], + Msg: fmt.Sprintf("failed to create interpreter for old registers: %s", err), + }) + return nil + } + + newInterpreter, err := interpreter.NewInterpreter( + nil, + nil, + &interpreter.Config{ + Storage: newRuntime.Storage, + }, + ) + if err != nil { + dr.reportWriter.Write( + diffError{ + Address: dr.address.Hex(), + Kind: diffErrorKindString[abortErrorKind], + Msg: fmt.Sprintf("failed to create interpreter for new registers: %s", err), + }) + return nil + } + + for job := range jobs { + diffValues(oldInterpreter, job.oldValue, newInterpreter, job.newValue, job.trace) + } + + return nil + }) + } + + // Launch goroutine to send account registers to jobs channel + go func() { + defer close(jobs) + + for _, key := range sharedKeys { + oldValue, newValue, trace, canDiff := getValues(key) + if canDiff { + jobs <- job{ + oldValue: oldValue, + newValue: newValue, + trace: trace, + } + } + } + }() + + // Wait for workers + err := g.Wait() + if err != nil { + dr.reportWriter.Write( + diffError{ + Address: dr.address.Hex(), + Kind: diffErrorKindString[abortErrorKind], + Msg: fmt.Sprintf("failed to diff domain %s: %s", domain, err), + }) + } + + log.Info(). + Msgf( + "Finished diffing %x storage domain containing %d elements (%d payloads) in %s", + dr.address[:], + len(sharedKeys), + oldRuntime.PayloadCount, + time.Since(startTime), + ) } func (dr *CadenceValueDiffReporter) diffValues( @@ -794,20 +988,47 @@ func (dr *CadenceValueDiffReporter) diffCadenceDictionaryValue( } oldKeys := make([]interpreter.Value, 0, v.Count()) - v.IterateKeys(vInterpreter, func(key interpreter.Value) (resume bool) { + v.IterateKeys(vInterpreter, interpreter.EmptyLocationRange, func(key interpreter.Value) (resume bool) { oldKeys = append(oldKeys, key) return true }) newKeys := make([]interpreter.Value, 0, otherDictionary.Count()) - otherDictionary.IterateKeys(otherInterpreter, func(key interpreter.Value) (resume bool) { + otherDictionary.IterateKeys(otherInterpreter, interpreter.EmptyLocationRange, func(key interpreter.Value) (resume bool) { newKeys = append(newKeys, key) return true }) - onlyOldKeys, onlyNewKeys, sharedKeys := diffCadenceValues(oldKeys, newKeys) + onlyOldKeys := make([]interpreter.Value, 0, len(oldKeys)) + + // Compare elements in both dict values + + for _, key := range oldKeys { + valueTrace := trace.Append(fmt.Sprintf("[%v]", key)) + + oldValue, _ := v.Get(vInterpreter, interpreter.EmptyLocationRange, key) + + newValue, found := otherDictionary.Get(otherInterpreter, interpreter.EmptyLocationRange, key) + if !found { + onlyOldKeys = append(onlyOldKeys, key) + continue + } + + elementHasDifference := dr.diffValues( + vInterpreter, + oldValue, + otherInterpreter, + newValue, + domain, + valueTrace, + ) + if elementHasDifference { + hasDifference = true + } + } // Log keys only present in old dict value + if len(onlyOldKeys) > 0 { hasDifference = true @@ -825,42 +1046,34 @@ func (dr *CadenceValueDiffReporter) diffCadenceDictionaryValue( }) } - // Log field names only present in new composite value - if len(onlyNewKeys) > 0 { - hasDifference = true + // Log keys only present in new dict value - dr.reportWriter.Write( - difference{ - Address: dr.address.Hex(), - Domain: domain, - Kind: diffKindString[cadenceValueDiffKind], - Trace: trace.String(), - Msg: fmt.Sprintf( - "new dict value has %d elements with keys %v, that are not present in old dict value", - len(onlyNewKeys), - onlyNewKeys, - ), - }) - } - - // Compare elements in both dict values - for _, key := range sharedKeys { - valueTrace := trace.Append(fmt.Sprintf("[%v]", key)) + if len(oldKeys) != len(newKeys) || len(onlyOldKeys) > 0 { + onlyNewKeys := make([]interpreter.Value, 0, len(newKeys)) - oldValue, _ := v.Get(vInterpreter, interpreter.EmptyLocationRange, key) - - newValue, _ := otherDictionary.Get(otherInterpreter, interpreter.EmptyLocationRange, key) + // find keys only present in new dict + for _, key := range newKeys { + found := v.ContainsKey(vInterpreter, interpreter.EmptyLocationRange, key) + if !found { + onlyNewKeys = append(onlyNewKeys, key) + } + } - elementHasDifference := dr.diffValues( - vInterpreter, - oldValue, - otherInterpreter, - newValue, - domain, - valueTrace, - ) - if elementHasDifference { + if len(onlyNewKeys) > 0 { hasDifference = true + + dr.reportWriter.Write( + difference{ + Address: dr.address.Hex(), + Domain: domain, + Kind: diffKindString[cadenceValueDiffKind], + Trace: trace.String(), + Msg: fmt.Sprintf( + "new dict value has %d elements with keys %v, that are not present in old dict value", + len(onlyNewKeys), + onlyNewKeys, + ), + }) } } @@ -915,54 +1128,39 @@ func diff[T comparable](old, new []T) (onlyOld, onlyNew, shared []T) { return } -func diffCadenceValues(old, new []interpreter.Value) (onlyOld, onlyNew, shared []interpreter.Value) { - onlyOld = make([]interpreter.Value, 0, len(old)) - onlyNew = make([]interpreter.Value, 0, len(new)) - shared = make([]interpreter.Value, 0, min(len(old), len(new))) - - sharedNew := make([]bool, len(new)) - - for _, o := range old { - found := false - - for i, n := range new { - foundShared := false - - if ev, ok := o.(interpreter.EquatableValue); ok { - if ev.Equal(nil, interpreter.EmptyLocationRange, n) { - foundShared = true - } - } else { - if o == n { - foundShared = true - } - } - - if foundShared { - shared = append(shared, o) - found = true - sharedNew[i] = true - break - } - } - - if !found { - onlyOld = append(onlyOld, o) - } +func min(a, b int) int { + if a <= b { + return a } + return b +} - for i, shared := range sharedNew { - if !shared { - onlyNew = append(onlyNew, new[i]) - } - } +func newReadonlyStorage(regs registers.Registers) *runtime.Storage { + ledger := ®isters.ReadOnlyLedger{Registers: regs} + return runtime.NewStorage(ledger, nil) +} - return +type readonlyStorageRuntime struct { + Interpreter *interpreter.Interpreter + Storage *runtime.Storage + PayloadCount int } -func min(a, b int) int { - if a <= b { - return a +func newReadonlyStorageRuntimeWithStorage(storage *runtime.Storage, payloadCount int) (*readonlyStorageRuntime, error) { + inter, err := interpreter.NewInterpreter( + nil, + nil, + &interpreter.Config{ + Storage: storage, + }, + ) + if err != nil { + return nil, err } - return b + + return &readonlyStorageRuntime{ + Interpreter: inter, + Storage: storage, + PayloadCount: payloadCount, + }, nil } diff --git a/cmd/util/ledger/migrations/cadence_values_migration_test.go b/cmd/util/ledger/migrations/cadence_values_migration_test.go index f63d2fa99e5..4cdb00650e3 100644 --- a/cmd/util/ledger/migrations/cadence_values_migration_test.go +++ b/cmd/util/ledger/migrations/cadence_values_migration_test.go @@ -457,6 +457,46 @@ func checkMigratedState( storageMapKey: interpreter.Uint64StorageMapKey(0x3), value: "StorageCapabilityController(borrowType: Type<&A.0ae53cb6e3f42a79.FlowToken.Vault>(), capabilityID: 3, target: /storage/flowTokenVault)", }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("flowTokenVault"), + value: "3", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("flowTokenVault"), + value: "1", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("flowTokenVault"), + value: "nil", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("flowTokenVault"), + value: "nil", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("flowTokenVault"), + value: "{3: nil, 1: nil}", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("r"), + value: "2", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("r"), + value: "nil", + }, + { + storageKey: interpreter.StorageKey{Key: "path_cap", Address: address}, + storageMapKey: interpreter.StringStorageMapKey("r"), + value: "{2: nil}", + }, }, visitMigration.visits, ) @@ -1210,6 +1250,16 @@ func TestProgramParsingError(t *testing.T) { ) require.NoError(t, err) + encodedContractNames, err := environment.EncodeContractNames([]string{contractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(testAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + // Migrate // TODO: EVM contract is not deployed in snapshot yet, so can't update it diff --git a/cmd/util/ledger/migrations/contract_checking_migration.go b/cmd/util/ledger/migrations/contract_checking_migration.go index d891ed5c966..6683ee5569a 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration.go +++ b/cmd/util/ledger/migrations/contract_checking_migration.go @@ -12,6 +12,7 @@ import ( "github.com/onflow/flow-go/cmd/util/ledger/reporters" "github.com/onflow/flow-go/cmd/util/ledger/util/registers" + "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/model/flow" ) @@ -30,6 +31,7 @@ func NewContractCheckingMigration( return func(registersByAccount *registers.ByAccount) error { reporter := rwf.ReportWriter(contractCheckingReporterName) + defer reporter.Close() mr, err := NewInterpreterMigrationRuntime( registersByAccount, @@ -42,6 +44,8 @@ func NewContractCheckingMigration( // Gather all contracts + log.Info().Msg("Gathering contracts ...") + contractsForPrettyPrinting := make(map[common.Location][]byte, contractCountEstimate) type contract struct { @@ -50,35 +54,49 @@ func NewContractCheckingMigration( } contracts := make([]contract, 0, contractCountEstimate) - err = registersByAccount.ForEach(func(owner string, key string, value []byte) error { + err = registersByAccount.ForEachAccount(func(accountRegisters *registers.AccountRegisters) error { + owner := accountRegisters.Owner() - // Skip payloads that are not contract code - contractName := flow.KeyContractName(key) - if contractName == "" { - return nil + encodedContractNames, err := accountRegisters.Get(owner, flow.ContractNamesKey) + if err != nil { + return err } - address := common.Address([]byte(owner)) - code := value - location := common.AddressLocation{ - Address: address, - Name: contractName, + contractNames, err := environment.DecodeContractNames(encodedContractNames) + if err != nil { + return err } - contracts = append( - contracts, - contract{ - location: location, - code: code, - }, - ) + for _, contractName := range contractNames { + + contractKey := flow.ContractKey(contractName) + + code, err := accountRegisters.Get(owner, contractKey) + if err != nil { + return err + } + + address := common.Address([]byte(owner)) + location := common.AddressLocation{ + Address: address, + Name: contractName, + } + + contracts = append( + contracts, + contract{ + location: location, + code: code, + }, + ) - contractsForPrettyPrinting[location] = code + contractsForPrettyPrinting[location] = code + } return nil }) if err != nil { - return fmt.Errorf("failed to iterate over registers: %w", err) + return fmt.Errorf("failed to get contracts of accounts: %w", err) } sort.Slice(contracts, func(i, j int) bool { @@ -87,6 +105,8 @@ func NewContractCheckingMigration( return a.location.ID() < b.location.ID() }) + log.Info().Msgf("Gathered all contracts (%d)", len(contracts)) + // Check all contracts for _, contract := range contracts { @@ -134,8 +154,6 @@ func NewContractCheckingMigration( } } - reporter.Close() - return nil } } diff --git a/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration.go b/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration.go index 6826c15896c..08eb99341c2 100644 --- a/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration.go +++ b/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration.go @@ -2,6 +2,7 @@ package migrations import ( "context" + "encoding/binary" "errors" "fmt" "path" @@ -23,12 +24,17 @@ import ( "github.com/onflow/flow-go/model/flow" ) -func registerFromStorageID(storageID atree.StorageID) (owner, key string) { - owner = string(storageID.Address[:]) +func registerFromSlabID(slabID atree.SlabID) (owner, key string) { + var address [8]byte + binary.BigEndian.PutUint64(address[:], slabID.AddressAsUint64()) + + index := slabID.Index() + + owner = string(address[:]) var sb strings.Builder sb.WriteByte(flow.SlabIndexPrefix) - sb.Write(storageID.Index[:]) + sb.Write(index[:]) key = sb.String() return owner, key @@ -103,7 +109,7 @@ func (m *FilterUnreferencedSlabsMigration) MigrateAccount( // Create a set of unreferenced slabs: root slabs, and all slabs they reference. - unreferencedSlabIDs := map[atree.StorageID]struct{}{} + unreferencedSlabIDs := map[atree.SlabID]struct{}{} for _, rootSlabID := range unreferencedRootSlabsErr.UnreferencedRootSlabIDs { unreferencedSlabIDs[rootSlabID] = struct{}{} @@ -129,28 +135,28 @@ func (m *FilterUnreferencedSlabsMigration) MigrateAccount( Str("account", address.HexWithPrefix()). Msgf("filtering %d unreferenced slabs", len(unreferencedSlabIDs)) - var storageIDs []atree.StorageID + var slabIDs []atree.SlabID for storageID := range unreferencedSlabIDs { - storageIDs = append(storageIDs, storageID) + slabIDs = append(slabIDs, storageID) } sort.Slice( - storageIDs, + slabIDs, func(i, j int) bool { - a := storageIDs[i] - b := storageIDs[j] + a := slabIDs[i] + b := slabIDs[j] return a.Compare(b) < 0 }, ) - for _, storageID := range storageIDs { - owner, key := registerFromStorageID(storageID) + for _, slabID := range slabIDs { + owner, key := registerFromSlabID(slabID) value, err := accountRegisters.Get(owner, key) if err != nil { return fmt.Errorf( "failed to get register for slab %x/%x: %w", owner, - storageID.Index, + slabID.Index(), err, ) } @@ -160,7 +166,7 @@ func (m *FilterUnreferencedSlabsMigration) MigrateAccount( return fmt.Errorf( "failed to set register for slab %x/%x: %w", owner, - storageID.Index, + slabID.Index(), err, ) } diff --git a/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration_test.go b/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration_test.go index 4c9d3aa3b99..0ed6f5f7752 100644 --- a/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration_test.go +++ b/cmd/util/ledger/migrations/filter_unreferenced_slabs_migration_test.go @@ -40,8 +40,8 @@ func TestFilterUnreferencedSlabs(t *testing.T) { payloadsLedger := util.NewPayloadsLedger(payloads) storageIndices := map[string]uint64{} - payloadsLedger.AllocateStorageIndexFunc = func(owner []byte) (atree.StorageIndex, error) { - var index atree.StorageIndex + payloadsLedger.AllocateSlabIndexFunc = func(owner []byte) (atree.SlabIndex, error) { + var index atree.SlabIndex storageIndices[string(owner)]++ @@ -78,7 +78,7 @@ func TestFilterUnreferencedSlabs(t *testing.T) { testAddress, ) - // Storage another dictionary, with a nested array, in the account. + // Store another dictionary, with a nested array, in the account. // It is not referenced through a storage map though. arrayStaticType := interpreter.NewVariableSizedStaticType(nil, interpreter.PrimitiveStaticTypeInt) @@ -96,16 +96,25 @@ func TestFilterUnreferencedSlabs(t *testing.T) { testAddress, ) - dict2.InsertWithoutTransfer( + // Ensure the array is large enough to be stored in a separate slab + arrayCount := 100 + arrayValues := make([]interpreter.Value, arrayCount) + for i := 0; i < arrayCount; i++ { + arrayValues[i] = interpreter.NewUnmeteredIntValueFromInt64(int64(i)) + } + + array := interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayStaticType, + common.ZeroAddress, + arrayValues..., + ) + + dict2.Insert( inter, interpreter.EmptyLocationRange, interpreter.NewUnmeteredIntValueFromInt64(2), - interpreter.NewArrayValue( - inter, - interpreter.EmptyLocationRange, - arrayStaticType, - testAddress, - interpreter.NewUnmeteredIntValueFromInt64(3), - ), + array, ) storageMap := storage.GetStorageMap( @@ -129,10 +138,19 @@ func TestFilterUnreferencedSlabs(t *testing.T) { oldPayloads := make([]*ledger.Payload, 0, len(payloads)) for _, payload := range payloadsLedger.Payloads { + if len(payload.Value()) == 0 { + // Don't count empty slabs as result of inlining. + continue + } oldPayloads = append(oldPayloads, payload) } - const totalSlabCount = 5 + // Storage has 4 non-empty payloads: + // - storage map + // - dict1 + // - dict2 + // - nested array in dict2 + const totalSlabCount = 4 require.Len(t, oldPayloads, totalSlabCount) diff --git a/cmd/util/ledger/migrations/fix_broken_data_migration.go b/cmd/util/ledger/migrations/fix_broken_data_migration.go index 03ed27cdb7b..5c717198a06 100644 --- a/cmd/util/ledger/migrations/fix_broken_data_migration.go +++ b/cmd/util/ledger/migrations/fix_broken_data_migration.go @@ -209,7 +209,7 @@ func (m *FixSlabsWithBrokenReferencesMigration) writeBrokenPayloads() error { func getAtreePayloadsByID( registers *registers.AccountRegisters, - ids map[atree.StorageID][]atree.StorageID, + ids map[atree.SlabID][]atree.SlabID, ) ( []*ledger.Payload, error, @@ -225,12 +225,12 @@ func getAtreePayloadsByID( return nil } - storageID := atree.NewStorageID( + slabID := atree.NewSlabID( atree.Address([]byte(owner)), - atree.StorageIndex([]byte(key[1:])), + atree.SlabIndex([]byte(key[1:])), ) - _, ok := ids[storageID] + _, ok := ids[slabID] if !ok { return nil } diff --git a/cmd/util/ledger/migrations/fix_broken_data_migration_test.go b/cmd/util/ledger/migrations/fix_broken_data_migration_test.go index 4c8a649b700..bb1c69c7f96 100644 --- a/cmd/util/ledger/migrations/fix_broken_data_migration_test.go +++ b/cmd/util/ledger/migrations/fix_broken_data_migration_test.go @@ -112,14 +112,14 @@ func TestFixSlabsWithBrokenReferences(t *testing.T) { fixedSlabWithBrokenReferences := ledger.NewPayload( ledger.NewKey([]ledger.KeyPart{ownerKey, {Type: 2, Value: slabIndexWithBrokenReferences}}), - mustDecodeHex("008883d8d982d8d582d8c0824848602d8056ff9d937046616e546f705065726d697373696f6e7546616e546f705065726d697373696f6e2e526f6c65d8ddf6001b535c9de83a38cab0008883005b00000000000000009b0000000000000000"), + ledger.Value(mustDecodeHex("108883d8d982d8d582d8c0824848602d8056ff9d937046616e546f705065726d697373696f6e7546616e546f705065726d697373696f6e2e526f6c65d8ddf6001b535c9de83a38cab08300590000990000")), ) // Account status register is updated to include address ID counter and new storage used. accountStatusRegisterID := mustDecodeHex("612e73") updatedAccountStatusRegister := ledger.NewPayload( ledger.NewKey([]ledger.KeyPart{ownerKey, {Type: 2, Value: accountStatusRegisterID}}), - []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + ledger.Value([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xbe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), ) expectedNewPayloads := make([]*ledger.Payload, len(oldPayloads)) diff --git a/cmd/util/ledger/migrations/migration_matrics_collector_test.go b/cmd/util/ledger/migrations/migration_matrics_collector_test.go index 1bd70a82739..29afafb19ef 100644 --- a/cmd/util/ledger/migrations/migration_matrics_collector_test.go +++ b/cmd/util/ledger/migrations/migration_matrics_collector_test.go @@ -87,7 +87,7 @@ func TestMigrationMetricsCollection(t *testing.T) { require.Equal( t, Metrics{ - TotalValues: 752, + TotalValues: 789, TotalErrors: 6, ErrorsPerContract: map[string]int{ "A.01cf0e2f2f715450.Test": 6, @@ -187,7 +187,7 @@ func TestMigrationMetricsCollection(t *testing.T) { require.Equal( t, Metrics{ - TotalValues: 752, + TotalValues: 789, TotalErrors: 6, ErrorsPerContract: map[string]int{ "A.01cf0e2f2f715450.Test": 6, diff --git a/cmd/util/ledger/migrations/staged_contracts_migration_test.go b/cmd/util/ledger/migrations/staged_contracts_migration_test.go index 2c803adcb82..bb253248893 100644 --- a/cmd/util/ledger/migrations/staged_contracts_migration_test.go +++ b/cmd/util/ledger/migrations/staged_contracts_migration_test.go @@ -763,7 +763,7 @@ func TestStagedContractsMigration(t *testing.T) { require.NoError(t, err) require.Len(t, logWriter.logs, 4) - require.Contains(t, logWriter.logs[0], "found 6 registers in account 0x2ceae959ed1a7e7a") + require.Contains(t, logWriter.logs[0], "found 4 registers in account 0x2ceae959ed1a7e7a") require.Contains(t, logWriter.logs[1], "found a value with an unexpected type `String`") require.Contains(t, logWriter.logs[2], "found 1 staged contracts from payloads") require.Contains(t, logWriter.logs[3], "total of 1 unique contracts are staged for all accounts") diff --git a/cmd/util/ledger/migrations/utils.go b/cmd/util/ledger/migrations/utils.go index e5f20d23417..7b860fc962b 100644 --- a/cmd/util/ledger/migrations/utils.go +++ b/cmd/util/ledger/migrations/utils.go @@ -31,8 +31,8 @@ func init() { } } -func getSlabIDsFromRegisters(registers registers.Registers) ([]atree.StorageID, error) { - storageIDs := make([]atree.StorageID, 0, registers.Count()) +func getSlabIDsFromRegisters(registers registers.Registers) ([]atree.SlabID, error) { + storageIDs := make([]atree.SlabID, 0, registers.Count()) err := registers.ForEach(func(owner string, key string, value []byte) error { @@ -40,12 +40,12 @@ func getSlabIDsFromRegisters(registers registers.Registers) ([]atree.StorageID, return nil } - storageID := atree.NewStorageID( + slabID := atree.NewSlabID( atree.Address([]byte(owner)), - atree.StorageIndex([]byte(key[1:])), + atree.SlabIndex([]byte(key[1:])), ) - storageIDs = append(storageIDs, storageID) + storageIDs = append(storageIDs, slabID) return nil }) diff --git a/cmd/util/ledger/util/migration_runtime_interface.go b/cmd/util/ledger/util/migration_runtime_interface.go index 75daf3b694f..eea051b625a 100644 --- a/cmd/util/ledger/util/migration_runtime_interface.go +++ b/cmd/util/ledger/util/migration_runtime_interface.go @@ -307,8 +307,8 @@ func (m *MigrationRuntimeInterface) GetAccountContractNames(_ runtime.Address) ( panic("unexpected GetAccountContractNames call") } -func (m *MigrationRuntimeInterface) AllocateStorageIndex(_ []byte) (atree.StorageIndex, error) { - panic("unexpected AllocateStorageIndex call") +func (m *MigrationRuntimeInterface) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { + panic("unexpected AllocateSlabIndex call") } func (m *MigrationRuntimeInterface) ComputationUsed() (uint64, error) { diff --git a/cmd/util/ledger/util/registers/registers.go b/cmd/util/ledger/util/registers/registers.go index ab2e8dd80ec..33c733bd77f 100644 --- a/cmd/util/ledger/util/registers/registers.go +++ b/cmd/util/ledger/util/registers/registers.go @@ -348,7 +348,7 @@ func (l ReadOnlyLedger) SetValue(_, _, _ []byte) error { panic("unexpected call of SetValue") } -func (l ReadOnlyLedger) AllocateStorageIndex(_ []byte) (atree.StorageIndex, error) { +func (l ReadOnlyLedger) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { panic("unexpected call of AllocateStorageIndex") } diff --git a/cmd/util/ledger/util/util.go b/cmd/util/ledger/util/util.go index 8b78b65b171..2afb36eb59e 100644 --- a/cmd/util/ledger/util/util.go +++ b/cmd/util/ledger/util/util.go @@ -64,11 +64,11 @@ func (a *AccountsAtreeLedger) ValueExists(owner, key []byte) (exists bool, err e return len(v) > 0, nil } -// AllocateStorageIndex allocates new storage index under the owner accounts to store a new register -func (a *AccountsAtreeLedger) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { - v, err := a.Accounts.AllocateStorageIndex(flow.BytesToAddress(owner)) +// AllocateSlabIndex allocates new storage index under the owner accounts to store a new register +func (a *AccountsAtreeLedger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + v, err := a.Accounts.AllocateSlabIndex(flow.BytesToAddress(owner)) if err != nil { - return atree.StorageIndex{}, fmt.Errorf("storage index allocation failed: %w", err) + return atree.SlabIndex{}, fmt.Errorf("storage index allocation failed: %w", err) } return v, nil } @@ -205,7 +205,7 @@ type PayloadMetaInfo struct { type PayloadsLedger struct { Payloads map[flow.RegisterID]*ledger.Payload - AllocateStorageIndexFunc func(owner []byte) (atree.StorageIndex, error) + AllocateSlabIndexFunc func(owner []byte) (atree.SlabIndex, error) } var _ atree.Ledger = &PayloadsLedger{} @@ -238,9 +238,9 @@ func (p *PayloadsLedger) ValueExists(owner, key []byte) (exists bool, err error) return ok, nil } -func (p *PayloadsLedger) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { - if p.AllocateStorageIndexFunc != nil { - return p.AllocateStorageIndexFunc(owner) +func (p *PayloadsLedger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + if p.AllocateSlabIndexFunc != nil { + return p.AllocateSlabIndexFunc(owner) } panic("AllocateStorageIndex not expected to be called") diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index 4f88f874675..3924b04ddbe 100644 --- a/engine/execution/state/bootstrap/bootstrap_test.go +++ b/engine/execution/state/bootstrap/bootstrap_test.go @@ -53,7 +53,7 @@ func TestBootstrapLedger(t *testing.T) { } func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) { - expectedStateCommitmentBytes, _ := hex.DecodeString("aff1aafa7a34803d7b545791e60b7ffbcae52be602cef514430170969652d050") + expectedStateCommitmentBytes, _ := hex.DecodeString("e62e7b626a97a82a1569795bbdec21d68dd28aab1e493093fc2e95b9ea3a58e3") expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes) require.NoError(t, err) diff --git a/fvm/accounts_test.go b/fvm/accounts_test.go index 33ff306d0ca..d86732d7e1c 100644 --- a/fvm/accounts_test.go +++ b/fvm/accounts_test.go @@ -1565,7 +1565,7 @@ func TestAccountBalanceFields(t *testing.T) { _, output, err = vm.Run(ctx, script, snapshotTree) assert.NoError(t, err) assert.NoError(t, output.Err) - assert.Equal(t, cadence.UFix64(99_989_590), output.Value) + assert.Equal(t, cadence.UFix64(99_990_950), output.Value) }), ) diff --git a/fvm/environment/account_key_updater_test.go b/fvm/environment/account_key_updater_test.go index bfb2fa9d2c7..2979d872d7f 100644 --- a/fvm/environment/account_key_updater_test.go +++ b/fvm/environment/account_key_updater_test.go @@ -174,8 +174,8 @@ func (f FakeAccounts) Create(_ []flow.AccountPublicKey, _ flow.Address) error { func (f FakeAccounts) GetValue(_ flow.RegisterID) (flow.RegisterValue, error) { return nil, nil } func (f FakeAccounts) GetStorageUsed(_ flow.Address) (uint64, error) { return 0, nil } func (f FakeAccounts) SetValue(_ flow.RegisterID, _ []byte) error { return nil } -func (f FakeAccounts) AllocateStorageIndex(_ flow.Address) (atree.StorageIndex, error) { - return atree.StorageIndex{}, nil +func (f FakeAccounts) AllocateSlabIndex(_ flow.Address) (atree.SlabIndex, error) { + return atree.SlabIndex{}, nil } func (f FakeAccounts) GenerateAccountLocalID(address flow.Address) (uint64, error) { return 0, nil diff --git a/fvm/environment/accounts.go b/fvm/environment/accounts.go index 01041f19a3f..c1342826611 100644 --- a/fvm/environment/accounts.go +++ b/fvm/environment/accounts.go @@ -36,7 +36,7 @@ type Accounts interface { GetValue(id flow.RegisterID) (flow.RegisterValue, error) GetStorageUsed(address flow.Address) (uint64, error) SetValue(id flow.RegisterID, value flow.RegisterValue) error - AllocateStorageIndex(address flow.Address) (atree.StorageIndex, error) + AllocateSlabIndex(address flow.Address) (atree.SlabIndex, error) GenerateAccountLocalID(address flow.Address) (uint64, error) } @@ -52,16 +52,16 @@ func NewAccounts(txnState state.NestedTransactionPreparer) *StatefulAccounts { } } -func (a *StatefulAccounts) AllocateStorageIndex( +func (a *StatefulAccounts) AllocateSlabIndex( address flow.Address, ) ( - atree.StorageIndex, + atree.SlabIndex, error, ) { // get status status, err := a.getAccountStatus(address) if err != nil { - return atree.StorageIndex{}, err + return atree.SlabIndex{}, err } // get and increment the index @@ -79,7 +79,7 @@ func (a *StatefulAccounts) AllocateStorageIndex( []byte{}) }) if err != nil { - return atree.StorageIndex{}, fmt.Errorf( + return atree.SlabIndex{}, fmt.Errorf( "failed to allocate an storage index: %w", err) } @@ -88,7 +88,7 @@ func (a *StatefulAccounts) AllocateStorageIndex( status.SetStorageIndex(newIndexBytes) err = a.setAccountStatus(address, status) if err != nil { - return atree.StorageIndex{}, fmt.Errorf( + return atree.SlabIndex{}, fmt.Errorf( "failed to allocate an storage index: %w", err) } @@ -431,6 +431,20 @@ func (a *StatefulAccounts) setContract( return nil } +func EncodeContractNames(contractNames contractNames) ([]byte, error) { + var buf bytes.Buffer + cborEncoder := cbor.NewEncoder(&buf) + err := cborEncoder.Encode(contractNames) + if err != nil { + return nil, errors.NewEncodingFailuref( + err, + "cannot encode contract names: %s", + contractNames, + ) + } + return buf.Bytes(), nil +} + func (a *StatefulAccounts) setContractNames( contractNames contractNames, address flow.Address, @@ -443,16 +457,11 @@ func (a *StatefulAccounts) setContractNames( if !ok { return errors.NewAccountNotFoundError(address) } - var buf bytes.Buffer - cborEncoder := cbor.NewEncoder(&buf) - err = cborEncoder.Encode(contractNames) + + newContractNames, err := EncodeContractNames(contractNames) if err != nil { - return errors.NewEncodingFailuref( - err, - "cannot encode contract names: %s", - contractNames) + return err } - newContractNames := buf.Bytes() id := flow.ContractNamesRegisterID(address) prevContractNames, err := a.GetValue(id) @@ -607,20 +616,26 @@ func (a *StatefulAccounts) getContractNames( error, ) { // TODO return fatal error if can't fetch - encContractNames, err := a.GetValue(flow.ContractNamesRegisterID(address)) + encodedContractNames, err := a.GetValue(flow.ContractNamesRegisterID(address)) if err != nil { return nil, fmt.Errorf("cannot get deployed contract names: %w", err) } + + return DecodeContractNames(encodedContractNames) +} + +func DecodeContractNames(encodedContractNames []byte) ([]string, error) { identifiers := make([]string, 0) - if len(encContractNames) > 0 { - buf := bytes.NewReader(encContractNames) + if len(encodedContractNames) > 0 { + buf := bytes.NewReader(encodedContractNames) cborDecoder := cbor.NewDecoder(buf) - err = cborDecoder.Decode(&identifiers) + err := cborDecoder.Decode(&identifiers) if err != nil { return nil, fmt.Errorf( "cannot decode deployed contract names %x: %w", - encContractNames, - err) + encodedContractNames, + err, + ) } } return identifiers, nil diff --git a/fvm/environment/accounts_status.go b/fvm/environment/accounts_status.go index a420051550f..93c9a81db6e 100644 --- a/fvm/environment/accounts_status.go +++ b/fvm/environment/accounts_status.go @@ -105,13 +105,13 @@ func (a *AccountStatus) StorageUsed() uint64 { } // SetStorageIndex updates the storage index of the account -func (a *AccountStatus) SetStorageIndex(index atree.StorageIndex) { +func (a *AccountStatus) SetStorageIndex(index atree.SlabIndex) { copy(a[storageIndexStartIndex:storageIndexStartIndex+storageIndexSize], index[:storageIndexSize]) } // StorageIndex returns the storage index of the account -func (a *AccountStatus) StorageIndex() atree.StorageIndex { - var index atree.StorageIndex +func (a *AccountStatus) StorageIndex() atree.SlabIndex { + var index atree.SlabIndex copy(index[:], a[storageIndexStartIndex:storageIndexStartIndex+storageIndexSize]) return index } diff --git a/fvm/environment/accounts_status_test.go b/fvm/environment/accounts_status_test.go index 543ee2b05f1..4ab3c9c1ee5 100644 --- a/fvm/environment/accounts_status_test.go +++ b/fvm/environment/accounts_status_test.go @@ -15,7 +15,7 @@ func TestAccountStatus(t *testing.T) { s := environment.NewAccountStatus() t.Run("test setting values", func(t *testing.T) { - index := atree.StorageIndex{1, 2, 3, 4, 5, 6, 7, 8} + index := atree.SlabIndex{1, 2, 3, 4, 5, 6, 7, 8} s.SetStorageIndex(index) s.SetPublicKeyCount(34) s.SetStorageUsed(56) @@ -58,7 +58,7 @@ func TestAccountStatus(t *testing.T) { migrated, err := environment.AccountStatusFromBytes(oldBytes) require.NoError(t, err) - require.Equal(t, atree.StorageIndex{0, 0, 0, 0, 0, 0, 0, 6}, migrated.StorageIndex()) + require.Equal(t, atree.SlabIndex{0, 0, 0, 0, 0, 0, 0, 6}, migrated.StorageIndex()) require.Equal(t, uint64(5), migrated.PublicKeyCount()) require.Equal(t, uint64(7)+increaseInSize, migrated.StorageUsed()) require.Equal(t, uint64(0), migrated.AccountIdCounter()) diff --git a/fvm/environment/accounts_test.go b/fvm/environment/accounts_test.go index c6ef3cce467..aa44f0b9c2c 100644 --- a/fvm/environment/accounts_test.go +++ b/fvm/environment/accounts_test.go @@ -422,17 +422,17 @@ func TestAccounts_AllocateStorageIndex(t *testing.T) { require.NoError(t, err) // no register set case - i, err := accounts.AllocateStorageIndex(address) + i, err := accounts.AllocateSlabIndex(address) require.NoError(t, err) - require.Equal(t, i, atree.StorageIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 1})) + require.Equal(t, i, atree.SlabIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 1})) // register already set case - i, err = accounts.AllocateStorageIndex(address) + i, err = accounts.AllocateSlabIndex(address) require.NoError(t, err) - require.Equal(t, i, atree.StorageIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 2})) + require.Equal(t, i, atree.SlabIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 2})) // register update successful - i, err = accounts.AllocateStorageIndex(address) + i, err = accounts.AllocateSlabIndex(address) require.NoError(t, err) - require.Equal(t, i, atree.StorageIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 3})) + require.Equal(t, i, atree.SlabIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 3})) } diff --git a/fvm/environment/mock/accounts.go b/fvm/environment/mock/accounts.go index 66c912c5f35..83c42d5955e 100644 --- a/fvm/environment/mock/accounts.go +++ b/fvm/environment/mock/accounts.go @@ -15,24 +15,24 @@ type Accounts struct { mock.Mock } -// AllocateStorageIndex provides a mock function with given fields: address -func (_m *Accounts) AllocateStorageIndex(address flow.Address) (atree.StorageIndex, error) { +// AllocateSlabIndex provides a mock function with given fields: address +func (_m *Accounts) AllocateSlabIndex(address flow.Address) (atree.SlabIndex, error) { ret := _m.Called(address) if len(ret) == 0 { - panic("no return value specified for AllocateStorageIndex") + panic("no return value specified for AllocateSlabIndex") } - var r0 atree.StorageIndex + var r0 atree.SlabIndex var r1 error - if rf, ok := ret.Get(0).(func(flow.Address) (atree.StorageIndex, error)); ok { + if rf, ok := ret.Get(0).(func(flow.Address) (atree.SlabIndex, error)); ok { return rf(address) } - if rf, ok := ret.Get(0).(func(flow.Address) atree.StorageIndex); ok { + if rf, ok := ret.Get(0).(func(flow.Address) atree.SlabIndex); ok { r0 = rf(address) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(atree.StorageIndex) + r0 = ret.Get(0).(atree.SlabIndex) } } diff --git a/fvm/environment/mock/environment.go b/fvm/environment/mock/environment.go index 5f4e3ca81e6..a15f8774d32 100644 --- a/fvm/environment/mock/environment.go +++ b/fvm/environment/mock/environment.go @@ -132,24 +132,24 @@ func (_m *Environment) AddAccountKey(address common.Address, publicKey *stdlib.P return r0, r1 } -// AllocateStorageIndex provides a mock function with given fields: owner -func (_m *Environment) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { +// AllocateSlabIndex provides a mock function with given fields: owner +func (_m *Environment) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { ret := _m.Called(owner) if len(ret) == 0 { - panic("no return value specified for AllocateStorageIndex") + panic("no return value specified for AllocateSlabIndex") } - var r0 atree.StorageIndex + var r0 atree.SlabIndex var r1 error - if rf, ok := ret.Get(0).(func([]byte) (atree.StorageIndex, error)); ok { + if rf, ok := ret.Get(0).(func([]byte) (atree.SlabIndex, error)); ok { return rf(owner) } - if rf, ok := ret.Get(0).(func([]byte) atree.StorageIndex); ok { + if rf, ok := ret.Get(0).(func([]byte) atree.SlabIndex); ok { r0 = rf(owner) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(atree.StorageIndex) + r0 = ret.Get(0).(atree.SlabIndex) } } diff --git a/fvm/environment/mock/value_store.go b/fvm/environment/mock/value_store.go index 1473ef43f8e..8790ea2653f 100644 --- a/fvm/environment/mock/value_store.go +++ b/fvm/environment/mock/value_store.go @@ -13,24 +13,24 @@ type ValueStore struct { mock.Mock } -// AllocateStorageIndex provides a mock function with given fields: owner -func (_m *ValueStore) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { +// AllocateSlabIndex provides a mock function with given fields: owner +func (_m *ValueStore) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { ret := _m.Called(owner) if len(ret) == 0 { - panic("no return value specified for AllocateStorageIndex") + panic("no return value specified for AllocateSlabIndex") } - var r0 atree.StorageIndex + var r0 atree.SlabIndex var r1 error - if rf, ok := ret.Get(0).(func([]byte) (atree.StorageIndex, error)); ok { + if rf, ok := ret.Get(0).(func([]byte) (atree.SlabIndex, error)); ok { return rf(owner) } - if rf, ok := ret.Get(0).(func([]byte) atree.StorageIndex); ok { + if rf, ok := ret.Get(0).(func([]byte) atree.SlabIndex); ok { r0 = rf(owner) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(atree.StorageIndex) + r0 = ret.Get(0).(atree.SlabIndex) } } diff --git a/fvm/environment/value_store.go b/fvm/environment/value_store.go index 8113de6762c..4f768378500 100644 --- a/fvm/environment/value_store.go +++ b/fvm/environment/value_store.go @@ -20,7 +20,7 @@ type ValueStore interface { ValueExists(owner []byte, key []byte) (bool, error) - AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) + AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) } type ParseRestrictedValueStore struct { @@ -82,16 +82,16 @@ func (store ParseRestrictedValueStore) ValueExists( key) } -func (store ParseRestrictedValueStore) AllocateStorageIndex( +func (store ParseRestrictedValueStore) AllocateSlabIndex( owner []byte, ) ( - atree.StorageIndex, + atree.SlabIndex, error, ) { return parseRestrict1Arg1Ret( store.txnState, trace.FVMEnvAllocateStorageIndex, - store.impl.AllocateStorageIndex, + store.impl.AllocateSlabIndex, owner) } @@ -189,26 +189,26 @@ func (store *valueStore) ValueExists( return len(v) > 0, nil } -// AllocateStorageIndex allocates new storage index under the owner accounts +// AllocateSlabIndex allocates new storage index under the owner accounts // to store a new register. -func (store *valueStore) AllocateStorageIndex( +func (store *valueStore) AllocateSlabIndex( owner []byte, ) ( - atree.StorageIndex, + atree.SlabIndex, error, ) { defer store.tracer.StartChildSpan(trace.FVMEnvAllocateStorageIndex).End() err := store.meter.MeterComputation(ComputationKindAllocateStorageIndex, 1) if err != nil { - return atree.StorageIndex{}, fmt.Errorf( + return atree.SlabIndex{}, fmt.Errorf( "allocate storage index failed: %w", err) } - v, err := store.accounts.AllocateStorageIndex(flow.BytesToAddress(owner)) + v, err := store.accounts.AllocateSlabIndex(flow.BytesToAddress(owner)) if err != nil { - return atree.StorageIndex{}, fmt.Errorf( + return atree.SlabIndex{}, fmt.Errorf( "storage address allocation failed: %w", err) } diff --git a/fvm/evm/backends/wrappedEnv.go b/fvm/evm/backends/wrappedEnv.go index d22aabc191c..152d54236ab 100644 --- a/fvm/evm/backends/wrappedEnv.go +++ b/fvm/evm/backends/wrappedEnv.go @@ -40,8 +40,8 @@ func (we *WrappedEnvironment) ValueExists(owner, key []byte) (bool, error) { return b, handleEnvironmentError(err) } -func (we *WrappedEnvironment) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { - index, err := we.env.AllocateStorageIndex(owner) +func (we *WrappedEnvironment) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + index, err := we.env.AllocateSlabIndex(owner) return index, handleEnvironmentError(err) } diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index ee06deb3d01..b86821e9f7f 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -234,7 +234,17 @@ func (bl *BlockView) DryRunTransaction( msg.SkipAccountChecks = true // return without commiting the state - return proc.run(msg, tx.Hash(), 0, tx.Type()) + txResult, err := proc.run(msg, tx.Hash(), 0, tx.Type()) + if txResult.Successful() { + // Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition: + // https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32 + txResult.GasConsumed += gethParams.SstoreSentryGasEIP2200 + // Take into account any gas refunds, which are calculated only after + // transaction execution. + txResult.GasConsumed += txResult.GasRefund + } + + return txResult, err } func (bl *BlockView) newProcedure() (*procedure, error) { @@ -522,6 +532,7 @@ func (proc *procedure) run( // if prechecks are passed, the exec result won't be nil if execResult != nil { res.GasConsumed = execResult.UsedGas + res.GasRefund = proc.state.GetRefund() res.Index = uint16(txIndex) // we need to capture the returned value no matter the status // if the tx is reverted the error message is returned as returned value diff --git a/fvm/evm/emulator/state/collection.go b/fvm/evm/emulator/state/collection.go index 780d81652df..482960370c5 100644 --- a/fvm/evm/emulator/state/collection.go +++ b/fvm/evm/emulator/state/collection.go @@ -47,16 +47,22 @@ func NewCollectionProvider( // calling twice for the same collection might result in odd-behaviours // currently collection provider doesn't do any internal caching to protect aginast these cases func (cp *CollectionProvider) CollectionByID(collectionID []byte) (*Collection, error) { - storageID, err := atree.NewStorageIDFromRawBytes(collectionID) + slabID, err := atree.NewSlabIDFromRawBytes(collectionID) if err != nil { return nil, err } + + // TODO: expose SlabID.Address() in atree + + var address atree.Address + binary.BigEndian.PutUint64(address[:], slabID.AddressAsUint64()) + // sanity check the storage ID address - if storageID.Address != cp.rootAddr { - return nil, fmt.Errorf("root address mismatch %x != %x", storageID.Address, cp.rootAddr) + if address != cp.rootAddr { + return nil, fmt.Errorf("root address mismatch %x != %x", address, cp.rootAddr) } - omap, err := atree.NewMapWithRootID(cp.storage, storageID, atree.NewDefaultDigesterBuilder()) + omap, err := atree.NewMapWithRootID(cp.storage, slabID, atree.NewDefaultDigesterBuilder()) if err != nil { return nil, err } @@ -74,7 +80,7 @@ func (cp *CollectionProvider) NewCollection() (*Collection, error) { return nil, err } storageIDBytes := make([]byte, storageIDSize) - _, err = omap.StorageID().ToRawBytes(storageIDBytes) + _, err = omap.SlabID().ToRawBytes(storageIDBytes) if err != nil { return nil, err } @@ -110,7 +116,7 @@ func (c *Collection) CollectionID() []byte { // // if key doesn't exist it returns nil (no error) func (c *Collection) Get(key []byte) ([]byte, error) { - data, err := c.omap.Get(compare, hashInputProvider, NewByteStringValue(key)) + value, err := c.omap.Get(compare, hashInputProvider, NewByteStringValue(key)) if err != nil { var keyNotFoundError *atree.KeyNotFoundError if errors.As(err, &keyNotFoundError) { @@ -119,11 +125,6 @@ func (c *Collection) Get(key []byte) ([]byte, error) { return nil, err } - value, err := data.StoredValue(c.omap.Storage) - if err != nil { - return nil, err - } - return value.(ByteStringValue).Bytes(), nil } @@ -136,9 +137,9 @@ func (c *Collection) Set(key, value []byte) error { return err } - if id, ok := existingValueStorable.(atree.StorageIDStorable); ok { + if id, ok := existingValueStorable.(atree.SlabIDStorable); ok { // NOTE: deep remove isn't necessary because value is ByteStringValue (not container) - err := c.storage.Remove(atree.StorageID(id)) + err := c.storage.Remove(atree.SlabID(id)) if err != nil { return err } @@ -159,9 +160,9 @@ func (c *Collection) Remove(key []byte) error { return err } - if id, ok := existingValueStorable.(atree.StorageIDStorable); ok { + if id, ok := existingValueStorable.(atree.SlabIDStorable); ok { // NOTE: deep remove isn't necessary because value is ByteStringValue (not container) - err := c.storage.Remove(atree.StorageID(id)) + err := c.storage.Remove(atree.SlabID(id)) if err != nil { return err } @@ -175,8 +176,8 @@ func (c *Collection) Destroy() ([][]byte, error) { keys := make([][]byte, c.omap.Count()) i := 0 err := c.omap.PopIterate(func(keyStorable atree.Storable, valueStorable atree.Storable) { - if id, ok := valueStorable.(atree.StorageIDStorable); ok { - err := c.storage.Remove(atree.StorageID(id)) + if id, ok := valueStorable.(atree.SlabIDStorable); ok { + err := c.storage.Remove(atree.SlabID(id)) if err != nil && cachedErr == nil { cachedErr = err } @@ -194,7 +195,7 @@ func (c *Collection) Destroy() ([][]byte, error) { if err != nil { return keys, err } - return keys, c.storage.Remove(c.omap.StorageID()) + return keys, c.storage.Remove(c.omap.SlabID()) } // Size returns the number of items in the collection @@ -229,24 +230,7 @@ func (v ByteStringValue) Storable(storage atree.SlabStorage, address atree.Addre } // Create StorableSlab - id, err := storage.GenerateStorageID(address) - if err != nil { - return nil, err - } - - slab := &atree.StorableSlab{ - StorageID: id, - Storable: v, - } - - // Store StorableSlab in storage - err = storage.Store(id, slab) - if err != nil { - return nil, err - } - - // Return storage id as storable - return atree.StorageIDStorable(id), nil + return atree.NewStorableSlab(storage, address, v) } func (v ByteStringValue) Encode(enc *atree.Encoder) error { @@ -311,7 +295,7 @@ func (v ByteStringValue) Bytes() []byte { return v.data } -func decodeStorable(dec *cbor.StreamDecoder, _ atree.StorageID) (atree.Storable, error) { +func decodeStorable(dec *cbor.StreamDecoder, slabID atree.SlabID, inlinedExtraData []atree.ExtraData) (atree.Storable, error) { t, err := dec.NextType() if err != nil { return nil, err @@ -333,8 +317,31 @@ func decodeStorable(dec *cbor.StreamDecoder, _ atree.StorageID) (atree.Storable, switch tagNumber { - case atree.CBORTagStorageID: - return atree.DecodeStorageIDStorable(dec) + case atree.CBORTagSlabID: + return atree.DecodeSlabIDStorable(dec) + + case atree.CBORTagInlinedArray: + return atree.DecodeInlinedArrayStorable( + dec, + decodeStorable, + slabID, + inlinedExtraData) + + case atree.CBORTagInlinedMap: + return atree.DecodeInlinedMapStorable( + dec, + decodeStorable, + slabID, + inlinedExtraData, + ) + + case atree.CBORTagInlinedCompactMap: + return atree.DecodeInlinedCompactMapStorable( + dec, + decodeStorable, + slabID, + inlinedExtraData, + ) default: return nil, fmt.Errorf("invalid tag number %d", tagNumber) @@ -403,6 +410,18 @@ type emptyTypeInfo struct{} var _ atree.TypeInfo = emptyTypeInfo{} +func (emptyTypeInfo) IsComposite() bool { + return false +} + +func (emptyTypeInfo) Identifier() string { + return "" +} + +func (e emptyTypeInfo) Copy() atree.TypeInfo { + return e +} + func (emptyTypeInfo) Encode(e *cbor.StreamEncoder) error { return e.EncodeNil() } diff --git a/fvm/evm/emulator/state/stateDB_test.go b/fvm/evm/emulator/state/stateDB_test.go index 5d526fc4ae7..bc4d620f464 100644 --- a/fvm/evm/emulator/state/stateDB_test.go +++ b/fvm/evm/emulator/state/stateDB_test.go @@ -247,8 +247,8 @@ func TestStateDB(t *testing.T) { SetValueFunc: func(owner, key, value []byte) error { return atree.NewUserError(fmt.Errorf("key not found")) }, - AllocateStorageIndexFunc: func(owner []byte) (atree.StorageIndex, error) { - return atree.StorageIndex{}, nil + AllocateStorageIndexFunc: func(owner []byte) (atree.SlabIndex, error) { + return atree.SlabIndex{}, nil }, } db, err := state.NewStateDB(ledger, rootAddr) @@ -271,8 +271,8 @@ func TestStateDB(t *testing.T) { SetValueFunc: func(owner, key, value []byte) error { return atree.NewFatalError(fmt.Errorf("key not found")) }, - AllocateStorageIndexFunc: func(owner []byte) (atree.StorageIndex, error) { - return atree.StorageIndex{}, nil + AllocateStorageIndexFunc: func(owner []byte) (atree.SlabIndex, error) { + return atree.SlabIndex{}, nil }, } db, err := state.NewStateDB(ledger, rootAddr) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 12cd3994b24..3346238a30a 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -12,6 +12,7 @@ import ( "github.com/onflow/cadence/encoding/ccf" gethTypes "github.com/onflow/go-ethereum/core/types" + gethParams "github.com/onflow/go-ethereum/params" "github.com/onflow/go-ethereum/rlp" "github.com/stretchr/testify/assert" @@ -1462,6 +1463,349 @@ func TestDryRun(t *testing.T) { }) }) + t.Run("test dry run store current value", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + data := testContract.MakeCallData(t, "store", big.NewInt(0)) + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + data, + ) + dryRunResult := dryRunTx(t, tx, ctx, vm, snapshot, testContract) + + require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode) + require.Equal(t, types.StatusSuccessful, dryRunResult.Status) + require.Greater(t, dryRunResult.GasConsumed, uint64(0)) + + code := []byte(fmt.Sprintf( + ` + import EVM from %s + access(all) + fun main(tx: [UInt8], coinbaseBytes: [UInt8; 20]): EVM.Result { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + return EVM.run(tx: tx, coinbase: coinbase) + } + `, + evmAddress, + )) + + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + data, + big.NewInt(0), + dryRunResult.GasConsumed, // use the gas estimation from Evm.dryRun + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + script := fvm.Script(code).WithArguments( + json.MustEncode(innerTx), + json.MustEncode(coinbase), + ) + + _, output, err := vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + res, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + require.Equal(t, types.StatusSuccessful, res.Status) + require.Equal(t, types.ErrCodeNoError, res.ErrorCode) + // Make sure that gas consumed from `EVM.dryRun` is bigger + // than the actual gas consumption of the equivalent + // `EVM.run`. + require.Equal( + t, + res.GasConsumed+gethParams.SstoreSentryGasEIP2200, + dryRunResult.GasConsumed, + ) + }) + }) + + t.Run("test dry run store new value", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){ + prepare(account: &Account) { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + let res = EVM.run(tx: tx, coinbase: coinbase) + + assert(res.status == EVM.Status.successful, message: "unexpected status") + assert(res.errorCode == 0, message: "unexpected error code") + } + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + num := int64(12) + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "store", big.NewInt(num)), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + tx := fvm.Transaction( + flow.NewTransactionBody(). + SetScript(code). + AddAuthorizer(sc.FlowServiceAccount.Address). + AddArgument(json.MustEncode(innerTx)). + AddArgument(json.MustEncode(coinbase)), + 0) + + _, output, err := vm.Run( + ctx, + tx, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + + data := testContract.MakeCallData(t, "store", big.NewInt(100)) + tx1 := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + data, + ) + dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot, testContract) + + require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode) + require.Equal(t, types.StatusSuccessful, dryRunResult.Status) + require.Greater(t, dryRunResult.GasConsumed, uint64(0)) + + code = []byte(fmt.Sprintf( + ` + import EVM from %s + access(all) + fun main(tx: [UInt8], coinbaseBytes: [UInt8; 20]): EVM.Result { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + return EVM.run(tx: tx, coinbase: coinbase) + } + `, + evmAddress, + )) + + // Decrease nonce because we are Cadence using scripts, and not + // transactions, which means that no state change is happening. + testAccount.SetNonce(testAccount.Nonce() - 1) + innerTxBytes = testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + data, + big.NewInt(0), + dryRunResult.GasConsumed, // use the gas estimation from Evm.dryRun + big.NewInt(0), + ) + + innerTx = cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase = cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + script := fvm.Script(code).WithArguments( + json.MustEncode(innerTx), + json.MustEncode(coinbase), + ) + + _, output, err = vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + res, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + require.Equal(t, types.StatusSuccessful, res.Status) + require.Equal(t, types.ErrCodeNoError, res.ErrorCode) + // Make sure that gas consumed from `EVM.dryRun` is bigger + // than the actual gas consumption of the equivalent + // `EVM.run`. + require.Equal( + t, + res.GasConsumed+gethParams.SstoreSentryGasEIP2200, + dryRunResult.GasConsumed, + ) + }) + }) + + t.Run("test dry run clear current value", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){ + prepare(account: &Account) { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + let res = EVM.run(tx: tx, coinbase: coinbase) + + assert(res.status == EVM.Status.successful, message: "unexpected status") + assert(res.errorCode == 0, message: "unexpected error code") + } + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + num := int64(100) + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "store", big.NewInt(num)), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + tx := fvm.Transaction( + flow.NewTransactionBody(). + SetScript(code). + AddAuthorizer(sc.FlowServiceAccount.Address). + AddArgument(json.MustEncode(innerTx)). + AddArgument(json.MustEncode(coinbase)), + 0) + + state, output, err := vm.Run( + ctx, + tx, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + snapshot = snapshot.Append(state) + + data := testContract.MakeCallData(t, "store", big.NewInt(0)) + tx1 := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + data, + ) + dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot, testContract) + + require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode) + require.Equal(t, types.StatusSuccessful, dryRunResult.Status) + require.Greater(t, dryRunResult.GasConsumed, uint64(0)) + + code = []byte(fmt.Sprintf( + ` + import EVM from %s + access(all) + fun main(tx: [UInt8], coinbaseBytes: [UInt8; 20]): EVM.Result { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + return EVM.run(tx: tx, coinbase: coinbase) + } + `, + evmAddress, + )) + + innerTxBytes = testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + data, + big.NewInt(0), + dryRunResult.GasConsumed, // use the gas estimation from Evm.dryRun + big.NewInt(0), + ) + + innerTx = cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase = cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + script := fvm.Script(code).WithArguments( + json.MustEncode(innerTx), + json.MustEncode(coinbase), + ) + + _, output, err = vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + res, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + //require.Equal(t, types.StatusSuccessful, res.Status) + require.Equal(t, types.ErrCodeNoError, res.ErrorCode) + // Make sure that gas consumed from `EVM.dryRun` is bigger + // than the actual gas consumption of the equivalent + // `EVM.run`. + require.Equal( + t, + res.GasConsumed+gethParams.SstoreSentryGasEIP2200+gethParams.SstoreClearsScheduleRefundEIP3529, + dryRunResult.GasConsumed, + ) + }) + }) + // this test makes sure the dry-run that updates the value on the contract // doesn't persist the change, and after when the value is read it isn't updated. t.Run("test dry run for any side-effects", func(t *testing.T) { diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index 62947b9fa5c..ededac7781d 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -50,6 +50,8 @@ contract EVM { payload: String, // code indicating a specific validation (201-300) or execution (301-400) error errorCode: UInt16, + // a human-readable message about the error (if any) + errorMessage: String, // the amount of gas transaction used gasConsumed: UInt64, // if transaction was a deployment contains a newly deployed contract address @@ -59,7 +61,14 @@ contract EVM { // block height in which transaction was inclued blockHeight: UInt64, // block hash in which transaction was included - blockHash: String + blockHash: String, + /// captures the hex encoded data that is returned from + /// the evm. For contract deployments + /// it returns the code deployed to + /// the address provided in the contractAddress field. + /// in case of revert, the smart contract custom error message + /// is also returned here (see EIP-140 for more details). + returnedData: String ) access(all) diff --git a/fvm/evm/testutils/backend.go b/fvm/evm/testutils/backend.go index 472d38201f4..9fe6a54a992 100644 --- a/fvm/evm/testutils/backend.go +++ b/fvm/evm/testutils/backend.go @@ -80,7 +80,7 @@ func GetSimpleValueStore() *TestValueStore { bytesRead += len(fk) + len(value) return len(value) > 0, nil }, - AllocateStorageIndexFunc: func(owner []byte) (atree.StorageIndex, error) { + AllocateStorageIndexFunc: func(owner []byte) (atree.SlabIndex, error) { index := allocator[string(owner)] // TODO: figure out why it result in a collision if index == 0 { @@ -91,7 +91,7 @@ func GetSimpleValueStore() *TestValueStore { binary.BigEndian.PutUint64(data[:], index) bytesRead += len(owner) + 8 bytesWritten += len(owner) + 8 - return atree.StorageIndex(data), nil + return atree.SlabIndex(data), nil }, TotalStorageSizeFunc: func() int { size := 0 @@ -211,7 +211,7 @@ type TestValueStore struct { GetValueFunc func(owner, key []byte) ([]byte, error) SetValueFunc func(owner, key, value []byte) error ValueExistsFunc func(owner, key []byte) (bool, error) - AllocateStorageIndexFunc func(owner []byte) (atree.StorageIndex, error) + AllocateStorageIndexFunc func(owner []byte) (atree.SlabIndex, error) TotalStorageSizeFunc func() int TotalBytesReadFunc func() int TotalBytesWrittenFunc func() int @@ -242,7 +242,7 @@ func (vs *TestValueStore) ValueExists(owner, key []byte) (bool, error) { return vs.ValueExistsFunc(owner, key) } -func (vs *TestValueStore) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { +func (vs *TestValueStore) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { if vs.AllocateStorageIndexFunc == nil { panic("method not set") } diff --git a/fvm/evm/testutils/cadence.go b/fvm/evm/testutils/cadence.go index 0ec8fd7ca87..8a1e9a7dd79 100644 --- a/fvm/evm/testutils/cadence.go +++ b/fvm/evm/testutils/cadence.go @@ -112,7 +112,7 @@ type TestLedger struct { OnValueExists func(owner, key []byte) (exists bool, err error) OnGetValue func(owner, key []byte) (value []byte, err error) OnSetValue func(owner, key, value []byte) (err error) - OnAllocateStorageIndex func(owner []byte) (atree.StorageIndex, error) + OnAllocateStorageIndex func(owner []byte) (atree.SlabIndex, error) } var _ atree.Ledger = TestLedger{} @@ -129,7 +129,7 @@ func (s TestLedger) ValueExists(owner, key []byte) (exists bool, err error) { return s.OnValueExists(owner, key) } -func (s TestLedger) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { +func (s TestLedger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { return s.OnAllocateStorageIndex(owner) } @@ -175,7 +175,7 @@ func NewTestLedger( } return nil }, - OnAllocateStorageIndex: func(owner []byte) (result atree.StorageIndex, err error) { + OnAllocateStorageIndex: func(owner []byte) (result atree.SlabIndex, err error) { index := storageIndices[string(owner)] + 1 storageIndices[string(owner)] = index binary.BigEndian.PutUint64(result[:], index) @@ -358,8 +358,8 @@ func (i *TestRuntimeInterface) SetValue(owner, key, value []byte) (err error) { return i.Storage.SetValue(owner, key, value) } -func (i *TestRuntimeInterface) AllocateStorageIndex(owner []byte) (atree.StorageIndex, error) { - return i.Storage.AllocateStorageIndex(owner) +func (i *TestRuntimeInterface) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + return i.Storage.AllocateSlabIndex(owner) } func (i *TestRuntimeInterface) CreateAccount(payer runtime.Address) (address runtime.Address, err error) { diff --git a/fvm/evm/types/events.go b/fvm/evm/types/events.go index 842f9f553e6..cd801a17e4d 100644 --- a/fvm/evm/types/events.go +++ b/fvm/evm/types/events.go @@ -92,13 +92,13 @@ func (p *transactionEvent) ToCadence(location common.Location) (cadence.Event, e cadence.NewField("type", cadence.UInt8Type), cadence.NewField("payload", cadence.StringType), cadence.NewField("errorCode", cadence.UInt16Type), + cadence.NewField("errorMessage", cadence.StringType), cadence.NewField("gasConsumed", cadence.UInt64Type), cadence.NewField("contractAddress", cadence.StringType), cadence.NewField("logs", cadence.StringType), cadence.NewField("blockHeight", cadence.UInt64Type), // todo we can remove hash and just reference block by height (evm-gateway dependency) cadence.NewField("blockHash", cadence.StringType), - cadence.NewField("errorMessage", cadence.StringType), cadence.NewField("returnedData", cadence.StringType), }, nil, @@ -110,12 +110,12 @@ func (p *transactionEvent) ToCadence(location common.Location) (cadence.Event, e cadence.NewUInt8(p.Result.TxType), cadence.String(hex.EncodeToString(p.Payload)), cadence.NewUInt16(uint16(p.Result.ResultSummary().ErrorCode)), + cadence.String(errorMsg), cadence.NewUInt64(p.Result.GasConsumed), deployedAddress, cadence.String(hex.EncodeToString(encodedLogs)), cadence.NewUInt64(p.BlockHeight), cadence.String(p.BlockHash.String()), - cadence.String(errorMsg), cadence.String(hex.EncodeToString(p.Result.ReturnedData)), }).WithType(eventType), nil } diff --git a/fvm/evm/types/result.go b/fvm/evm/types/result.go index ac22760464c..aba686c1a9f 100644 --- a/fvm/evm/types/result.go +++ b/fvm/evm/types/result.go @@ -39,6 +39,7 @@ type ResultSummary struct { ErrorCode ErrorCode ErrorMessage string GasConsumed uint64 + GasRefund uint64 DeployedContractAddress *Address ReturnedData Data } @@ -71,6 +72,8 @@ type Result struct { TxType uint8 // total gas consumed during an opeartion GasConsumed uint64 + // total gas refunds after transaction execution + GasRefund uint64 // the address where the contract is deployed (if any) DeployedContractAddress *Address // returned data from a function call @@ -93,6 +96,11 @@ func (res *Result) Failed() bool { return res.VMError != nil } +// Successful returns true if transaction has been executed without any errors +func (res *Result) Successful() bool { + return !res.Failed() && !res.Invalid() +} + // SetValidationError sets the validation error // and also sets the gas used to the fixed invalid gas usage func (res *Result) SetValidationError(err error) { @@ -142,6 +150,7 @@ func (res *Result) Receipt() *gethTypes.Receipt { func (res *Result) ResultSummary() *ResultSummary { rs := &ResultSummary{ GasConsumed: res.GasConsumed, + GasRefund: res.GasRefund, DeployedContractAddress: res.DeployedContractAddress, ReturnedData: res.ReturnedData, Status: StatusSuccessful, diff --git a/go.mod b/go.mod index 6f22a64920c..e8e5ae490b7 100644 --- a/go.mod +++ b/go.mod @@ -46,13 +46,13 @@ require ( github.com/multiformats/go-multiaddr v0.12.2 github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/multiformats/go-multihash v0.2.3 - github.com/onflow/atree v0.7.0-rc.2 - github.com/onflow/cadence v1.0.0-preview.33 + github.com/onflow/atree v0.8.0-rc.3 + github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34 github.com/onflow/crypto v0.25.1 github.com/onflow/flow v0.3.4 - github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 - github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 - github.com/onflow/flow-go-sdk v1.0.0-preview.35 + github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0 + github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0 + github.com/onflow/flow-go-sdk v1.0.0-preview.36 github.com/onflow/flow/protobuf/go/flow v0.4.4 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pierrec/lz4 v2.6.1+incompatible diff --git a/go.sum b/go.sum index 0883c06396a..7b574cc65f1 100644 --- a/go.sum +++ b/go.sum @@ -2166,29 +2166,29 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onflow/atree v0.6.1-0.20230711151834-86040b30171f/go.mod h1:xvP61FoOs95K7IYdIYRnNcYQGf4nbF/uuJ0tHf4DRuM= -github.com/onflow/atree v0.7.0-rc.2 h1:mZmVrl/zPlfI44EjV3FdR2QwIqT8nz1sCONUBFcML/U= -github.com/onflow/atree v0.7.0-rc.2/go.mod h1:xvP61FoOs95K7IYdIYRnNcYQGf4nbF/uuJ0tHf4DRuM= +github.com/onflow/atree v0.8.0-rc.3 h1:BHVkJLrBHhHo7ET8gkuS1+lyQGNekYYOyoICGK3RFNM= +github.com/onflow/atree v0.8.0-rc.3/go.mod h1:7YNAyCd5JENq+NzH+fR1ABUZVzbSq9dkt0+5fZH3L2A= github.com/onflow/boxo v0.0.0-20240201202436-f2477b92f483 h1:LpiQhTAfM9CAmNVEs0n//cBBgCg+vJSiIxTHYUklZ84= github.com/onflow/boxo v0.0.0-20240201202436-f2477b92f483/go.mod h1:pIZgTWdm3k3pLF9Uq6MB8JEcW07UDwNJjlXW1HELW80= github.com/onflow/cadence v1.0.0-M3/go.mod h1:odXGZZ/wGNA5mwT8bC9v8u8EXACHllB2ABSZK65TGL8= -github.com/onflow/cadence v1.0.0-preview.33 h1:kqkU+9//PRsyL3SMokeK2mStarZVxiwrGypyiOX/On8= -github.com/onflow/cadence v1.0.0-preview.33/go.mod h1:jOwvPSSLTr9TvaKMs7KKiBYMmpdpNNAFxBsjMlrqVD0= +github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34 h1:YB/7HUIsKTB6ZKCJbiAhqDI19Np+0j5zPuUUM2uMqGo= +github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34/go.mod h1:zyl2lWOLVKiKt8BoGAModCHqFEivJ44Y7Myx6NwND7o= github.com/onflow/crypto v0.25.0/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= github.com/onflow/crypto v0.25.1 h1:0txy2PKPMM873JbpxQNbJmuOJtD56bfs48RQfm0ts5A= github.com/onflow/crypto v0.25.1/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= github.com/onflow/flow v0.3.4 h1:FXUWVdYB90f/rjNcY0Owo30gL790tiYff9Pb/sycXYE= github.com/onflow/flow v0.3.4/go.mod h1:lzyAYmbu1HfkZ9cfnL5/sjrrsnJiUU8fRL26CqLP7+c= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 h1:AegPBm079X0qjneUYs+mRCpEUxSZ1lw5h4MbuXHlqn0= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= -github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 h1:za6bxPPW4JIsthhasUDTa1ruKjIO8DIhun9INQfj61Y= -github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0 h1:cq3RfBr9TnTSnsGlUHMjMGZib24Horfb1XJqMpkN5ew= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0 h1:aMFJdB2CW+Dzm+AJ5QN6J1yWh+a0l2RxHN2/TtLaXUo= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= -github.com/onflow/flow-go-sdk v1.0.0-preview.35 h1:2ptBhFYFGOaYghZTRbj51BbYqTZjkyEpXDyaWDYrHwA= -github.com/onflow/flow-go-sdk v1.0.0-preview.35/go.mod h1:/G8vtAekhvgynLYVDtd6OnhixoGTzzknmhYCJB2YWWU= +github.com/onflow/flow-go-sdk v1.0.0-preview.36 h1:3g72MjmZPEEVAbtDATbjwqKoNSB7yHLWswUHSAB5zwQ= +github.com/onflow/flow-go-sdk v1.0.0-preview.36/go.mod h1:mjkXIluC+kseYyd8Z1aTq73IiffAUeoY5fuX/C2Z+1w= github.com/onflow/flow-nft/lib/go/contracts v1.2.1 h1:woAAS5z651sDpi7ihAHll8NvRS9uFXIXkL6xR+bKFZY= github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkpBPlggIRkI53Suo0n2AyA2HcE= github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= diff --git a/insecure/go.mod b/insecure/go.mod index d4f03241831..8c652598c68 100644 --- a/insecure/go.mod +++ b/insecure/go.mod @@ -198,13 +198,13 @@ require ( github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/onflow/atree v0.7.0-rc.2 // indirect - github.com/onflow/cadence v1.0.0-preview.33 // indirect - github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 // indirect - github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 // indirect + github.com/onflow/atree v0.8.0-rc.3 // indirect + github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34 // indirect + github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0 // indirect + github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0 // indirect github.com/onflow/flow-ft/lib/go/contracts v1.0.0 // indirect github.com/onflow/flow-ft/lib/go/templates v1.0.0 // indirect - github.com/onflow/flow-go-sdk v1.0.0-preview.35 // indirect + github.com/onflow/flow-go-sdk v1.0.0-preview.36 // indirect github.com/onflow/flow-nft/lib/go/contracts v1.2.1 // indirect github.com/onflow/flow-nft/lib/go/templates v1.2.0 // indirect github.com/onflow/flow/protobuf/go/flow v0.4.4 // indirect diff --git a/insecure/go.sum b/insecure/go.sum index fec8cd1bfb7..f3e45ee059e 100644 --- a/insecure/go.sum +++ b/insecure/go.sum @@ -2157,25 +2157,25 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onflow/atree v0.6.1-0.20230711151834-86040b30171f/go.mod h1:xvP61FoOs95K7IYdIYRnNcYQGf4nbF/uuJ0tHf4DRuM= -github.com/onflow/atree v0.7.0-rc.2 h1:mZmVrl/zPlfI44EjV3FdR2QwIqT8nz1sCONUBFcML/U= -github.com/onflow/atree v0.7.0-rc.2/go.mod h1:xvP61FoOs95K7IYdIYRnNcYQGf4nbF/uuJ0tHf4DRuM= +github.com/onflow/atree v0.8.0-rc.3 h1:BHVkJLrBHhHo7ET8gkuS1+lyQGNekYYOyoICGK3RFNM= +github.com/onflow/atree v0.8.0-rc.3/go.mod h1:7YNAyCd5JENq+NzH+fR1ABUZVzbSq9dkt0+5fZH3L2A= github.com/onflow/cadence v1.0.0-M3/go.mod h1:odXGZZ/wGNA5mwT8bC9v8u8EXACHllB2ABSZK65TGL8= -github.com/onflow/cadence v1.0.0-preview.33 h1:kqkU+9//PRsyL3SMokeK2mStarZVxiwrGypyiOX/On8= -github.com/onflow/cadence v1.0.0-preview.33/go.mod h1:jOwvPSSLTr9TvaKMs7KKiBYMmpdpNNAFxBsjMlrqVD0= +github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34 h1:YB/7HUIsKTB6ZKCJbiAhqDI19Np+0j5zPuUUM2uMqGo= +github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34/go.mod h1:zyl2lWOLVKiKt8BoGAModCHqFEivJ44Y7Myx6NwND7o= github.com/onflow/crypto v0.25.0/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= github.com/onflow/crypto v0.25.1 h1:0txy2PKPMM873JbpxQNbJmuOJtD56bfs48RQfm0ts5A= github.com/onflow/crypto v0.25.1/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 h1:AegPBm079X0qjneUYs+mRCpEUxSZ1lw5h4MbuXHlqn0= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= -github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 h1:za6bxPPW4JIsthhasUDTa1ruKjIO8DIhun9INQfj61Y= -github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0 h1:cq3RfBr9TnTSnsGlUHMjMGZib24Horfb1XJqMpkN5ew= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0 h1:aMFJdB2CW+Dzm+AJ5QN6J1yWh+a0l2RxHN2/TtLaXUo= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= -github.com/onflow/flow-go-sdk v1.0.0-preview.35 h1:2ptBhFYFGOaYghZTRbj51BbYqTZjkyEpXDyaWDYrHwA= -github.com/onflow/flow-go-sdk v1.0.0-preview.35/go.mod h1:/G8vtAekhvgynLYVDtd6OnhixoGTzzknmhYCJB2YWWU= +github.com/onflow/flow-go-sdk v1.0.0-preview.36 h1:3g72MjmZPEEVAbtDATbjwqKoNSB7yHLWswUHSAB5zwQ= +github.com/onflow/flow-go-sdk v1.0.0-preview.36/go.mod h1:mjkXIluC+kseYyd8Z1aTq73IiffAUeoY5fuX/C2Z+1w= github.com/onflow/flow-nft/lib/go/contracts v1.2.1 h1:woAAS5z651sDpi7ihAHll8NvRS9uFXIXkL6xR+bKFZY= github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkpBPlggIRkI53Suo0n2AyA2HcE= github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= diff --git a/integration/go.mod b/integration/go.mod index 138d2e0374c..25c8b66e0c9 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -19,13 +19,13 @@ require ( github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-badger2 v0.1.3 github.com/libp2p/go-libp2p v0.32.2 - github.com/onflow/cadence v1.0.0-preview.33 + github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34 github.com/onflow/crypto v0.25.1 - github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 - github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 + github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0 + github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0 github.com/onflow/flow-emulator v1.0.0-preview.24 github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd - github.com/onflow/flow-go-sdk v1.0.0-preview.35 + github.com/onflow/flow-go-sdk v1.0.0-preview.36 github.com/onflow/flow-go/insecure v0.0.0-00010101000000-000000000000 github.com/onflow/flow/protobuf/go/flow v0.4.4 github.com/onflow/go-ethereum v1.13.4 @@ -242,7 +242,7 @@ require ( github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/onflow/atree v0.7.0-rc.2 // indirect + github.com/onflow/atree v0.8.0-rc.3 // indirect github.com/onflow/flow-ft/lib/go/contracts v1.0.0 // indirect github.com/onflow/flow-ft/lib/go/templates v1.0.0 // indirect github.com/onflow/flow-nft/lib/go/contracts v1.2.1 // indirect diff --git a/integration/go.sum b/integration/go.sum index 310bad73ae6..835cc65b30f 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -2147,18 +2147,18 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onflow/atree v0.6.1-0.20230711151834-86040b30171f/go.mod h1:xvP61FoOs95K7IYdIYRnNcYQGf4nbF/uuJ0tHf4DRuM= -github.com/onflow/atree v0.7.0-rc.2 h1:mZmVrl/zPlfI44EjV3FdR2QwIqT8nz1sCONUBFcML/U= -github.com/onflow/atree v0.7.0-rc.2/go.mod h1:xvP61FoOs95K7IYdIYRnNcYQGf4nbF/uuJ0tHf4DRuM= +github.com/onflow/atree v0.8.0-rc.3 h1:BHVkJLrBHhHo7ET8gkuS1+lyQGNekYYOyoICGK3RFNM= +github.com/onflow/atree v0.8.0-rc.3/go.mod h1:7YNAyCd5JENq+NzH+fR1ABUZVzbSq9dkt0+5fZH3L2A= github.com/onflow/cadence v1.0.0-M3/go.mod h1:odXGZZ/wGNA5mwT8bC9v8u8EXACHllB2ABSZK65TGL8= -github.com/onflow/cadence v1.0.0-preview.33 h1:kqkU+9//PRsyL3SMokeK2mStarZVxiwrGypyiOX/On8= -github.com/onflow/cadence v1.0.0-preview.33/go.mod h1:jOwvPSSLTr9TvaKMs7KKiBYMmpdpNNAFxBsjMlrqVD0= +github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34 h1:YB/7HUIsKTB6ZKCJbiAhqDI19Np+0j5zPuUUM2uMqGo= +github.com/onflow/cadence v1.0.0-preview-atree-register-inlining.34/go.mod h1:zyl2lWOLVKiKt8BoGAModCHqFEivJ44Y7Myx6NwND7o= github.com/onflow/crypto v0.25.0/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= github.com/onflow/crypto v0.25.1 h1:0txy2PKPMM873JbpxQNbJmuOJtD56bfs48RQfm0ts5A= github.com/onflow/crypto v0.25.1/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 h1:AegPBm079X0qjneUYs+mRCpEUxSZ1lw5h4MbuXHlqn0= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= -github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 h1:za6bxPPW4JIsthhasUDTa1ruKjIO8DIhun9INQfj61Y= -github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0 h1:cq3RfBr9TnTSnsGlUHMjMGZib24Horfb1XJqMpkN5ew= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0 h1:aMFJdB2CW+Dzm+AJ5QN6J1yWh+a0l2RxHN2/TtLaXUo= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= github.com/onflow/flow-emulator v1.0.0-preview.24 h1:SonXMBeYxVwNn94M+OUmKIYScIMQG22wugh9n/tHY5k= github.com/onflow/flow-emulator v1.0.0-preview.24/go.mod h1:QprPouTWO3iv9VF/y4Ksltv2XIbzNMzjjr5zzq51i7Q= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= @@ -2166,8 +2166,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/ github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= -github.com/onflow/flow-go-sdk v1.0.0-preview.35 h1:2ptBhFYFGOaYghZTRbj51BbYqTZjkyEpXDyaWDYrHwA= -github.com/onflow/flow-go-sdk v1.0.0-preview.35/go.mod h1:/G8vtAekhvgynLYVDtd6OnhixoGTzzknmhYCJB2YWWU= +github.com/onflow/flow-go-sdk v1.0.0-preview.36 h1:3g72MjmZPEEVAbtDATbjwqKoNSB7yHLWswUHSAB5zwQ= +github.com/onflow/flow-go-sdk v1.0.0-preview.36/go.mod h1:mjkXIluC+kseYyd8Z1aTq73IiffAUeoY5fuX/C2Z+1w= github.com/onflow/flow-nft/lib/go/contracts v1.2.1 h1:woAAS5z651sDpi7ihAHll8NvRS9uFXIXkL6xR+bKFZY= github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkpBPlggIRkI53Suo0n2AyA2HcE= github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= diff --git a/integration/tests/access/cohort3/grpc_state_stream_test.go b/integration/tests/access/cohort3/grpc_state_stream_test.go index d4f77b9245d..2f2d883bb1e 100644 --- a/integration/tests/access/cohort3/grpc_state_stream_test.go +++ b/integration/tests/access/cohort3/grpc_state_stream_test.go @@ -114,7 +114,6 @@ func (s *GrpcStateStreamSuite) SetupTest() { testnet.AsGhost()) consensusConfigs := []func(config *testnet.NodeConfig){ - testnet.WithAdditionalFlag("--cruise-ctl-fallback-proposal-duration=400ms"), testnet.WithAdditionalFlag(fmt.Sprintf("--required-verification-seal-approvals=%d", 1)), testnet.WithAdditionalFlag(fmt.Sprintf("--required-construction-seal-approvals=%d", 1)), testnet.WithLogLevel(zerolog.FatalLevel), diff --git a/integration/tests/access/cohort3/grpc_streaming_blocks_test.go b/integration/tests/access/cohort3/grpc_streaming_blocks_test.go index 1cc6139676b..96c7655406a 100644 --- a/integration/tests/access/cohort3/grpc_streaming_blocks_test.go +++ b/integration/tests/access/cohort3/grpc_streaming_blocks_test.go @@ -74,7 +74,6 @@ func (s *GrpcBlocksStreamSuite) SetupTest() { ) consensusConfigs := []func(config *testnet.NodeConfig){ - testnet.WithAdditionalFlag("--cruise-ctl-fallback-proposal-duration=400ms"), testnet.WithAdditionalFlag(fmt.Sprintf("--required-verification-seal-approvals=%d", 1)), testnet.WithAdditionalFlag(fmt.Sprintf("--required-construction-seal-approvals=%d", 1)), testnet.WithLogLevel(zerolog.FatalLevel), diff --git a/integration/tests/epochs/base_suite.go b/integration/tests/epochs/base_suite.go index 3f2d037a146..69dce17d98f 100644 --- a/integration/tests/epochs/base_suite.go +++ b/integration/tests/epochs/base_suite.go @@ -50,9 +50,8 @@ type BaseSuite struct { // SetupTest is run automatically by the testing framework before each test case. func (s *BaseSuite) SetupTest() { - // If unset, use default value 100ms if s.ConsensusProposalDuration == 0 { - s.ConsensusProposalDuration = time.Millisecond * 100 + s.ConsensusProposalDuration = time.Millisecond * 250 } minEpochLength := s.StakingAuctionLen + s.DKGPhaseLen*3 + 20 diff --git a/integration/tests/epochs/cohort2/epoch_join_and_leave_sn_test.go b/integration/tests/epochs/cohort2/epoch_join_and_leave_sn_test.go index d101af6371d..1dfd01fec0a 100644 --- a/integration/tests/epochs/cohort2/epoch_join_and_leave_sn_test.go +++ b/integration/tests/epochs/cohort2/epoch_join_and_leave_sn_test.go @@ -2,7 +2,6 @@ package cohort2 import ( "testing" - "time" "github.com/stretchr/testify/suite" @@ -19,11 +18,6 @@ type EpochJoinAndLeaveSNSuite struct { } func (s *EpochJoinAndLeaveSNSuite) SetupTest() { - // slow down the block rate. This is needed since the crypto module - // update provides faster BLS operations. - // TODO: fix the access integration test logic to function without slowing down - // the block rate - s.ConsensusProposalDuration = time.Millisecond * 250 s.DynamicEpochTransitionSuite.SetupTest() } diff --git a/integration/tests/epochs/cohort2/epoch_join_and_leave_vn_test.go b/integration/tests/epochs/cohort2/epoch_join_and_leave_vn_test.go index 9f87c156ed7..716927ddfc3 100644 --- a/integration/tests/epochs/cohort2/epoch_join_and_leave_vn_test.go +++ b/integration/tests/epochs/cohort2/epoch_join_and_leave_vn_test.go @@ -2,7 +2,6 @@ package cohort2 import ( "testing" - "time" "github.com/stretchr/testify/suite" @@ -21,8 +20,6 @@ type EpochJoinAndLeaveVNSuite struct { func (s *EpochJoinAndLeaveVNSuite) SetupTest() { // require approvals for seals to verify that the joining VN is producing valid seals in the second epoch s.RequiredSealApprovals = 1 - // slow down consensus, as sealing tends to lag behind - s.ConsensusProposalDuration = time.Millisecond * 250 // increase epoch length to account for greater sealing lag due to above // NOTE: this value is set fairly aggressively to ensure shorter test times. // If flakiness due to failure to complete staking operations in time is observed, diff --git a/model/flow/chunk.go b/model/flow/chunk.go index e8aeafea8bf..83eabde4b1e 100644 --- a/model/flow/chunk.go +++ b/model/flow/chunk.go @@ -1,9 +1,11 @@ package flow import ( + "fmt" "log" "github.com/ipfs/go-cid" + "github.com/vmihailenco/msgpack/v4" ) var EmptyEventCollectionID Identifier @@ -203,3 +205,65 @@ type BlockExecutionDataRoot struct { // associated with this block. ChunkExecutionDataIDs []cid.Cid } + +// MarshalMsgpack implements the msgpack.Marshaler interface +func (b BlockExecutionDataRoot) MarshalMsgpack() ([]byte, error) { + return msgpack.Marshal(struct { + BlockID Identifier + ChunkExecutionDataIDs []string + }{ + BlockID: b.BlockID, + ChunkExecutionDataIDs: cidsToStrings(b.ChunkExecutionDataIDs), + }) +} + +// UnmarshalMsgpack implements the msgpack.Unmarshaler interface +func (b *BlockExecutionDataRoot) UnmarshalMsgpack(data []byte) error { + var temp struct { + BlockID Identifier + ChunkExecutionDataIDs []string + } + + if err := msgpack.Unmarshal(data, &temp); err != nil { + return err + } + + b.BlockID = temp.BlockID + cids, err := stringsToCids(temp.ChunkExecutionDataIDs) + + if err != nil { + return fmt.Errorf("failed to decode chunk execution data ids: %w", err) + } + + b.ChunkExecutionDataIDs = cids + + return nil +} + +// Helper function to convert a slice of cid.Cid to a slice of strings +func cidsToStrings(cids []cid.Cid) []string { + if cids == nil { + return nil + } + strs := make([]string, len(cids)) + for i, c := range cids { + strs[i] = c.String() + } + return strs +} + +// Helper function to convert a slice of strings to a slice of cid.Cid +func stringsToCids(strs []string) ([]cid.Cid, error) { + if strs == nil { + return nil, nil + } + cids := make([]cid.Cid, len(strs)) + for i, s := range strs { + c, err := cid.Decode(s) + if err != nil { + return nil, fmt.Errorf("failed to decode cid %v: %w", s, err) + } + cids[i] = c + } + return cids, nil +} diff --git a/model/flow/ledger_test.go b/model/flow/ledger_test.go index b287c3d3bb0..f188e1625cf 100644 --- a/model/flow/ledger_test.go +++ b/model/flow/ledger_test.go @@ -87,7 +87,7 @@ func TestRegisterID_IsInternalState(t *testing.T) { func TestRegisterID_String(t *testing.T) { t.Run("atree slab", func(t *testing.T) { // slab with 189 should result in \\xbd - slabIndex := atree.StorageIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 189}) + slabIndex := atree.SlabIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 189}) id := flow.NewRegisterID( flow.BytesToAddress([]byte{1, 2, 3, 10}), diff --git a/network/p2p/cache/protocol_state_provider.go b/network/p2p/cache/protocol_state_provider.go index 6f7a4462b5b..5d4d5fa06e1 100644 --- a/network/p2p/cache/protocol_state_provider.go +++ b/network/p2p/cache/protocol_state_provider.go @@ -70,7 +70,6 @@ func NewProtocolStateIDCache( // and virtually latency free. However, we run data base queries and acquire locks here, // which is undesired. func (p *ProtocolStateIDCache) EpochTransition(newEpochCounter uint64, header *flow.Header) { - p.logger.Info().Uint64("newEpochCounter", newEpochCounter).Msg("epoch transition") p.update(header.ID()) } @@ -82,7 +81,6 @@ func (p *ProtocolStateIDCache) EpochTransition(newEpochCounter uint64, header *f // and virtually latency free. However, we run data base queries and acquire locks here, // which is undesired. func (p *ProtocolStateIDCache) EpochSetupPhaseStarted(currentEpochCounter uint64, header *flow.Header) { - p.logger.Info().Uint64("currentEpochCounter", currentEpochCounter).Msg("epoch setup phase started") p.update(header.ID()) } @@ -94,7 +92,6 @@ func (p *ProtocolStateIDCache) EpochSetupPhaseStarted(currentEpochCounter uint64 // and virtually latency free. However, we run data base queries and acquire locks here, // which is undesired. func (p *ProtocolStateIDCache) EpochCommittedPhaseStarted(currentEpochCounter uint64, header *flow.Header) { - p.logger.Info().Uint64("currentEpochCounter", currentEpochCounter).Msg("epoch committed phase started") p.update(header.ID()) } diff --git a/state/protocol/events/logger.go b/state/protocol/events/logger.go new file mode 100644 index 00000000000..942ba00e480 --- /dev/null +++ b/state/protocol/events/logger.go @@ -0,0 +1,42 @@ +package events + +import ( + "github.com/rs/zerolog" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/state/protocol" +) + +type EventLogger struct { + Noop // satisfy protocol events consumer interface + logger zerolog.Logger +} + +var _ protocol.Consumer = (*EventLogger)(nil) + +func NewEventLogger(logger zerolog.Logger) *EventLogger { + return &EventLogger{ + logger: logger.With().Str("module", "protocol_events_logger").Logger(), + } +} + +func (p EventLogger) EpochTransition(newEpochCounter uint64, header *flow.Header) { + p.logger.Info().Uint64("newEpochCounter", newEpochCounter). + Uint64("height", header.Height). + Uint64("view", header.View). + Msg("epoch transition") +} + +func (p EventLogger) EpochSetupPhaseStarted(currentEpochCounter uint64, header *flow.Header) { + p.logger.Info().Uint64("currentEpochCounter", currentEpochCounter). + Uint64("height", header.Height). + Uint64("view", header.View). + Msg("epoch setup phase started") +} + +func (p EventLogger) EpochCommittedPhaseStarted(currentEpochCounter uint64, header *flow.Header) { + p.logger.Info().Uint64("currentEpochCounter", currentEpochCounter). + Uint64("height", header.Height). + Uint64("view", header.View). + Msg("epoch committed phase started") +} diff --git a/storage/badger/chunkDataPacks.go b/storage/badger/chunkDataPacks.go index 63865a574ce..05f42cf7856 100644 --- a/storage/badger/chunkDataPacks.go +++ b/storage/badger/chunkDataPacks.go @@ -9,7 +9,6 @@ import ( "github.com/onflow/flow-go/module" "github.com/onflow/flow-go/module/metrics" "github.com/onflow/flow-go/storage" - badgermodel "github.com/onflow/flow-go/storage/badger/model" "github.com/onflow/flow-go/storage/badger/operation" "github.com/onflow/flow-go/storage/badger/transaction" ) @@ -17,25 +16,25 @@ import ( type ChunkDataPacks struct { db *badger.DB collections storage.Collections - byChunkIDCache *Cache[flow.Identifier, *badgermodel.StoredChunkDataPack] + byChunkIDCache *Cache[flow.Identifier, *storage.StoredChunkDataPack] } func NewChunkDataPacks(collector module.CacheMetrics, db *badger.DB, collections storage.Collections, byChunkIDCacheSize uint) *ChunkDataPacks { - store := func(key flow.Identifier, val *badgermodel.StoredChunkDataPack) func(*transaction.Tx) error { + store := func(key flow.Identifier, val *storage.StoredChunkDataPack) func(*transaction.Tx) error { return transaction.WithTx(operation.SkipDuplicates(operation.InsertChunkDataPack(val))) } - retrieve := func(key flow.Identifier) func(tx *badger.Txn) (*badgermodel.StoredChunkDataPack, error) { - return func(tx *badger.Txn) (*badgermodel.StoredChunkDataPack, error) { - var c badgermodel.StoredChunkDataPack + retrieve := func(key flow.Identifier) func(tx *badger.Txn) (*storage.StoredChunkDataPack, error) { + return func(tx *badger.Txn) (*storage.StoredChunkDataPack, error) { + var c storage.StoredChunkDataPack err := operation.RetrieveChunkDataPack(key, &c)(tx) return &c, err } } cache := newCache(collector, metrics.ResourceChunkDataPack, - withLimit[flow.Identifier, *badgermodel.StoredChunkDataPack](byChunkIDCacheSize), + withLimit[flow.Identifier, *storage.StoredChunkDataPack](byChunkIDCacheSize), withStore(store), withRetrieve(retrieve), ) @@ -71,7 +70,7 @@ func (ch *ChunkDataPacks) Remove(chunkIDs []flow.Identifier) error { // No errors are expected during normal operation, but it may return generic error // if entity is not serializable or Badger unexpectedly fails to process request func (ch *ChunkDataPacks) BatchStore(c *flow.ChunkDataPack, batch storage.BatchStorage) error { - sc := toStoredChunkDataPack(c) + sc := storage.ToStoredChunkDataPack(c) writeBatch := batch.GetWriter() batch.OnSucceed(func() { ch.byChunkIDCache.Insert(sc.ChunkID, sc) @@ -133,7 +132,7 @@ func (ch *ChunkDataPacks) ByChunkID(chunkID flow.Identifier) (*flow.ChunkDataPac return chdp, nil } -func (ch *ChunkDataPacks) byChunkID(chunkID flow.Identifier) (*badgermodel.StoredChunkDataPack, error) { +func (ch *ChunkDataPacks) byChunkID(chunkID flow.Identifier) (*storage.StoredChunkDataPack, error) { tx := ch.db.NewTransaction(false) defer tx.Discard() @@ -145,8 +144,8 @@ func (ch *ChunkDataPacks) byChunkID(chunkID flow.Identifier) (*badgermodel.Store return schdp, nil } -func (ch *ChunkDataPacks) retrieveCHDP(chunkID flow.Identifier) func(*badger.Txn) (*badgermodel.StoredChunkDataPack, error) { - return func(tx *badger.Txn) (*badgermodel.StoredChunkDataPack, error) { +func (ch *ChunkDataPacks) retrieveCHDP(chunkID flow.Identifier) func(*badger.Txn) (*storage.StoredChunkDataPack, error) { + return func(tx *badger.Txn) (*storage.StoredChunkDataPack, error) { val, err := ch.byChunkIDCache.Get(chunkID)(tx) if err != nil { return nil, err @@ -154,22 +153,3 @@ func (ch *ChunkDataPacks) retrieveCHDP(chunkID flow.Identifier) func(*badger.Txn return val, nil } } - -func toStoredChunkDataPack(c *flow.ChunkDataPack) *badgermodel.StoredChunkDataPack { - sc := &badgermodel.StoredChunkDataPack{ - ChunkID: c.ChunkID, - StartState: c.StartState, - Proof: c.Proof, - SystemChunk: false, - ExecutionDataRoot: c.ExecutionDataRoot, - } - - if c.Collection != nil { - // non system chunk - sc.CollectionID = c.Collection.ID() - } else { - sc.SystemChunk = true - } - - return sc -} diff --git a/storage/badger/model/storedChunkDataPack.go b/storage/badger/model/storedChunkDataPack.go deleted file mode 100644 index 31349604070..00000000000 --- a/storage/badger/model/storedChunkDataPack.go +++ /dev/null @@ -1,17 +0,0 @@ -package badgermodel - -import ( - "github.com/onflow/flow-go/model/flow" -) - -// StoredChunkDataPack is an in-storage representation of chunk data pack. -// Its prime difference is instead of an actual collection, it keeps a collection ID hence relying on maintaining -// the collection on a secondary storage. -type StoredChunkDataPack struct { - ChunkID flow.Identifier - StartState flow.StateCommitment - Proof flow.StorageProof - CollectionID flow.Identifier - SystemChunk bool - ExecutionDataRoot flow.BlockExecutionDataRoot -} diff --git a/storage/badger/operation/chunkDataPacks.go b/storage/badger/operation/chunkDataPacks.go index 687712985d4..e0f2deb2ce2 100644 --- a/storage/badger/operation/chunkDataPacks.go +++ b/storage/badger/operation/chunkDataPacks.go @@ -4,16 +4,16 @@ import ( "github.com/dgraph-io/badger/v2" "github.com/onflow/flow-go/model/flow" - badgermodel "github.com/onflow/flow-go/storage/badger/model" + "github.com/onflow/flow-go/storage" ) // InsertChunkDataPack inserts a chunk data pack keyed by chunk ID. -func InsertChunkDataPack(c *badgermodel.StoredChunkDataPack) func(*badger.Txn) error { +func InsertChunkDataPack(c *storage.StoredChunkDataPack) func(*badger.Txn) error { return insert(makePrefix(codeChunkDataPack, c.ChunkID), c) } // BatchInsertChunkDataPack inserts a chunk data pack keyed by chunk ID into a batch -func BatchInsertChunkDataPack(c *badgermodel.StoredChunkDataPack) func(batch *badger.WriteBatch) error { +func BatchInsertChunkDataPack(c *storage.StoredChunkDataPack) func(batch *badger.WriteBatch) error { return batchWrite(makePrefix(codeChunkDataPack, c.ChunkID), c) } @@ -25,7 +25,7 @@ func BatchRemoveChunkDataPack(chunkID flow.Identifier) func(batch *badger.WriteB } // RetrieveChunkDataPack retrieves a chunk data pack by chunk ID. -func RetrieveChunkDataPack(chunkID flow.Identifier, c *badgermodel.StoredChunkDataPack) func(*badger.Txn) error { +func RetrieveChunkDataPack(chunkID flow.Identifier, c *storage.StoredChunkDataPack) func(*badger.Txn) error { return retrieve(makePrefix(codeChunkDataPack, chunkID), c) } diff --git a/storage/badger/operation/chunkDataPacks_test.go b/storage/badger/operation/chunkDataPacks_test.go index 0dc79ef7266..f3a90af8d00 100644 --- a/storage/badger/operation/chunkDataPacks_test.go +++ b/storage/badger/operation/chunkDataPacks_test.go @@ -7,14 +7,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - storagemodel "github.com/onflow/flow-go/storage/badger/model" + "github.com/onflow/flow-go/storage" "github.com/onflow/flow-go/utils/unittest" ) func TestChunkDataPack(t *testing.T) { unittest.RunWithBadgerDB(t, func(db *badger.DB) { collectionID := unittest.IdentifierFixture() - expected := &storagemodel.StoredChunkDataPack{ + expected := &storage.StoredChunkDataPack{ ChunkID: unittest.IdentifierFixture(), StartState: unittest.StateCommitmentFixture(), Proof: []byte{'p'}, @@ -22,7 +22,7 @@ func TestChunkDataPack(t *testing.T) { } t.Run("Retrieve non-existent", func(t *testing.T) { - var actual storagemodel.StoredChunkDataPack + var actual storage.StoredChunkDataPack err := db.View(RetrieveChunkDataPack(expected.ChunkID, &actual)) assert.Error(t, err) }) @@ -31,7 +31,7 @@ func TestChunkDataPack(t *testing.T) { err := db.Update(InsertChunkDataPack(expected)) require.NoError(t, err) - var actual storagemodel.StoredChunkDataPack + var actual storage.StoredChunkDataPack err = db.View(RetrieveChunkDataPack(expected.ChunkID, &actual)) assert.NoError(t, err) @@ -42,7 +42,7 @@ func TestChunkDataPack(t *testing.T) { err := db.Update(RemoveChunkDataPack(expected.ChunkID)) require.NoError(t, err) - var actual storagemodel.StoredChunkDataPack + var actual storage.StoredChunkDataPack err = db.View(RetrieveChunkDataPack(expected.ChunkID, &actual)) assert.Error(t, err) }) diff --git a/storage/chunkDataPacks.go b/storage/chunkDataPacks.go index d9f49735d90..60d4fe2375d 100644 --- a/storage/chunkDataPacks.go +++ b/storage/chunkDataPacks.go @@ -15,9 +15,6 @@ type ChunkDataPacks interface { // No errors are expected during normal operation, but it may return generic error Remove(cs []flow.Identifier) error - // BatchStore inserts the chunk header, keyed by chunk ID into a given batch - BatchStore(c *flow.ChunkDataPack, batch BatchStorage) error - // ByChunkID returns the chunk data for the given a chunk ID. ByChunkID(chunkID flow.Identifier) (*flow.ChunkDataPack, error) @@ -26,3 +23,34 @@ type ChunkDataPacks interface { // If Badger unexpectedly fails to process the request, the error is wrapped in a generic error and returned. BatchRemove(chunkID flow.Identifier, batch BatchStorage) error } + +// StoredChunkDataPack is an in-storage representation of chunk data pack. +// Its prime difference is instead of an actual collection, it keeps a collection ID hence relying on maintaining +// the collection on a secondary storage. +type StoredChunkDataPack struct { + ChunkID flow.Identifier + StartState flow.StateCommitment + Proof flow.StorageProof + CollectionID flow.Identifier + SystemChunk bool + ExecutionDataRoot flow.BlockExecutionDataRoot +} + +func ToStoredChunkDataPack(c *flow.ChunkDataPack) *StoredChunkDataPack { + sc := &StoredChunkDataPack{ + ChunkID: c.ChunkID, + StartState: c.StartState, + Proof: c.Proof, + SystemChunk: false, + ExecutionDataRoot: c.ExecutionDataRoot, + } + + if c.Collection != nil { + // non system chunk + sc.CollectionID = c.Collection.ID() + } else { + sc.SystemChunk = true + } + + return sc +} diff --git a/storage/mock/chunk_data_packs.go b/storage/mock/chunk_data_packs.go index 3fbacab10d8..2db2f6dbe63 100644 --- a/storage/mock/chunk_data_packs.go +++ b/storage/mock/chunk_data_packs.go @@ -32,24 +32,6 @@ func (_m *ChunkDataPacks) BatchRemove(chunkID flow.Identifier, batch storage.Bat return r0 } -// BatchStore provides a mock function with given fields: c, batch -func (_m *ChunkDataPacks) BatchStore(c *flow.ChunkDataPack, batch storage.BatchStorage) error { - ret := _m.Called(c, batch) - - if len(ret) == 0 { - panic("no return value specified for BatchStore") - } - - var r0 error - if rf, ok := ret.Get(0).(func(*flow.ChunkDataPack, storage.BatchStorage) error); ok { - r0 = rf(c, batch) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // ByChunkID provides a mock function with given fields: chunkID func (_m *ChunkDataPacks) ByChunkID(chunkID flow.Identifier) (*flow.ChunkDataPack, error) { ret := _m.Called(chunkID) diff --git a/storage/pebble/batch.go b/storage/pebble/batch.go new file mode 100644 index 00000000000..9a45e55bc02 --- /dev/null +++ b/storage/pebble/batch.go @@ -0,0 +1,58 @@ +package pebble + +import ( + "sync" + + "github.com/cockroachdb/pebble" +) + +type Batch struct { + writer *pebble.Batch + + lock sync.RWMutex + callbacks []func() +} + +func NewBatch(db *pebble.DB) *Batch { + batch := db.NewBatch() + return &Batch{ + writer: batch, + callbacks: make([]func(), 0), + } +} + +func (b *Batch) GetWriter() *pebble.Batch { + return b.writer +} + +// OnSucceed adds a callback to execute after the batch has +// been successfully flushed. +// useful for implementing the cache where we will only cache +// after the batch has been successfully flushed +func (b *Batch) OnSucceed(callback func()) { + b.lock.Lock() + defer b.lock.Unlock() + b.callbacks = append(b.callbacks, callback) +} + +// Flush will call the badger Batch's Flush method, in +// addition, it will call the callbacks added by +// OnSucceed +// any error are exceptions +func (b *Batch) Flush() error { + err := b.writer.Commit(nil) + if err != nil { + return err + } + + b.lock.RLock() + defer b.lock.RUnlock() + for _, callback := range b.callbacks { + callback() + } + return nil +} + +func (b *Batch) Close() error { + return b.writer.Close() +} diff --git a/storage/pebble/chunk_data_packs.go b/storage/pebble/chunk_data_packs.go new file mode 100644 index 00000000000..c0b5b47eeab --- /dev/null +++ b/storage/pebble/chunk_data_packs.go @@ -0,0 +1,144 @@ +package pebble + +import ( + "fmt" + + "github.com/cockroachdb/pebble" + "github.com/rs/zerolog/log" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module" + "github.com/onflow/flow-go/module/metrics" + "github.com/onflow/flow-go/storage" + "github.com/onflow/flow-go/storage/pebble/operation" +) + +type ChunkDataPacks struct { + db *pebble.DB + collections storage.Collections + byChunkIDCache *Cache[flow.Identifier, *storage.StoredChunkDataPack] +} + +var _ storage.ChunkDataPacks = (*ChunkDataPacks)(nil) + +func NewChunkDataPacks(collector module.CacheMetrics, db *pebble.DB, collections storage.Collections, byChunkIDCacheSize uint) *ChunkDataPacks { + + retrieve := func(key flow.Identifier) func(pebble.Reader) (*storage.StoredChunkDataPack, error) { + return func(r pebble.Reader) (*storage.StoredChunkDataPack, error) { + var c storage.StoredChunkDataPack + err := operation.RetrieveChunkDataPack(key, &c)(r) + return &c, err + } + } + + cache := newCache(collector, metrics.ResourceChunkDataPack, + withLimit[flow.Identifier, *storage.StoredChunkDataPack](byChunkIDCacheSize), + withRetrieve(retrieve), + ) + + return &ChunkDataPacks{ + db: db, + collections: collections, + byChunkIDCache: cache, + } +} + +// Store stores the given chunk data pack lists, it stores them atomically. +// Any error are exceptions +func (ch *ChunkDataPacks) Store(cs []*flow.ChunkDataPack) error { + batch := NewBatch(ch.db) + defer func() { + err := batch.Close() + if err != nil { + log.Error().Err(err).Msgf("failed to close batch when storing chunk data pack") + } + }() + + for _, c := range cs { + err := ch.batchStore(c, batch) + if err != nil { + return fmt.Errorf("cannot store chunk data pack: %w", err) + } + } + + err := batch.Flush() + if err != nil { + return fmt.Errorf("cannot commit batch: %w", err) + } + + return nil +} + +// Remove removes chunk data packs by IDs, it removes them atomically. +// Any errors are exceptions +func (ch *ChunkDataPacks) Remove(cs []flow.Identifier) error { + batch := ch.db.NewBatch() + + for _, c := range cs { + err := ch.batchRemove(c, batch) + if err != nil { + return fmt.Errorf("cannot remove chunk data pack: %w", err) + } + } + + err := batch.Commit(pebble.Sync) + if err != nil { + return fmt.Errorf("cannot commit batch: %w", err) + } + + for _, c := range cs { + ch.byChunkIDCache.Remove(c) + } + + return nil +} + +// ByChunkID finds the chunk data pack by chunk ID. +// it returns storage.ErrNotFound if not found +// other errors are exceptions +func (ch *ChunkDataPacks) ByChunkID(chunkID flow.Identifier) (*flow.ChunkDataPack, error) { + var sc storage.StoredChunkDataPack + err := operation.RetrieveChunkDataPack(chunkID, &sc)(ch.db) + if err != nil { + return nil, fmt.Errorf("could not retrieve stored chunk data pack: %w", err) + } + + chdp := &flow.ChunkDataPack{ + ChunkID: sc.ChunkID, + StartState: sc.StartState, + Proof: sc.Proof, + Collection: nil, // to be filled in later + ExecutionDataRoot: sc.ExecutionDataRoot, + } + if !sc.SystemChunk { + collection, err := ch.collections.ByID(sc.CollectionID) + if err != nil { + return nil, fmt.Errorf("could not retrive collection (id: %x) for stored chunk data pack: %w", sc.CollectionID, err) + } + + chdp.Collection = collection + } + return chdp, nil +} + +// BatchRemove is not used in pebble implementation +func (ch *ChunkDataPacks) BatchRemove(chunkID flow.Identifier, batch storage.BatchStorage) error { + return fmt.Errorf("not implemented") +} + +func (ch *ChunkDataPacks) batchRemove(chunkID flow.Identifier, batch pebble.Writer) error { + return operation.RemoveChunkDataPack(chunkID)(batch) +} + +func (ch *ChunkDataPacks) batchStore(c *flow.ChunkDataPack, batch *Batch) error { + sc := storage.ToStoredChunkDataPack(c) + writer := batch.GetWriter() + batch.OnSucceed(func() { + ch.byChunkIDCache.Insert(sc.ChunkID, sc) + }) + err := operation.InsertChunkDataPack(sc)(writer) + if err != nil { + return fmt.Errorf("failed to store chunk data pack: %w", err) + } + return nil +} diff --git a/storage/pebble/chunk_data_packs_test.go b/storage/pebble/chunk_data_packs_test.go new file mode 100644 index 00000000000..f170b22114c --- /dev/null +++ b/storage/pebble/chunk_data_packs_test.go @@ -0,0 +1,116 @@ +package pebble + +import ( + "errors" + "path/filepath" + "testing" + + "github.com/cockroachdb/pebble" + "github.com/dgraph-io/badger/v2" + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/metrics" + "github.com/onflow/flow-go/storage" + badgerstorage "github.com/onflow/flow-go/storage/badger" + "github.com/onflow/flow-go/utils/unittest" +) + +func TestMsgPacks(t *testing.T) { + chunkDataPacks := unittest.ChunkDataPacksFixture(10) + for _, chunkDataPack := range chunkDataPacks { + sc := storage.ToStoredChunkDataPack(chunkDataPack) + value, err := msgpack.Marshal(sc) + require.NoError(t, err) + + var actual storage.StoredChunkDataPack + err = msgpack.Unmarshal(value, &actual) + require.NoError(t, err) + + require.Equal(t, *sc, actual) + } +} + +// TestChunkDataPacks_Store evaluates correct storage and retrieval of chunk data packs in the storage. +// It also evaluates that re-inserting is idempotent. +func TestChunkDataPacks_Store(t *testing.T) { + WithChunkDataPacks(t, 100, func(t *testing.T, chunkDataPacks []*flow.ChunkDataPack, chunkDataPackStore storage.ChunkDataPacks, _ *pebble.DB) { + // can store + require.NoError(t, chunkDataPackStore.Store(chunkDataPacks)) + + // can read back + for _, c := range chunkDataPacks { + c2, err := chunkDataPackStore.ByChunkID(c.ChunkID) + require.NoError(t, err) + require.Equal(t, c, c2) + } + + // can store again + require.NoError(t, chunkDataPackStore.Store(chunkDataPacks)) + + cids := make([]flow.Identifier, 0, len(chunkDataPacks)) + for i, c := range chunkDataPacks { + // remove everything except the first one + if i > 0 { + cids = append(cids, c.ChunkID) + } + } + // can remove + require.NoError(t, chunkDataPackStore.Remove(cids)) + for i, c := range chunkDataPacks { + if i == 0 { + // the first one is not removed + _, err := chunkDataPackStore.ByChunkID(c.ChunkID) + require.NoError(t, err) + continue + } + // the rest are removed + _, err := chunkDataPackStore.ByChunkID(c.ChunkID) + require.True(t, errors.Is(err, storage.ErrNotFound)) + } + + // can remove again + require.NoError(t, chunkDataPackStore.Remove(cids)) + }) +} + +// WithChunkDataPacks is a test helper that generates specified number of chunk data packs, store them using the storeFunc, and +// then evaluates whether they are successfully retrieved from storage. +func WithChunkDataPacks(t *testing.T, chunks int, storeFunc func(*testing.T, []*flow.ChunkDataPack, storage.ChunkDataPacks, *pebble.DB)) { + RunWithBadgerDBAndPebbleDB(t, func(badgerDB *badger.DB, db *pebble.DB) { + transactions := badgerstorage.NewTransactions(&metrics.NoopCollector{}, badgerDB) + collections := badgerstorage.NewCollections(badgerDB, transactions) + // keep the cache size at 1 to make sure that entries are written and read from storage itself. + store := NewChunkDataPacks(&metrics.NoopCollector{}, db, collections, 1) + + chunkDataPacks := unittest.ChunkDataPacksFixture(chunks) + for _, chunkDataPack := range chunkDataPacks { + // stores collection in Collections storage (which ChunkDataPacks store uses internally) + err := collections.Store(chunkDataPack.Collection) + require.NoError(t, err) + } + + // stores chunk data packs in the memory using provided store function. + storeFunc(t, chunkDataPacks, store, db) + }) +} + +func RunWithBadgerDBAndPebbleDB(t *testing.T, fn func(*badger.DB, *pebble.DB)) { + unittest.RunWithTempDir(t, func(dir string) { + badgerDB := unittest.BadgerDB(t, filepath.Join(dir, "badger")) + defer func() { + require.NoError(t, badgerDB.Close()) + }() + + cache := pebble.NewCache(1 << 20) + defer cache.Unref() + // currently pebble is only used for registers + opts := DefaultPebbleOptions(cache, pebble.DefaultComparer) + pebbledb, err := pebble.Open(filepath.Join(dir, "pebble"), opts) + require.NoError(t, err) + defer pebbledb.Close() + + fn(badgerDB, pebbledb) + }) +} diff --git a/storage/pebble/open.go b/storage/pebble/open.go index a0d7ea6c0d5..80f328ce87a 100644 --- a/storage/pebble/open.go +++ b/storage/pebble/open.go @@ -12,6 +12,8 @@ import ( "github.com/onflow/flow-go/storage/pebble/registers" ) +const DefaultPebbleCacheSize = 1 << 20 + // NewBootstrappedRegistersWithPath initializes a new Registers instance with a pebble db // if the database is not initialized, it close the database and return storage.ErrNotBootstrapped func NewBootstrappedRegistersWithPath(dir string) (*Registers, *pebble.DB, error) { @@ -34,8 +36,12 @@ func NewBootstrappedRegistersWithPath(dir string) (*Registers, *pebble.DB, error } // OpenRegisterPebbleDB opens the database +// The difference between OpenDefaultPebbleDB is that it uses +// a customized comparer (NewMVCCComparer) which is needed to +// implement finding register values at any given height using +// pebble's SeekPrefixGE function func OpenRegisterPebbleDB(dir string) (*pebble.DB, error) { - cache := pebble.NewCache(1 << 20) + cache := pebble.NewCache(DefaultPebbleCacheSize) defer cache.Unref() // currently pebble is only used for registers opts := DefaultPebbleOptions(cache, registers.NewMVCCComparer()) @@ -47,6 +53,20 @@ func OpenRegisterPebbleDB(dir string) (*pebble.DB, error) { return db, nil } +// OpenDefaultPebbleDB opens a pebble database using default options, +// such as cache size and comparer +func OpenDefaultPebbleDB(dir string) (*pebble.DB, error) { + cache := pebble.NewCache(DefaultPebbleCacheSize) + defer cache.Unref() + opts := DefaultPebbleOptions(cache, pebble.DefaultComparer) + db, err := pebble.Open(dir, opts) + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + + return db, nil +} + // ReadHeightsFromBootstrappedDB reads the first and latest height from a bootstrapped register db // If the register db is not bootstrapped, it returns storage.ErrNotBootstrapped // If the register db is corrupted, it returns an error diff --git a/storage/pebble/operation/chunk_data_pack.go b/storage/pebble/operation/chunk_data_pack.go new file mode 100644 index 00000000000..f5cec13cdbe --- /dev/null +++ b/storage/pebble/operation/chunk_data_pack.go @@ -0,0 +1,35 @@ +package operation + +import ( + "github.com/cockroachdb/pebble" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/storage" +) + +// InsertChunkDataPack inserts a chunk data pack keyed by chunk ID. +// any error are exceptions +func InsertChunkDataPack(sc *storage.StoredChunkDataPack) func(w pebble.Writer) error { + key := makeKey(codeChunkDataPack, sc.ChunkID) + return insert(key, sc) +} + +// RetrieveChunkDataPack retrieves a chunk data pack by chunk ID. +// it returns storage.ErrNotFound if the chunk data pack is not found +func RetrieveChunkDataPack(chunkID flow.Identifier, sc *storage.StoredChunkDataPack) func(r pebble.Reader) error { + key := makeKey(codeChunkDataPack, chunkID) + return retrieve(key, sc) +} + +// RemoveChunkDataPack removes the chunk data pack with the given chunk ID. +// any error are exceptions +func RemoveChunkDataPack(chunkID flow.Identifier) func(w pebble.Writer) error { + key := makeKey(codeChunkDataPack, chunkID) + return func(w pebble.Writer) error { + return w.Delete(key, nil) + } +} + +func makeKey(prefix byte, identifier flow.Identifier) []byte { + return append([]byte{prefix}, identifier[:]...) +} diff --git a/storage/pebble/operation/codes.go b/storage/pebble/operation/codes.go new file mode 100644 index 00000000000..1d9057646c3 --- /dev/null +++ b/storage/pebble/operation/codes.go @@ -0,0 +1,5 @@ +package operation + +const ( + codeChunkDataPack = 100 +) diff --git a/storage/pebble/operation/common.go b/storage/pebble/operation/common.go new file mode 100644 index 00000000000..ad9e96c2c8b --- /dev/null +++ b/storage/pebble/operation/common.go @@ -0,0 +1,50 @@ +package operation + +import ( + "errors" + + "github.com/cockroachdb/pebble" + "github.com/vmihailenco/msgpack" + + "github.com/onflow/flow-go/module/irrecoverable" + "github.com/onflow/flow-go/storage" +) + +func insert(key []byte, val interface{}) func(pebble.Writer) error { + return func(w pebble.Writer) error { + value, err := msgpack.Marshal(val) + if err != nil { + return irrecoverable.NewExceptionf("failed to encode value: %w", err) + } + + err = w.Set(key, value, nil) + if err != nil { + return irrecoverable.NewExceptionf("failed to store data: %w", err) + } + + return nil + } +} + +func retrieve(key []byte, sc interface{}) func(r pebble.Reader) error { + return func(r pebble.Reader) error { + val, closer, err := r.Get(key) + if err != nil { + return convertNotFoundError(err) + } + defer closer.Close() + + err = msgpack.Unmarshal(val, &sc) + if err != nil { + return irrecoverable.NewExceptionf("failed to decode value: %w", err) + } + return nil + } +} + +func convertNotFoundError(err error) error { + if errors.Is(err, pebble.ErrNotFound) { + return storage.ErrNotFound + } + return err +} diff --git a/storage/pebble/value_cache.go b/storage/pebble/value_cache.go new file mode 100644 index 00000000000..38f1f394910 --- /dev/null +++ b/storage/pebble/value_cache.go @@ -0,0 +1,152 @@ +package pebble + +import ( + "errors" + "fmt" + + "github.com/cockroachdb/pebble" + lru "github.com/hashicorp/golang-lru/v2" + + "github.com/onflow/flow-go/module" + "github.com/onflow/flow-go/storage" +) + +func withLimit[K comparable, V any](limit uint) func(*Cache[K, V]) { + return func(c *Cache[K, V]) { + c.limit = limit + } +} + +type storeFunc[K comparable, V any] func(key K, val V) func(pebble.Writer) error + +// func withStore[K comparable, V any](store storeFunc[K, V]) func(*Cache[K, V]) { +// return func(c *Cache[K, V]) { +// c.store = store +// } +// } +func noStore[K comparable, V any](_ K, _ V) func(pebble.Writer) error { + return func(pebble.Writer) error { + return fmt.Errorf("no store function for cache put available") + } +} + +// func noopStore[K comparable, V any](_ K, _ V) func(pebble.Reader) error { +// return func(pebble.Reader) error { +// return nil +// } +// } +type retrieveFunc[K comparable, V any] func(key K) func(pebble.Reader) (V, error) + +func withRetrieve[K comparable, V any](retrieve retrieveFunc[K, V]) func(*Cache[K, V]) { + return func(c *Cache[K, V]) { + c.retrieve = retrieve + } +} + +func noRetrieve[K comparable, V any](_ K) func(pebble.Reader) (V, error) { + return func(pebble.Reader) (V, error) { + var nullV V + return nullV, fmt.Errorf("no retrieve function for cache get available") + } +} + +// Cache is a read-through cache for underlying storage layer. +// Note: when a resource is not found in the cache nor the underlying storage, then +// it will not be cached. In other words, finding the missing item again will +// query the underlying storage again. +type Cache[K comparable, V any] struct { + metrics module.CacheMetrics + limit uint + store storeFunc[K, V] + retrieve retrieveFunc[K, V] + resource string + cache *lru.Cache[K, V] +} + +func newCache[K comparable, V any](collector module.CacheMetrics, resourceName string, options ...func(*Cache[K, V])) *Cache[K, V] { + c := Cache[K, V]{ + metrics: collector, + limit: 1000, + store: noStore[K, V], + retrieve: noRetrieve[K, V], + resource: resourceName, + } + for _, option := range options { + option(&c) + } + c.cache, _ = lru.New[K, V](int(c.limit)) + c.metrics.CacheEntries(c.resource, uint(c.cache.Len())) + return &c +} + +// IsCached returns true if the key exists in the cache. +// It DOES NOT check whether the key exists in the underlying data store. +func (c *Cache[K, V]) IsCached(key K) bool { + return c.cache.Contains(key) +} + +// Get will try to retrieve the resource from cache first, and then from the +// injected. During normal operations, the following error returns are expected: +// - `storage.ErrNotFound` if key is unknown. +func (c *Cache[K, V]) Get(key K) func(pebble.Reader) (V, error) { + return func(r pebble.Reader) (V, error) { + + // check if we have it in the cache + resource, cached := c.cache.Get(key) + if cached { + c.metrics.CacheHit(c.resource) + return resource, nil + } + + // get it from the database + resource, err := c.retrieve(key)(r) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + c.metrics.CacheNotFound(c.resource) + } + var nullV V + return nullV, fmt.Errorf("could not retrieve resource: %w", err) + } + + c.metrics.CacheMiss(c.resource) + + // cache the resource and eject least recently used one if we reached limit + evicted := c.cache.Add(key, resource) + if !evicted { + c.metrics.CacheEntries(c.resource, uint(c.cache.Len())) + } + + return resource, nil + } +} + +func (c *Cache[K, V]) Remove(key K) { + c.cache.Remove(key) +} + +// Insert will add a resource directly to the cache with the given ID +// assuming the resource has been added to storage already. +func (c *Cache[K, V]) Insert(key K, resource V) { + // cache the resource and eject least recently used one if we reached limit + evicted := c.cache.Add(key, resource) + if !evicted { + c.metrics.CacheEntries(c.resource, uint(c.cache.Len())) + } +} + +// PutTx will return tx which adds a resource to the cache with the given ID. +func (c *Cache[K, V]) PutTx(key K, resource V) func(pebble.Writer) error { + storeOps := c.store(key, resource) // assemble DB operations to store resource (no execution) + + return func(w pebble.Writer) error { + // the storeOps must be sync operation + err := storeOps(w) // execute operations to store resource + if err != nil { + return fmt.Errorf("could not store resource: %w", err) + } + + c.Insert(key, resource) + + return nil + } +} diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index cb8dbd90786..e7cbd071b65 100644 --- a/utils/unittest/execution_state.go +++ b/utils/unittest/execution_state.go @@ -23,7 +23,7 @@ const ServiceAccountPrivateKeySignAlgo = crypto.ECDSAP256 const ServiceAccountPrivateKeyHashAlgo = hash.SHA2_256 // Pre-calculated state commitment with root account with the above private key -const GenesisStateCommitmentHex = "884400ce45738c071438df5926248e284446afc2528314bf734447d1cbc51bd9" +const GenesisStateCommitmentHex = "cde7208d53bc55ed4d79ecc67586c78729ecec06260ac52e581f98f6643d660d" var GenesisStateCommitment flow.StateCommitment @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string { return GenesisStateCommitmentHex } if chainID == flow.Testnet { - return "00b2d5c8f1d09e4578eb01accbdc86969122e9e3b00e21f27f7e341b509c47bd" + return "0b173069a20b35d550a02ba9ef356aa3b030d61b517fc021856d534293916ae5" } if chainID == flow.Sandboxnet { return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1" } - return "a9f69641c6412877f12bd0078dd58911d92971b3fd47b2b0ccbc377a96a1589c" + return "60129a3d35697db3bb5bc21ba997484992e8673800ce2f8fbbc941cbe30ba77c" }