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/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 ed1d13ef30a..414f7147d36 100644 --- a/cmd/util/cmd/diff-states/cmd.go +++ b/cmd/util/cmd/diff-states/cmd.go @@ -30,6 +30,7 @@ var ( flagStateCommitment1 string flagStateCommitment2 string flagRaw bool + flagAlwaysDiffValues bool flagNWorker int flagChain string ) @@ -113,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", @@ -294,6 +302,8 @@ func diffAccount( }) } + diffValues := flagAlwaysDiffValues + err = accountRegisters1.ForEach(func(owner, key string, value1 []byte) error { var value2 []byte value2, err = accountRegisters2.Get(owner, key) @@ -323,6 +333,10 @@ func diffAccount( return err } + diffValues = true + } + + if diffValues { address, err := common.BytesToAddress([]byte(owner)) if err != nil { return err 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 319f5fc057e..f53b53167c3 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" @@ -325,19 +324,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/cmd/find-trie-root/cmd.go b/cmd/util/cmd/find-trie-root/cmd.go new file mode 100644 index 00000000000..d50b4d9b3dd --- /dev/null +++ b/cmd/util/cmd/find-trie-root/cmd.go @@ -0,0 +1,370 @@ +package find_trie_root + +import ( + "encoding/hex" + "fmt" + "math" + "os" + "path/filepath" + + prometheusWAL "github.com/onflow/wal/wal" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/hash" + "github.com/onflow/flow-go/ledger/complete/wal" +) + +var ( + flagExecutionStateDir string + flagRootHash string + flagFrom int + flagTo int + flagBackupDir string + flagTrimAsLatestWAL bool +) + +// find trie root hash from the wal files. +// useful for state extraction and rolling back executed height. +// for instance, when extracting state for a target height, it requires the wal files +// has the trie root hash of the target block as the latest few records. If not the case, +// then it is necessary to trim the wal files to the last record with the target trie root hash. +// in order to do that, this command can be used to find the trie root hash in the wal files, +// and copy the wal that contains the trie root hash to a new directory and trim it to +// have the target trie root hash as the last record. +// after that, the new wal file can be used to extract the state for the target height. +var Cmd = &cobra.Command{ + Use: "find-trie-root", + Short: "find trie root hash from the wal files", + Run: run, +} + +func init() { + Cmd.Flags().StringVar(&flagExecutionStateDir, "execution-state-dir", "/var/flow/data/execution", + "directory to the execution state") + _ = Cmd.MarkFlagRequired("execution-state-dir") + + Cmd.Flags().StringVar(&flagRootHash, "root-hash", "", + "ledger root hash (hex-encoded, 64 characters)") + _ = Cmd.MarkFlagRequired("root-hash") + + Cmd.Flags().IntVar(&flagFrom, "from", 0, "from segment") + Cmd.Flags().IntVar(&flagTo, "to", math.MaxInt32, "to segment") + + Cmd.Flags().StringVar(&flagBackupDir, "backup-dir", "", + "directory for backup wal files. must be not exist or empty folder. required when --trim-as-latest-wal flag is set to true.") + + Cmd.Flags().BoolVar(&flagTrimAsLatestWAL, "trim-as-latest-wal", false, + "trim the wal file to the last record with the target trie root hash") +} + +func run(*cobra.Command, []string) { + rootHash, err := parseInput(flagRootHash) + if err != nil { + log.Fatal().Err(err).Msg("cannot parse input") + } + + if flagExecutionStateDir == flagBackupDir { + log.Fatal().Msg("--backup-dir directory cannot be the same as the execution state directory") + } + + // making sure the backup dir is empty + empty, err := checkFolderIsEmpty(flagBackupDir) + if err != nil { + log.Fatal().Msgf("--backup-dir directory %v must exist and empty", flagBackupDir) + } + + if !empty { + log.Fatal().Msgf("--backup-dir directory %v must be empty", flagBackupDir) + } + + segment, offset, err := searchRootHashInSegments(rootHash, flagExecutionStateDir, flagFrom, flagTo) + if err != nil { + log.Fatal().Err(err).Msg("cannot find root hash in segments") + } + + segmentFile := prometheusWAL.SegmentName(flagExecutionStateDir, segment) + + log.Info().Msgf("found root hash in segment %d at offset %d, segment file: %v", segment, offset, segmentFile) + + if !flagTrimAsLatestWAL { + log.Info().Msg("not trimming WAL. Exiting. to trim the WAL, use --trim-as-latest-wal flag") + return + } + + if len(flagBackupDir) == 0 { + log.Error().Msgf("--backup-dir directory is not provided") + return + } + + // create a temporary folder in the backup folder to store the new segment file + tmpFolder := filepath.Join(flagBackupDir, "flow-last-segment-file") + + log.Info().Msgf("creating temporary folder %v", tmpFolder) + + err = os.Mkdir(tmpFolder, os.ModePerm) + if err != nil { + log.Fatal().Err(err).Msg("cannot create temporary folder") + } + + defer func() { + log.Info().Msgf("removing temporary folder %v", tmpFolder) + err := os.RemoveAll(tmpFolder) + if err != nil { + log.Error().Err(err).Msg("cannot remove temporary folder") + } + }() + + // genereate a segment file to the temporary folder with the root hash as its last record + newSegmentFile, err := findRootHashAndCreateTrimmed(flagExecutionStateDir, segment, rootHash, tmpFolder) + if err != nil { + log.Fatal().Err(err).Msg("cannot copy WAL") + } + + log.Info().Msgf("successfully copied WAL to the temporary folder %v", newSegmentFile) + + // before replacing the last wal file with the newly generated one, backup the rollbacked wals + // then move the last segment file to the execution state directory + err = backupRollbackedWALsAndMoveLastSegmentFile( + segment, flagExecutionStateDir, flagBackupDir, newSegmentFile) + if err != nil { + log.Fatal().Err(err).Msg("cannot backup rollbacked WALs") + } + + log.Info().Msgf("successfully trimmed WAL %v the trie root hash %v as its last record, original wal files are moved to %v", + segment, rootHash, flagBackupDir) +} + +func parseInput(rootHashStr string) (ledger.RootHash, error) { + rootHashBytes, err := hex.DecodeString(rootHashStr) + if err != nil { + return ledger.RootHash(hash.DummyHash), fmt.Errorf("cannot decode root hash: %w", err) + } + rootHash, err := ledger.ToRootHash(rootHashBytes) + if err != nil { + return ledger.RootHash(hash.DummyHash), fmt.Errorf("invalid root hash: %w", err) + } + return rootHash, nil +} + +func searchRootHashInSegments( + expectedHash ledger.RootHash, + dir string, + wantFrom, wantTo int, +) (int, int64, error) { + lg := zerolog.New(os.Stderr).With().Timestamp().Logger() + from, to, err := prometheusWAL.Segments(dir) + if err != nil { + return 0, 0, fmt.Errorf("cannot get segments: %w", err) + } + + if from < 0 { + return 0, 0, fmt.Errorf("no segments found in %s", dir) + } + + if wantFrom > to { + return 0, 0, fmt.Errorf("from segment %d is greater than the last segment %d", wantFrom, to) + } + + if wantTo < from { + return 0, 0, fmt.Errorf("to segment %d is less than the first segment %d", wantTo, from) + } + + if wantFrom > from { + from = wantFrom + } + + if wantTo < to { + to = wantTo + } + + lg.Info(). + Str("dir", dir). + Int("from", from). + Int("to", to). + Int("want-from", wantFrom). + Int("want-to", wantTo). + Msgf("searching for trie root hash %v in segments [%d,%d]", expectedHash, wantFrom, wantTo) + + sr, err := prometheusWAL.NewSegmentsRangeReader(lg, prometheusWAL.SegmentRange{ + Dir: dir, + First: from, + Last: to, + }) + + if err != nil { + return 0, 0, fmt.Errorf("cannot create WAL segments reader: %w", err) + } + + defer sr.Close() + + reader := prometheusWAL.NewReader(sr) + + for reader.Next() { + record := reader.Record() + operation, _, update, err := wal.Decode(record) + if err != nil { + return 0, 0, fmt.Errorf("cannot decode LedgerWAL record: %w", err) + } + + switch operation { + case wal.WALUpdate: + rootHash := update.RootHash + + log.Debug(). + Uint8("operation", uint8(operation)). + Str("root-hash", rootHash.String()). + Msg("found WALUpdate") + + if rootHash.Equals(expectedHash) { + log.Info().Msgf("found expected trie root hash %v", rootHash) + return reader.Segment(), reader.Offset(), nil + } + default: + } + + err = reader.Err() + if err != nil { + return 0, 0, fmt.Errorf("cannot read LedgerWAL: %w", err) + } + } + + return 0, 0, fmt.Errorf("finish reading all segment files from %d to %d, but not found", from, to) +} + +// findRootHashAndCreateTrimmed finds the root hash in the segment file from the given dir folder +// and creates a new segment file with the expected root hash as the last record in a temporary folder. +// it return the path to the new segment file. +func findRootHashAndCreateTrimmed( + dir string, segment int, expectedRoot ledger.RootHash, tmpFolder string) (string, error) { + // the new segment file will be created in the temporary folder + // and it's always 00000000 + newSegmentFile := prometheusWAL.SegmentName(tmpFolder, 0) + + log.Info().Msgf("writing new segment file to %v", newSegmentFile) + + writer, err := prometheusWAL.NewSize(log.Logger, nil, tmpFolder, wal.SegmentSize, false) + if err != nil { + return "", fmt.Errorf("cannot create writer WAL: %w", err) + } + + defer writer.Close() + + sr, err := prometheusWAL.NewSegmentsRangeReader(log.Logger, prometheusWAL.SegmentRange{ + Dir: dir, + First: segment, + Last: segment, + }) + if err != nil { + return "", fmt.Errorf("cannot create WAL segments reader: %w", err) + } + + defer sr.Close() + + reader := prometheusWAL.NewReader(sr) + + for reader.Next() { + record := reader.Record() + operation, _, update, err := wal.Decode(record) + if err != nil { + return "", fmt.Errorf("cannot decode LedgerWAL record: %w", err) + } + + switch operation { + case wal.WALUpdate: + + bytes := wal.EncodeUpdate(update) + _, err = writer.Log(bytes) + if err != nil { + return "", fmt.Errorf("cannot write LedgerWAL record: %w", err) + } + + rootHash := update.RootHash + + if rootHash.Equals(expectedRoot) { + log.Info().Msgf("found expected trie root hash %v, finish writing", rootHash) + return newSegmentFile, nil + } + default: + } + + err = reader.Err() + if err != nil { + return "", fmt.Errorf("cannot read LedgerWAL: %w", err) + } + } + + return "", fmt.Errorf("finish reading all segment files from %d to %d, but not found", segment, segment) +} + +func checkFolderIsEmpty(folderPath string) (bool, error) { + // Check if the folder exists + info, err := os.Stat(folderPath) + if err != nil { + if os.IsNotExist(err) { + log.Info().Msgf("folder %v does not exist, creating the folder", folderPath) + + // create the folder if not exist + err = os.MkdirAll(folderPath, os.ModePerm) + if err != nil { + return false, fmt.Errorf("Cannot create the folder.") + } + + return true, nil + } + return false, err + } + + // Check if the path is a directory + if !info.IsDir() { + return false, fmt.Errorf("The path is not a directory.") + } + + // Check if the folder is empty + files, err := os.ReadDir(folderPath) + if err != nil { + return false, fmt.Errorf("Cannot read the folder.") + } + + return len(files) == 0, nil +} + +// backup new wals before replacing +func backupRollbackedWALsAndMoveLastSegmentFile( + segment int, walDir, backupDir string, newSegmentFile string) error { + first, last, err := prometheusWAL.Segments(walDir) + if err != nil { + return fmt.Errorf("cannot get segments: %w", err) + } + + if segment < first { + return fmt.Errorf("segment %d is less than the first segment %d", segment, first) + } + + // backup all the segment files that have higher number than the given segment, including + // the segment file itself, since it will be replaced. + for i := segment; i <= last; i++ { + segmentFile := prometheusWAL.SegmentName(walDir, i) + backupFile := prometheusWAL.SegmentName(backupDir, i) + + log.Info().Msgf("backup segment file %s to %s, %v/%v", segmentFile, backupFile, i, last) + err := os.Rename(segmentFile, backupFile) + if err != nil { + return fmt.Errorf("cannot move segment file %s to %s: %w", segmentFile, backupFile, err) + } + } + + // after backup the segment files, replace the last segment file + segmentToBeReplaced := prometheusWAL.SegmentName(walDir, segment) + + log.Info().Msgf("moving segment file %s to %s", newSegmentFile, segmentToBeReplaced) + + err = os.Rename(newSegmentFile, segmentToBeReplaced) + if err != nil { + return fmt.Errorf("cannot move segment file %s to %s: %w", newSegmentFile, segmentToBeReplaced, err) + } + + return nil +} diff --git a/cmd/util/cmd/root.go b/cmd/util/cmd/root.go index dc80ff7203a..865eb4481c3 100644 --- a/cmd/util/cmd/root.go +++ b/cmd/util/cmd/root.go @@ -25,6 +25,7 @@ import ( export_json_transactions "github.com/onflow/flow-go/cmd/util/cmd/export-json-transactions" extractpayloads "github.com/onflow/flow-go/cmd/util/cmd/extract-payloads-by-address" find_inconsistent_result "github.com/onflow/flow-go/cmd/util/cmd/find-inconsistent-result" + find_trie_root "github.com/onflow/flow-go/cmd/util/cmd/find-trie-root" read_badger "github.com/onflow/flow-go/cmd/util/cmd/read-badger/cmd" read_execution_state "github.com/onflow/flow-go/cmd/util/cmd/read-execution-state" read_hotstuff "github.com/onflow/flow-go/cmd/util/cmd/read-hotstuff/cmd" @@ -110,6 +111,7 @@ func addCommands() { rootCmd.AddCommand(find_inconsistent_result.Cmd) rootCmd.AddCommand(diff_states.Cmd) rootCmd.AddCommand(atree_inlined_status.Cmd) + rootCmd.AddCommand(find_trie_root.Cmd) } func initConfig() { diff --git a/cmd/util/ledger/migrations/cadence_values_migration_test.go b/cmd/util/ledger/migrations/cadence_values_migration_test.go index 882952b9362..cb65054c35d 100644 --- a/cmd/util/ledger/migrations/cadence_values_migration_test.go +++ b/cmd/util/ledger/migrations/cadence_values_migration_test.go @@ -1274,6 +1274,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..3080d4bcb53 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration.go +++ b/cmd/util/ledger/migrations/contract_checking_migration.go @@ -1,6 +1,7 @@ package migrations import ( + "encoding/json" "fmt" "sort" "strings" @@ -12,12 +13,18 @@ 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" ) const contractCheckingReporterName = "contract-checking" const contractCountEstimate = 1000 +type AddressContract struct { + location common.AddressLocation + code []byte +} + // NewContractCheckingMigration returns a migration that checks all contracts. // It parses and checks all contract code and stores the programs in the provided map. func NewContractCheckingMigration( @@ -30,6 +37,7 @@ func NewContractCheckingMigration( return func(registersByAccount *registers.ByAccount) error { reporter := rwf.ReportWriter(contractCheckingReporterName) + defer reporter.Close() mr, err := NewInterpreterMigrationRuntime( registersByAccount, @@ -42,43 +50,55 @@ func NewContractCheckingMigration( // Gather all contracts + log.Info().Msg("Gathering contracts ...") + contractsForPrettyPrinting := make(map[common.Location][]byte, contractCountEstimate) - type contract struct { - location common.AddressLocation - code []byte - } - contracts := make([]contract, 0, contractCountEstimate) + contracts := make([]AddressContract, 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 + } - contractsForPrettyPrinting[location] = code + address := common.Address([]byte(owner)) + location := common.AddressLocation{ + Address: address, + Name: contractName, + } + + contracts = append( + contracts, + AddressContract{ + location: location, + code: 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,61 +107,131 @@ 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 { - location := contract.location - code := contract.code + checkContract( + contract, + log, + mr, + contractsForPrettyPrinting, + verboseErrorOutput, + reporter, + programs, + ) + } - log.Info().Msgf("checking contract %s ...", location) + return nil + } +} - // Check contract code - const getAndSetProgram = true - program, err := mr.ContractAdditionHandler.ParseAndCheckProgram(code, location, getAndSetProgram) - if err != nil { +func checkContract( + contract AddressContract, + log zerolog.Logger, + mr *InterpreterMigrationRuntime, + contractsForPrettyPrinting map[common.Location][]byte, + verboseErrorOutput bool, + reporter reporters.ReportWriter, + programs map[common.Location]*interpreter.Program, +) { + location := contract.location + code := contract.code - // Pretty print the error - var builder strings.Builder - errorPrinter := pretty.NewErrorPrettyPrinter(&builder, false) + log.Info().Msgf("checking contract %s ...", location) - printErr := errorPrinter.PrettyPrintError(err, location, contractsForPrettyPrinting) + // Check contract code + const getAndSetProgram = true + program, err := mr.ContractAdditionHandler.ParseAndCheckProgram(code, location, getAndSetProgram) + if err != nil { - var errorDetails string - if printErr == nil { - errorDetails = builder.String() - } else { - errorDetails = err.Error() - } + // Pretty print the error + var builder strings.Builder + errorPrinter := pretty.NewErrorPrettyPrinter(&builder, false) - if verboseErrorOutput { - log.Error().Msgf( - "error checking contract %s: %s", - location, - errorDetails, - ) - } + printErr := errorPrinter.PrettyPrintError(err, location, contractsForPrettyPrinting) - reporter.Write(contractCheckingFailure{ - AccountAddressHex: location.Address.HexWithPrefix(), - ContractName: location.Name, - Error: errorDetails, - }) + var errorDetails string + if printErr == nil { + errorDetails = builder.String() + } else { + errorDetails = err.Error() + } - continue - } else { - // Record the checked program for future use - programs[location] = program - } + if verboseErrorOutput { + log.Error().Msgf( + "error checking contract %s: %s", + location, + errorDetails, + ) } - reporter.Close() + reporter.Write(contractCheckingFailure{ + AccountAddress: location.Address, + ContractName: location.Name, + Code: string(code), + Error: errorDetails, + }) - return nil + return } + + // Record the checked program for future use + programs[location] = program + + reporter.Write(contractCheckingSuccess{ + AccountAddress: location.Address, + ContractName: location.Name, + Code: string(code), + }) + + log.Info().Msgf("finished checking contract %s", location) } type contractCheckingFailure struct { - AccountAddressHex string `json:"address"` - ContractName string `json:"name"` - Error string `json:"error"` + AccountAddress common.Address + ContractName string + Code string + Error string +} + +var _ json.Marshaler = contractCheckingFailure{} + +func (e contractCheckingFailure) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + AccountAddress string `json:"address"` + ContractName string `json:"name"` + Code string `json:"code"` + Error string `json:"error"` + }{ + Kind: "checking-failure", + AccountAddress: e.AccountAddress.HexWithPrefix(), + ContractName: e.ContractName, + Code: e.Code, + Error: e.Error, + }) +} + +type contractCheckingSuccess struct { + AccountAddress common.Address + ContractName string + Code string +} + +var _ json.Marshaler = contractCheckingSuccess{} + +func (e contractCheckingSuccess) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + AccountAddress string `json:"address"` + ContractName string `json:"name"` + Code string `json:"code"` + }{ + Kind: "checking-success", + AccountAddress: e.AccountAddress.HexWithPrefix(), + ContractName: e.ContractName, + Code: e.Code, + }) } diff --git a/engine/access/rpc/backend/script_executor.go b/engine/access/rpc/backend/script_executor.go index f38fdbcc8e8..1f74249b8e4 100644 --- a/engine/access/rpc/backend/script_executor.go +++ b/engine/access/rpc/backend/script_executor.go @@ -102,6 +102,30 @@ func (s *ScriptExecutor) GetAccountAtBlockHeight(ctx context.Context, address fl return s.scriptExecutor.GetAccountAtBlockHeight(ctx, address, height) } +// GetAccountBalance returns +// Expected errors: +// - Script execution related errors +// - storage.ErrHeightNotIndexed if the data for the block height is not available +func (s *ScriptExecutor) GetAccountBalance(ctx context.Context, address flow.Address, height uint64) (uint64, error) { + if err := s.checkDataAvailable(height); err != nil { + return 0, err + } + + return s.scriptExecutor.GetAccountBalance(ctx, address, height) +} + +// GetAccountKeys returns +// Expected errors: +// - Script execution related errors +// - storage.ErrHeightNotIndexed if the data for the block height is not available +func (s *ScriptExecutor) GetAccountKeys(ctx context.Context, address flow.Address, height uint64) ([]flow.AccountPublicKey, error) { + if err := s.checkDataAvailable(height); err != nil { + return nil, err + } + + return s.scriptExecutor.GetAccountKeys(ctx, address, height) +} + func (s *ScriptExecutor) checkDataAvailable(height uint64) error { if !s.initialized.Load() { return fmt.Errorf("%w: script executor not initialized", storage.ErrHeightNotIndexed) diff --git a/engine/execution/block_result.go b/engine/execution/block_result.go index e10a6aeea42..1cfbb9bc0d4 100644 --- a/engine/execution/block_result.go +++ b/engine/execution/block_result.go @@ -1,6 +1,8 @@ package execution import ( + "fmt" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/executiondatasync/execution_data" @@ -31,7 +33,7 @@ func (er *BlockExecutionResult) Size() int { } func (er *BlockExecutionResult) CollectionExecutionResultAt(colIndex int) *CollectionExecutionResult { - if colIndex < 0 && colIndex > len(er.collectionExecutionResults) { + if colIndex < 0 || colIndex > len(er.collectionExecutionResults) { return nil } return &er.collectionExecutionResults[colIndex] @@ -185,6 +187,12 @@ func (ar *BlockAttestationResult) ChunkAt(index int) *flow.Chunk { execRes := ar.collectionExecutionResults[index] attestRes := ar.collectionAttestationResults[index] + if execRes.executionSnapshot == nil { + // This should never happen + // In case it does, attach additional information to the error message + panic(fmt.Sprintf("execution snapshot is nil. Block ID: %s, EndState: %s", ar.Block.ID(), attestRes.endStateCommit)) + } + return flow.NewChunk( ar.Block.ID(), index, diff --git a/engine/execution/computation/query/executor.go b/engine/execution/computation/query/executor.go index b15ab8bdaf1..3ac50f87dc5 100644 --- a/engine/execution/computation/query/executor.go +++ b/engine/execution/computation/query/executor.go @@ -50,6 +50,26 @@ type Executor interface { *flow.Account, error, ) + + GetAccountBalance( + ctx context.Context, + addr flow.Address, + header *flow.Header, + snapshot snapshot.StorageSnapshot, + ) ( + uint64, + error, + ) + + GetAccountKeys( + ctx context.Context, + addr flow.Address, + header *flow.Header, + snapshot snapshot.StorageSnapshot, + ) ( + []flow.AccountPublicKey, + error, + ) } type QueryConfig struct { @@ -248,3 +268,50 @@ func (e *QueryExecutor) GetAccount( return account, nil } + +func (e *QueryExecutor) GetAccountBalance(ctx context.Context, address flow.Address, blockHeader *flow.Header, snapshot snapshot.StorageSnapshot) (uint64, error) { + + // TODO(ramtin): utilize ctx + blockCtx := fvm.NewContextFromParent( + e.vmCtx, + fvm.WithBlockHeader(blockHeader), + fvm.WithDerivedBlockData( + e.derivedChainData.NewDerivedBlockDataForScript(blockHeader.ID()))) + + accountBalance, err := fvm.GetAccountBalance( + blockCtx, + address, + snapshot) + + if err != nil { + return 0, fmt.Errorf( + "failed to get account balance (%s) at block (%s): %w", + address.String(), + blockHeader.ID(), + err) + } + + return accountBalance, nil +} + +func (e *QueryExecutor) GetAccountKeys(ctx context.Context, address flow.Address, blockHeader *flow.Header, snapshot snapshot.StorageSnapshot) ([]flow.AccountPublicKey, error) { + // TODO(ramtin): utilize ctx + blockCtx := fvm.NewContextFromParent( + e.vmCtx, + fvm.WithBlockHeader(blockHeader), + fvm.WithDerivedBlockData( + e.derivedChainData.NewDerivedBlockDataForScript(blockHeader.ID()))) + + accountKeys, err := fvm.GetAccountKeys(blockCtx, + address, + snapshot) + if err != nil { + return nil, fmt.Errorf( + "failed to get account keys (%s) at block (%s): %w", + address.String(), + blockHeader.ID(), + err) + } + + return accountKeys, nil +} diff --git a/engine/execution/computation/query/mock/executor.go b/engine/execution/computation/query/mock/executor.go index 9f6d372358b..5ff276961c6 100644 --- a/engine/execution/computation/query/mock/executor.go +++ b/engine/execution/computation/query/mock/executor.go @@ -83,6 +83,64 @@ func (_m *Executor) GetAccount(ctx context.Context, addr flow.Address, header *f return r0, r1 } +// GetAccountBalance provides a mock function with given fields: ctx, addr, header, _a3 +func (_m *Executor) GetAccountBalance(ctx context.Context, addr flow.Address, header *flow.Header, _a3 snapshot.StorageSnapshot) (uint64, error) { + ret := _m.Called(ctx, addr, header, _a3) + + if len(ret) == 0 { + panic("no return value specified for GetAccountBalance") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, *flow.Header, snapshot.StorageSnapshot) (uint64, error)); ok { + return rf(ctx, addr, header, _a3) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, *flow.Header, snapshot.StorageSnapshot) uint64); ok { + r0 = rf(ctx, addr, header, _a3) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Address, *flow.Header, snapshot.StorageSnapshot) error); ok { + r1 = rf(ctx, addr, header, _a3) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAccountKeys provides a mock function with given fields: ctx, addr, header, _a3 +func (_m *Executor) GetAccountKeys(ctx context.Context, addr flow.Address, header *flow.Header, _a3 snapshot.StorageSnapshot) ([]flow.AccountPublicKey, error) { + ret := _m.Called(ctx, addr, header, _a3) + + if len(ret) == 0 { + panic("no return value specified for GetAccountKeys") + } + + var r0 []flow.AccountPublicKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, *flow.Header, snapshot.StorageSnapshot) ([]flow.AccountPublicKey, error)); ok { + return rf(ctx, addr, header, _a3) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, *flow.Header, snapshot.StorageSnapshot) []flow.AccountPublicKey); ok { + r0 = rf(ctx, addr, header, _a3) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]flow.AccountPublicKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Address, *flow.Header, snapshot.StorageSnapshot) error); ok { + r1 = rf(ctx, addr, header, _a3) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // NewExecutor creates a new instance of Executor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewExecutor(t interface { diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index cd3edb6f2cb..307cec97ddd 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("7cb3d30faaaab3cb402338023b3a068bc856fb788086b1212aa0f1950f24d854") + expectedStateCommitmentBytes, _ := hex.DecodeString("3fc810ac804039b7e577053a78cad4d13b58332cb52303e098d337aa87e783a5") expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes) require.NoError(t, err) diff --git a/fvm/environment/account_info.go b/fvm/environment/account_info.go index 2f86aec2670..b6ee9f43771 100644 --- a/fvm/environment/account_info.go +++ b/fvm/environment/account_info.go @@ -15,12 +15,13 @@ import ( // AccountInfo exposes various account balance and storage statistics. type AccountInfo interface { // Cadence's runtime APIs. - GetStorageUsed(runtimeaddress common.Address) (uint64, error) + GetStorageUsed(runtimeAddress common.Address) (uint64, error) GetStorageCapacity(runtimeAddress common.Address) (uint64, error) GetAccountBalance(runtimeAddress common.Address) (uint64, error) GetAccountAvailableBalance(runtimeAddress common.Address) (uint64, error) GetAccount(address flow.Address) (*flow.Account, error) + GetAccountKeys(address flow.Address) ([]flow.AccountPublicKey, error) } type ParseRestrictedAccountInfo struct { @@ -103,6 +104,19 @@ func (info ParseRestrictedAccountInfo) GetAccount( address) } +func (info ParseRestrictedAccountInfo) GetAccountKeys( + address flow.Address, +) ( + []flow.AccountPublicKey, + error, +) { + return parseRestrict1Arg1Ret( + info.txnState, + trace.FVMEnvGetAccountKeys, + info.impl.GetAccountKeys, + address) +} + type accountInfo struct { tracer tracing.TracerSpan meter Meter @@ -152,7 +166,7 @@ func (info *accountInfo) GetStorageUsed( } // StorageMBUFixToBytesUInt converts the return type of storage capacity which -// is a UFix64 with the unit of megabytes to UInt with the unit of bytes +// is a UFix64 with the unit of megabytes to UInt with the unit of bytes. func StorageMBUFixToBytesUInt(result cadence.Value) uint64 { // Divide the unsigned int by (1e8 (the scale of Fix64) / 1e6 (for mega)) // to get bytes (rounded down) @@ -254,3 +268,20 @@ func (info *accountInfo) GetAccount( return account, nil } + +func (info *accountInfo) GetAccountKeys( + address flow.Address, +) ( + []flow.AccountPublicKey, + error, +) { + defer info.tracer.StartChildSpan(trace.FVMEnvGetAccountKeys).End() + + accountKeys, err := info.accounts.GetPublicKeys(address) + + if err != nil { + return nil, err + } + + return accountKeys, nil +} diff --git a/fvm/environment/account_key_updater_test.go b/fvm/environment/account_key_updater_test.go index bfb2fa9d2c7..2194150296a 100644 --- a/fvm/environment/account_key_updater_test.go +++ b/fvm/environment/account_key_updater_test.go @@ -165,6 +165,9 @@ func (f FakeAccounts) GetPublicKey(address flow.Address, keyIndex uint64) (flow. func (f FakeAccounts) SetPublicKey(_ flow.Address, _ uint64, _ flow.AccountPublicKey) ([]byte, error) { return nil, nil } +func (f FakeAccounts) GetPublicKeys(address flow.Address) ([]flow.AccountPublicKey, error) { + return make([]flow.AccountPublicKey, f.keyCount), nil +} func (f FakeAccounts) GetContractNames(_ flow.Address) ([]string, error) { return nil, nil } func (f FakeAccounts) GetContract(_ string, _ flow.Address) ([]byte, error) { return nil, nil } func (f FakeAccounts) ContractExists(_ string, _ flow.Address) (bool, error) { return false, nil } diff --git a/fvm/environment/accounts.go b/fvm/environment/accounts.go index 01041f19a3f..df1bf68e9c3 100644 --- a/fvm/environment/accounts.go +++ b/fvm/environment/accounts.go @@ -27,6 +27,7 @@ type Accounts interface { AppendPublicKey(address flow.Address, key flow.AccountPublicKey) error GetPublicKey(address flow.Address, keyIndex uint64) (flow.AccountPublicKey, error) SetPublicKey(address flow.Address, keyIndex uint64, publicKey flow.AccountPublicKey) ([]byte, error) + GetPublicKeys(address flow.Address) ([]flow.AccountPublicKey, error) GetContractNames(address flow.Address) ([]string, error) GetContract(contractName string, address flow.Address) ([]byte, error) ContractExists(contractName string, address flow.Address) (bool, error) @@ -256,9 +257,7 @@ func (a *StatefulAccounts) GetPublicKeys( ) { count, err := a.GetPublicKeyCount(address) if err != nil { - return nil, fmt.Errorf( - "failed to get public key count of account: %w", - err) + return nil, err } publicKeys = make([]flow.AccountPublicKey, count) @@ -431,6 +430,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 +456,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 +615,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/env.go b/fvm/environment/env.go index 031eb460dc4..b276e3645b4 100644 --- a/fvm/environment/env.go +++ b/fvm/environment/env.go @@ -4,12 +4,9 @@ import ( "github.com/onflow/cadence" "github.com/onflow/cadence/runtime" "github.com/rs/zerolog" - otelTrace "go.opentelemetry.io/otel/trace" reusableRuntime "github.com/onflow/flow-go/fvm/runtime" - "github.com/onflow/flow-go/fvm/tracing" "github.com/onflow/flow-go/model/flow" - "github.com/onflow/flow-go/module/trace" ) // Environment implements the accounts business logic and exposes cadence @@ -17,11 +14,7 @@ import ( type Environment interface { runtime.Interface - // Tracer - StartChildSpan( - name trace.SpanName, - options ...otelTrace.SpanStartOption, - ) tracing.TracerSpan + Tracer Meter @@ -70,6 +63,7 @@ type Environment interface { // AccountInfo GetAccount(address flow.Address) (*flow.Account, error) + GetAccountKeys(address flow.Address) ([]flow.AccountPublicKey, error) // RandomSourceHistory is the current block's derived random source. // This source is only used by the core-contract that tracks the random source diff --git a/fvm/environment/mock/account_info.go b/fvm/environment/mock/account_info.go index c28110c9561..96382e9ea06 100644 --- a/fvm/environment/mock/account_info.go +++ b/fvm/environment/mock/account_info.go @@ -101,6 +101,36 @@ func (_m *AccountInfo) GetAccountBalance(runtimeAddress common.Address) (uint64, return r0, r1 } +// GetAccountKeys provides a mock function with given fields: address +func (_m *AccountInfo) GetAccountKeys(address flow.Address) ([]flow.AccountPublicKey, error) { + ret := _m.Called(address) + + if len(ret) == 0 { + panic("no return value specified for GetAccountKeys") + } + + var r0 []flow.AccountPublicKey + var r1 error + if rf, ok := ret.Get(0).(func(flow.Address) ([]flow.AccountPublicKey, error)); ok { + return rf(address) + } + if rf, ok := ret.Get(0).(func(flow.Address) []flow.AccountPublicKey); ok { + r0 = rf(address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]flow.AccountPublicKey) + } + } + + if rf, ok := ret.Get(1).(func(flow.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetStorageCapacity provides a mock function with given fields: runtimeAddress func (_m *AccountInfo) GetStorageCapacity(runtimeAddress common.Address) (uint64, error) { ret := _m.Called(runtimeAddress) @@ -129,9 +159,9 @@ func (_m *AccountInfo) GetStorageCapacity(runtimeAddress common.Address) (uint64 return r0, r1 } -// GetStorageUsed provides a mock function with given fields: runtimeaddress -func (_m *AccountInfo) GetStorageUsed(runtimeaddress common.Address) (uint64, error) { - ret := _m.Called(runtimeaddress) +// GetStorageUsed provides a mock function with given fields: runtimeAddress +func (_m *AccountInfo) GetStorageUsed(runtimeAddress common.Address) (uint64, error) { + ret := _m.Called(runtimeAddress) if len(ret) == 0 { panic("no return value specified for GetStorageUsed") @@ -140,16 +170,16 @@ func (_m *AccountInfo) GetStorageUsed(runtimeaddress common.Address) (uint64, er var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(common.Address) (uint64, error)); ok { - return rf(runtimeaddress) + return rf(runtimeAddress) } if rf, ok := ret.Get(0).(func(common.Address) uint64); ok { - r0 = rf(runtimeaddress) + r0 = rf(runtimeAddress) } else { r0 = ret.Get(0).(uint64) } if rf, ok := ret.Get(1).(func(common.Address) error); ok { - r1 = rf(runtimeaddress) + r1 = rf(runtimeAddress) } else { r1 = ret.Error(1) } diff --git a/fvm/environment/mock/accounts.go b/fvm/environment/mock/accounts.go index 66c912c5f35..c71bf866acb 100644 --- a/fvm/environment/mock/accounts.go +++ b/fvm/environment/mock/accounts.go @@ -329,6 +329,36 @@ func (_m *Accounts) GetPublicKeyCount(address flow.Address) (uint64, error) { return r0, r1 } +// GetPublicKeys provides a mock function with given fields: address +func (_m *Accounts) GetPublicKeys(address flow.Address) ([]flow.AccountPublicKey, error) { + ret := _m.Called(address) + + if len(ret) == 0 { + panic("no return value specified for GetPublicKeys") + } + + var r0 []flow.AccountPublicKey + var r1 error + if rf, ok := ret.Get(0).(func(flow.Address) ([]flow.AccountPublicKey, error)); ok { + return rf(address) + } + if rf, ok := ret.Get(0).(func(flow.Address) []flow.AccountPublicKey); ok { + r0 = rf(address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]flow.AccountPublicKey) + } + } + + if rf, ok := ret.Get(1).(func(flow.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetStorageUsed provides a mock function with given fields: address func (_m *Accounts) GetStorageUsed(address flow.Address) (uint64, error) { ret := _m.Called(address) diff --git a/fvm/environment/mock/environment.go b/fvm/environment/mock/environment.go index 5f4e3ca81e6..2a8cee38f02 100644 --- a/fvm/environment/mock/environment.go +++ b/fvm/environment/mock/environment.go @@ -774,6 +774,36 @@ func (_m *Environment) GetAccountKey(address common.Address, index int) (*stdlib return r0, r1 } +// GetAccountKeys provides a mock function with given fields: address +func (_m *Environment) GetAccountKeys(address flow.Address) ([]flow.AccountPublicKey, error) { + ret := _m.Called(address) + + if len(ret) == 0 { + panic("no return value specified for GetAccountKeys") + } + + var r0 []flow.AccountPublicKey + var r1 error + if rf, ok := ret.Get(0).(func(flow.Address) ([]flow.AccountPublicKey, error)); ok { + return rf(address) + } + if rf, ok := ret.Get(0).(func(flow.Address) []flow.AccountPublicKey); ok { + r0 = rf(address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]flow.AccountPublicKey) + } + } + + if rf, ok := ret.Get(1).(func(flow.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetBlockAtHeight provides a mock function with given fields: height func (_m *Environment) GetBlockAtHeight(height uint64) (stdlib.Block, bool, error) { ret := _m.Called(height) diff --git a/fvm/environment/tracer.go b/fvm/environment/tracer.go new file mode 100644 index 00000000000..f276286475e --- /dev/null +++ b/fvm/environment/tracer.go @@ -0,0 +1,16 @@ +package environment + +import ( + otelTrace "go.opentelemetry.io/otel/trace" + + "github.com/onflow/flow-go/fvm/tracing" + "github.com/onflow/flow-go/module/trace" +) + +// Tracer captures traces +type Tracer interface { + StartChildSpan( + name trace.SpanName, + options ...otelTrace.SpanStartOption, + ) tracing.TracerSpan +} diff --git a/fvm/evm/backends/wrappedEnv.go b/fvm/evm/backends/wrappedEnv.go index d22aabc191c..a996dff05da 100644 --- a/fvm/evm/backends/wrappedEnv.go +++ b/fvm/evm/backends/wrappedEnv.go @@ -5,12 +5,15 @@ import ( "github.com/onflow/cadence" "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" + otelTrace "go.opentelemetry.io/otel/trace" "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/errors" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/meter" + "github.com/onflow/flow-go/fvm/tracing" "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/trace" ) // WrappedEnvironment wraps an FVM environment @@ -144,6 +147,13 @@ func (we *WrappedEnvironment) GenerateUUID() (uint64, error) { return uuid, handleEnvironmentError(err) } +func (we *WrappedEnvironment) StartChildSpan( + name trace.SpanName, + options ...otelTrace.SpanStartOption, +) tracing.TracerSpan { + return we.env.StartChildSpan(name, options...) +} + func handleEnvironmentError(err error) error { if err == nil { return nil diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index b86821e9f7f..afe9a7f7366 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -106,14 +106,16 @@ type BlockView struct { // DirectCall executes a direct call func (bl *BlockView) DirectCall(call *types.DirectCall) (*types.Result, error) { - proc, err := bl.newProcedure() - if err != nil { - return nil, err + // negative amounts are not acceptable. + if call.Value.Sign() < 0 { + return nil, types.ErrInvalidBalance } - txHash, err := call.Hash() + + proc, err := bl.newProcedure() if err != nil { return nil, err } + txHash := call.Hash() switch call.SubType { case types.DepositCallSubType: return proc.mintTo(call, txHash) @@ -150,6 +152,11 @@ func (bl *BlockView) RunTransaction( return types.NewInvalidResult(tx, err), nil } + // negative amounts are not acceptable. + if msg.Value.Sign() < 0 { + return nil, types.ErrInvalidBalance + } + // update tx context origin proc.evm.TxContext.Origin = msg.From res, err := proc.run(msg, tx.Hash(), 0, tx.Type()) @@ -182,6 +189,11 @@ func (bl *BlockView) BatchRunTransactions(txs []*gethTypes.Transaction) ([]*type continue } + // negative amounts are not acceptable. + if msg.Value.Sign() < 0 { + return nil, types.ErrInvalidBalance + } + // update tx context origin proc.evm.TxContext.Origin = msg.From res, err := proc.run(msg, tx.Hash(), uint(i), tx.Type()) @@ -222,6 +234,11 @@ func (bl *BlockView) DryRunTransaction( GetSigner(bl.config), proc.config.BlockContext.BaseFee, ) + // negative amounts are not acceptable. + if msg.Value.Sign() < 0 { + return nil, types.ErrInvalidBalance + } + // we can ignore invalid signature errors since we don't expect signed transctions if !errors.Is(err, gethTypes.ErrInvalidSig) { return nil, err @@ -236,9 +253,21 @@ func (bl *BlockView) DryRunTransaction( // return without commiting the state txResult, err := proc.run(msg, tx.Hash(), 0, tx.Type()) if txResult.Successful() { + // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification + // Define "all but one 64th" of N as N - floor(N / 64). + // If a call asks for more gas than the maximum allowed amount + // (i.e. the total amount of gas remaining in the parent after subtracting + // the gas cost of the call and memory expansion), do not return an OOG error; + // instead, if a call asks for more gas than all but one 64th of the maximum + // allowed amount, call with all but one 64th of the maximum allowed amount of + // gas (this is equivalent to a version of EIP-901 plus EIP-1142). + // CREATE only provides all but one 64th of the parent gas to the child call. + txResult.GasConsumed = AddOne64th(txResult.GasConsumed) + // 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 @@ -291,6 +320,11 @@ func (proc *procedure) mintTo( call *types.DirectCall, txHash gethCommon.Hash, ) (*types.Result, error) { + // negative amounts are not acceptable. + if call.Value.Sign() < 0 { + return nil, types.ErrInvalidBalance + } + bridge := call.From.ToCommon() // create bridge account if not exist @@ -325,6 +359,11 @@ func (proc *procedure) withdrawFrom( call *types.DirectCall, txHash gethCommon.Hash, ) (*types.Result, error) { + // negative amounts are not acceptable. + if call.Value.Sign() < 0 { + return nil, types.ErrInvalidBalance + } + bridge := call.To.ToCommon() // create bridge account if not exist @@ -557,3 +596,8 @@ func (proc *procedure) run( } return &res, nil } + +func AddOne64th(n uint64) uint64 { + // NOTE: Go's integer division floors, but that is desirable here + return n + (n / 64) +} diff --git a/fvm/evm/emulator/emulator_test.go b/fvm/evm/emulator/emulator_test.go index 62dbb248b62..8f1cb1bbba1 100644 --- a/fvm/evm/emulator/emulator_test.go +++ b/fvm/evm/emulator/emulator_test.go @@ -60,9 +60,7 @@ func TestNativeTokenBridging(t *testing.T) { res, err := blk.DirectCall(call) require.NoError(t, err) require.Equal(t, defaultCtx.DirectCallBaseGasUsage, res.GasConsumed) - expectedHash, err := call.Hash() - require.NoError(t, err) - require.Equal(t, expectedHash, res.TxHash) + require.Equal(t, call.Hash(), res.TxHash) nonce += 1 }) }) @@ -94,9 +92,7 @@ func TestNativeTokenBridging(t *testing.T) { res, err := blk.DirectCall(call) require.NoError(t, err) require.Equal(t, defaultCtx.DirectCallBaseGasUsage, res.GasConsumed) - expectedHash, err := call.Hash() - require.NoError(t, err) - require.Equal(t, expectedHash, res.TxHash) + require.Equal(t, call.Hash(), res.TxHash) nonce += 1 }) }) @@ -155,9 +151,7 @@ func TestContractInteraction(t *testing.T) { require.NoError(t, err) require.NotNil(t, res.DeployedContractAddress) contractAddr = *res.DeployedContractAddress - expectedHash, err := call.Hash() - require.NoError(t, err) - require.Equal(t, expectedHash, res.TxHash) + require.Equal(t, call.Hash(), res.TxHash) nonce += 1 }) RunWithNewReadOnlyBlockView(t, env, func(blk types.ReadOnlyBlockView) { diff --git a/fvm/evm/emulator/state/stateDB.go b/fvm/evm/emulator/state/stateDB.go index 64eafbeb399..803040b84b0 100644 --- a/fvm/evm/emulator/state/stateDB.go +++ b/fvm/evm/emulator/state/stateDB.go @@ -108,12 +108,22 @@ func (db *StateDB) HasSelfDestructed(addr gethCommon.Address) bool { // SubBalance substitutes the amount from the balance of the given address func (db *StateDB) SubBalance(addr gethCommon.Address, amount *big.Int) { + // negative amounts are not accepted. + if amount.Sign() < 0 { + db.handleError(types.ErrInvalidBalance) + return + } err := db.lastestView().SubBalance(addr, amount) db.handleError(err) } // AddBalance adds the amount from the balance of the given address func (db *StateDB) AddBalance(addr gethCommon.Address, amount *big.Int) { + // negative amounts are not accepted. + if amount.Sign() < 0 { + db.handleError(types.ErrInvalidBalance) + return + } err := db.lastestView().AddBalance(addr, amount) db.handleError(err) } diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 3346238a30a..971e7b99fdb 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -25,6 +25,7 @@ import ( "github.com/onflow/flow-go/fvm/crypto" envMock "github.com/onflow/flow-go/fvm/environment/mock" "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator" "github.com/onflow/flow-go/fvm/evm/stdlib" "github.com/onflow/flow-go/fvm/evm/testutils" . "github.com/onflow/flow-go/fvm/evm/testutils" @@ -1534,9 +1535,10 @@ func TestDryRun(t *testing.T) { // Make sure that gas consumed from `EVM.dryRun` is bigger // than the actual gas consumption of the equivalent // `EVM.run`. + totalGas := emulator.AddOne64th(res.GasConsumed) + gethParams.SstoreSentryGasEIP2200 require.Equal( t, - res.GasConsumed+gethParams.SstoreSentryGasEIP2200, + totalGas, dryRunResult.GasConsumed, ) }) @@ -1667,9 +1669,10 @@ func TestDryRun(t *testing.T) { // Make sure that gas consumed from `EVM.dryRun` is bigger // than the actual gas consumption of the equivalent // `EVM.run`. + totalGas := emulator.AddOne64th(res.GasConsumed) + gethParams.SstoreSentryGasEIP2200 require.Equal( t, - res.GasConsumed+gethParams.SstoreSentryGasEIP2200, + totalGas, dryRunResult.GasConsumed, ) }) @@ -1798,9 +1801,10 @@ func TestDryRun(t *testing.T) { // Make sure that gas consumed from `EVM.dryRun` is bigger // than the actual gas consumption of the equivalent // `EVM.run`. + totalGas := emulator.AddOne64th(res.GasConsumed) + gethParams.SstoreSentryGasEIP2200 + gethParams.SstoreClearsScheduleRefundEIP3529 require.Equal( t, - res.GasConsumed+gethParams.SstoreSentryGasEIP2200+gethParams.SstoreClearsScheduleRefundEIP3529, + totalGas, dryRunResult.GasConsumed, ) }) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 2a0a048ccfe..eb1dbd7822d 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -6,6 +6,7 @@ import ( "github.com/onflow/cadence/runtime/common" gethCommon "github.com/onflow/go-ethereum/common" gethTypes "github.com/onflow/go-ethereum/core/types" + "go.opentelemetry.io/otel/attribute" "github.com/onflow/flow-go/fvm/environment" fvmErrors "github.com/onflow/flow-go/fvm/errors" @@ -13,6 +14,7 @@ import ( "github.com/onflow/flow-go/fvm/evm/handler/coa" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/trace" ) // ContractHandler is responsible for triggering calls to emulator, metering, @@ -65,6 +67,8 @@ func NewContractHandler( // DeployCOA deploys a cadence-owned-account and returns the address func (h *ContractHandler) DeployCOA(uuid uint64) types.Address { + defer h.backend.StartChildSpan(trace.FVMEVMDeployCOA).End() + res, err := h.deployCOA(uuid) panicOnErrorOrInvalidOrFailedState(res, err) return *res.DeployedContractAddress @@ -119,6 +123,8 @@ func (h *ContractHandler) RunOrPanic(rlpEncodedTx []byte, coinbase types.Address // Run tries to run an rlpencoded evm transaction and // collects the gas fees and pay it to the coinbase address provided. func (h *ContractHandler) Run(rlpEncodedTx []byte, coinbase types.Address) *types.ResultSummary { + defer h.backend.StartChildSpan(trace.FVMEVMRun).End() + res, err := h.run(rlpEncodedTx, coinbase) panicOnError(err) return res.ResultSummary() @@ -129,6 +135,10 @@ func (h *ContractHandler) Run(rlpEncodedTx []byte, coinbase types.Address) *type // All transactions provided in the batch are included in a single block, // except for invalid transactions func (h *ContractHandler) BatchRun(rlpEncodedTxs [][]byte, coinbase types.Address) []*types.ResultSummary { + span := h.backend.StartChildSpan(trace.FVMEVMBatchRun) + span.SetAttributes(attribute.Int("tx_counts", len(rlpEncodedTxs))) + defer span.End() + res, err := h.batchRun(rlpEncodedTxs, coinbase) panicOnError(err) @@ -341,6 +351,8 @@ func (h *ContractHandler) DryRun( rlpEncodedTx []byte, from types.Address, ) *types.ResultSummary { + defer h.backend.StartChildSpan(trace.FVMEVMDryRun).End() + res, err := h.dryRun(rlpEncodedTx, from) panicOnError(err) return res.ResultSummary() @@ -667,6 +679,8 @@ func (a *Account) codeHash() ([]byte, error) { // Deposit deposits the token from the given vault into the flow evm main vault // and update the account balance with the new amount func (a *Account) Deposit(v *types.FLOWTokenVault) { + defer a.fch.backend.StartChildSpan(trace.FVMEVMDeposit).End() + res, err := a.deposit(v) panicOnErrorOrInvalidOrFailedState(res, err) } @@ -692,6 +706,8 @@ func (a *Account) deposit(v *types.FLOWTokenVault) (*types.Result, error) { // Withdraw deducts the balance from the account and // withdraw and return flow token from the Flex main vault. func (a *Account) Withdraw(b types.Balance) *types.FLOWTokenVault { + defer a.fch.backend.StartChildSpan(trace.FVMEVMWithdraw).End() + res, err := a.withdraw(b) panicOnErrorOrInvalidOrFailedState(res, err) @@ -745,6 +761,8 @@ func (a *Account) transfer(to types.Address, balance types.Balance) (*types.Resu // contained in the result summary as data and // the contract data is not controlled by the caller accounts func (a *Account) Deploy(code types.Code, gaslimit types.GasLimit, balance types.Balance) *types.ResultSummary { + defer a.fch.backend.StartChildSpan(trace.FVMEVMDeploy).End() + res, err := a.deploy(code, gaslimit, balance) panicOnError(err) return res.ResultSummary() @@ -771,6 +789,8 @@ func (a *Account) deploy(code types.Code, gaslimit types.GasLimit, balance types // given it doesn't goes beyond what Flow transaction allows. // the balance would be deducted from the OFA account and would be transferred to the target address func (a *Account) Call(to types.Address, data types.Data, gaslimit types.GasLimit, balance types.Balance) *types.ResultSummary { + defer a.fch.backend.StartChildSpan(trace.FVMEVMCall).End() + res, err := a.call(to, data, gaslimit, balance) panicOnError(err) return res.ResultSummary() diff --git a/fvm/evm/handler/handler_test.go b/fvm/evm/handler/handler_test.go index 829ba593309..6064a54a475 100644 --- a/fvm/evm/handler/handler_test.go +++ b/fvm/evm/handler/handler_test.go @@ -33,6 +33,7 @@ import ( "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/trace" ) // TODO add test for fatal errors @@ -1257,6 +1258,57 @@ func TestHandler_TransactionRun(t *testing.T) { }) }) }) + + t.Run("test - open tracing", func(t *testing.T) { + t.Parallel() + + testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { + testutils.RunWithTestFlowEVMRootAddress(t, backend, func(rootAddr flow.Address) { + testutils.RunWithEOATestAccount(t, backend, rootAddr, func(eoa *testutils.EOATestAccount) { + + tx := gethTypes.NewTransaction( + uint64(1), + gethCommon.Address{1, 2}, + big.NewInt(13), + uint64(0), + big.NewInt(1000), + []byte{}, + ) + + rlpTx, err := tx.MarshalBinary() + require.NoError(t, err) + + handler := SetupHandler(t, backend, rootAddr) + + backend.ExpectedSpan(t, trace.FVMEVMDryRun) + handler.DryRun(rlpTx, types.EmptyAddress) + + backend.ExpectedSpan(t, trace.FVMEVMRun) + handler.Run(rlpTx, types.EmptyAddress) + + backend.ExpectedSpan(t, trace.FVMEVMBatchRun) + handler.BatchRun([][]byte{rlpTx}, types.EmptyAddress) + + backend.ExpectedSpan(t, trace.FVMEVMDeployCOA) + coa := handler.DeployCOA(1) + + acc := handler.AccountByAddress(coa, true) + + backend.ExpectedSpan(t, trace.FVMEVMCall) + acc.Call(types.EmptyAddress, nil, 1000, types.EmptyBalance) + + backend.ExpectedSpan(t, trace.FVMEVMDeposit) + acc.Deposit(types.NewFlowTokenVault(types.EmptyBalance)) + + backend.ExpectedSpan(t, trace.FVMEVMWithdraw) + acc.Withdraw(types.EmptyBalance) + + backend.ExpectedSpan(t, trace.FVMEVMDeploy) + acc.Deploy(nil, 1, types.EmptyBalance) + }) + }) + }) + }) } // returns true if error passes the checks diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index ededac7781d..14772ffba92 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -77,14 +77,28 @@ contract EVM { /// FLOWTokensDeposited is emitted when FLOW tokens is bridged /// into the EVM environment. Note that this event is not emitted /// for transfer of flow tokens between two EVM addresses. + /// Similar to the FungibleToken.Deposited event + /// this event includes a depositedUUID that captures the + /// uuid of the source vault. access(all) - event FLOWTokensDeposited(address: String, amount: UFix64) + event FLOWTokensDeposited( + address: String, + amount: UFix64, + depositedUUID: UInt64 + ) /// FLOWTokensWithdrawn is emitted when FLOW tokens are bridged /// out of the EVM environment. Note that this event is not emitted /// for transfer of flow tokens between two EVM addresses. + /// similar to the FungibleToken.Withdrawn events + /// this event includes a withdrawnUUID that captures the + /// uuid of the returning vault. access(all) - event FLOWTokensWithdrawn(address: String, amount: UFix64) + event FLOWTokensWithdrawn( + address: String, + amount: UFix64, + withdrawnUUID: UInt64 + ) /// BridgeAccessorUpdated is emitted when the BridgeAccessor Capability /// is updated in the stored BridgeRouter along with identifying @@ -152,11 +166,16 @@ contract EVM { if amount == 0.0 { panic("calling deposit function with an empty vault is not allowed") } + let depositedUUID = from.uuid InternalEVM.deposit( from: <-from, to: self.bytes ) - emit FLOWTokensDeposited(address: self.toString(), amount: amount) + emit FLOWTokensDeposited( + address: self.toString(), + amount: amount, + depositedUUID: depositedUUID + ) } /// Serializes the address to a hex string without the 0x prefix @@ -383,7 +402,11 @@ contract EVM { from: self.addressBytes, amount: balance.attoflow ) as! @FlowToken.Vault - emit FLOWTokensWithdrawn(address: self.address().toString(), amount: balance.inFLOW()) + emit FLOWTokensWithdrawn( + address: self.address().toString(), + amount: balance.inFLOW(), + withdrawnUUID: vault.uuid + ) return <-vault } diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 696b3c9dfb7..a3af22e26a9 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -3834,6 +3834,14 @@ func TestCOADeposit(t *testing.T) { expectedBalance, tokenDepositEventFields["amount"], ) + + // check depositedUUID, based on the transaction content + // its expected the uuid of 4 be allocated to the source vault. + expectedDepositedUUID := cadence.UInt64(4) + require.Equal(t, + expectedDepositedUUID, + tokenDepositEventFields["depositedUUID"], + ) } func TestCadenceOwnedAccountWithdraw(t *testing.T) { @@ -4004,6 +4012,14 @@ func TestCadenceOwnedAccountWithdraw(t *testing.T) { expectedWithdrawBalance, tokenWithdrawEventFields["amount"], ) + + // check expectedWithdrawnUUID + // last allocated UUID is 1 + expectedWithdrawnUUID := cadence.UInt64(1) + require.Equal(t, + expectedWithdrawnUUID, + tokenWithdrawEventFields["withdrawnUUID"], + ) } func TestCadenceOwnedAccountDeploy(t *testing.T) { diff --git a/fvm/evm/testutils/backend.go b/fvm/evm/testutils/backend.go index 472d38201f4..6d5616a488e 100644 --- a/fvm/evm/testutils/backend.go +++ b/fvm/evm/testutils/backend.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/onflow/cadence/runtime/stdlib" + otelTrace "go.opentelemetry.io/otel/trace" "github.com/onflow/atree" "github.com/onflow/cadence" @@ -19,7 +20,9 @@ import ( "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/meter" + "github.com/onflow/flow-go/fvm/tracing" "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/trace" ) var TestFlowEVMRootAddress = flow.BytesToAddress([]byte("FlowEVM")) @@ -40,6 +43,7 @@ func RunWithTestBackend(t testing.TB, f func(*TestBackend)) { TestBlockInfo: getSimpleBlockStore(), TestRandomGenerator: getSimpleRandomGenerator(), TestContractFunctionInvoker: &TestContractFunctionInvoker{}, + TestTracer: &TestTracer{}, } f(tb) } @@ -185,6 +189,7 @@ type TestBackend struct { *TestRandomGenerator *TestContractFunctionInvoker *testUUIDGenerator + *TestTracer } var _ types.Backend = &TestBackend{} @@ -497,3 +502,30 @@ func (t *testUUIDGenerator) GenerateUUID() (uint64, error) { } return t.generateUUID() } + +type TestTracer struct { + StartChildSpanFunc func(trace.SpanName, ...otelTrace.SpanStartOption) tracing.TracerSpan +} + +var _ environment.Tracer = &TestTracer{} + +func (tt *TestTracer) StartChildSpan( + name trace.SpanName, + options ...otelTrace.SpanStartOption, +) tracing.TracerSpan { + // if not set we use noop tracer + if tt.StartChildSpanFunc == nil { + return tracing.NewMockTracerSpan() + } + return tt.StartChildSpanFunc(name, options...) +} + +func (tt *TestTracer) ExpectedSpan(t *testing.T, expected trace.SpanName) { + tt.StartChildSpanFunc = func( + sn trace.SpanName, + sso ...otelTrace.SpanStartOption, + ) tracing.TracerSpan { + require.Equal(t, expected, sn) + return tracing.NewMockTracerSpan() + } +} diff --git a/fvm/evm/types/backend.go b/fvm/evm/types/backend.go index 2a281f94a6b..23fd2c5e377 100644 --- a/fvm/evm/types/backend.go +++ b/fvm/evm/types/backend.go @@ -15,4 +15,5 @@ type Backend interface { environment.RandomGenerator environment.ContractFunctionInvoker environment.UUIDGenerator + environment.Tracer } diff --git a/fvm/evm/types/call.go b/fvm/evm/types/call.go index b766b0f641d..e10cab278f3 100644 --- a/fvm/evm/types/call.go +++ b/fvm/evm/types/call.go @@ -73,10 +73,12 @@ func (dc *DirectCall) Encode() ([]byte, error) { } // Hash computes the hash of a direct call -func (dc *DirectCall) Hash() (gethCommon.Hash, error) { +func (dc *DirectCall) Hash() gethCommon.Hash { // we use geth transaction hash calculation since direct call hash is included in the // block transaction hashes, and thus observed as any other transaction - return dc.Transaction().Hash(), nil + // We construct this Legacy tx type so the external 3rd party tools + // don't have to support a new type for the purpose of hash computation + return dc.Transaction().Hash() } // Message constructs a core.Message from the direct call @@ -98,6 +100,11 @@ func (dc *DirectCall) Message() *gethCore.Message { // Transaction constructs a geth.Transaction from the direct call func (dc *DirectCall) Transaction() *gethTypes.Transaction { + // Since a direct call doesn't have a valid siganture + // and we need to somehow include the From feild for the purpose + // of hash calculation. we define the canonical format by + // using the FROM bytes to set the bytes for the R part of the tx (big endian), + // S captures the subtype of transaction and V is set to DirectCallTxType (255). return gethTypes.NewTx(&gethTypes.LegacyTx{ GasPrice: big.NewInt(0), Gas: dc.GasLimit, @@ -105,6 +112,9 @@ func (dc *DirectCall) Transaction() *gethTypes.Transaction { Value: dc.Value, Data: dc.Data, Nonce: dc.Nonce, + R: new(big.Int).SetBytes(dc.From.Bytes()), + S: new(big.Int).SetBytes([]byte{dc.SubType}), + V: new(big.Int).SetBytes([]byte{DirectCallTxType}), }) } diff --git a/fvm/evm/types/call_test.go b/fvm/evm/types/call_test.go index 54880558aa9..8c57600968f 100644 --- a/fvm/evm/types/call_test.go +++ b/fvm/evm/types/call_test.go @@ -1,9 +1,13 @@ package types import ( + "bytes" + "io" "math/big" "testing" + gethTypes "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/rlp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,20 +24,43 @@ func TestDirectCall(t *testing.T) { } t.Run("calculate hash", func(t *testing.T) { - h, err := dc.Hash() + h := dc.Hash() + assert.Equal(t, "0xed76124cc3c59f13e1113f5c380e2a67dab9bf616afc645073d2491fe3aecb62", h.Hex()) + + // the hash should stay the same after RLP encoding and decoding + var b bytes.Buffer + writer := io.Writer(&b) + err := dc.Transaction().EncodeRLP(writer) + require.NoError(t, err) + + reconstructedTx := &gethTypes.Transaction{} + err = reconstructedTx.DecodeRLP(rlp.NewStream(io.Reader(&b), 1000)) require.NoError(t, err) - assert.Equal(t, "0xe28ff08eca95608646d765e3007b3710f7f2a8ac5e297431da1962c33487e7b6", h.Hex()) + + h = reconstructedTx.Hash() + assert.Equal(t, "0xed76124cc3c59f13e1113f5c380e2a67dab9bf616afc645073d2491fe3aecb62", h.Hex()) + }) + + t.Run("same content except `from` should result in different hashes", func(t *testing.T) { + h := dc.Hash() + dc.From = Address{0x4, 0x5} + h2 := dc.Hash() + assert.NotEqual(t, h2.Hex(), h.Hex()) }) t.Run("construct transaction", func(t *testing.T) { tx := dc.Transaction() - h, err := dc.Hash() - require.NoError(t, err) + h := dc.Hash() assert.Equal(t, dc.Value, tx.Value()) assert.Equal(t, dc.To.ToCommon(), *tx.To()) assert.Equal(t, h, tx.Hash()) assert.Equal(t, dc.GasLimit, tx.Gas()) assert.Equal(t, dc.Data, tx.Data()) assert.Equal(t, uint64(0), tx.Nonce()) // no nonce exists for direct call + + v, r, s := tx.RawSignatureValues() + require.Equal(t, dc.From.Bytes(), r.Bytes()) + require.Equal(t, []byte{dc.SubType}, s.Bytes()) + require.Equal(t, []byte{DirectCallTxType}, v.Bytes()) }) } diff --git a/fvm/evm/types/errors.go b/fvm/evm/types/errors.go index bf69e2cd4f7..1b249ab6ac9 100644 --- a/fvm/evm/types/errors.go +++ b/fvm/evm/types/errors.go @@ -87,8 +87,8 @@ const ( ) var ( - // ErrInvalidBalance is returned when an invalid balance is provided for transfer (e.g. negative) - ErrInvalidBalance = errors.New("invalid balance for transfer") + // ErrInvalidBalance is returned when an invalid amount is provided for transfer or balance change (e.g. negative) + ErrInvalidBalance = errors.New("invalid amount for transfer or balance change") // ErrInsufficientComputation is returned when not enough computation is // left in the context of flow transaction to execute the evm operation. diff --git a/fvm/fvm.go b/fvm/fvm.go index d58978af57a..fa90f762760 100644 --- a/fvm/fvm.go +++ b/fvm/fvm.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/onflow/cadence" + "github.com/onflow/cadence/runtime/common" "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/errors" @@ -83,7 +84,7 @@ func Run(executor ProcedureExecutor) error { return executor.Execute() } -// An Procedure is an operation (or set of operations) that reads or writes ledger state. +// A Procedure is an operation (or set of operations) that reads or writes ledger state. type Procedure interface { NewExecutor( ctx Context, @@ -211,6 +212,61 @@ func (vm *VirtualMachine) GetAccount( *flow.Account, error, ) { + env := getScriptEnvironment(ctx, storageSnapshot) + + account, err := env.GetAccount(address) + if err != nil { + if errors.IsLedgerFailure(err) { + return nil, fmt.Errorf( + "cannot get account, this error usually happens if the "+ + "reference block for this query is not set to a recent "+ + "block: %w", + err) + } + return nil, fmt.Errorf("cannot get account: %w", err) + } + return account, nil +} + +// GetAccountBalance returns an account balance by address or an error if none exists. +func GetAccountBalance( + ctx Context, + address flow.Address, + storageSnapshot snapshot.StorageSnapshot, +) ( + uint64, + error, +) { + env := getScriptEnvironment(ctx, storageSnapshot) + + accountBalance, err := env.GetAccountBalance(common.MustBytesToAddress(address.Bytes())) + + if err != nil { + return 0, fmt.Errorf("cannot get account balance: %w", err) + } + return accountBalance, nil +} + +// GetAccountKeys returns an account keys by address or an error if none exists. +func GetAccountKeys( + ctx Context, + address flow.Address, + storageSnapshot snapshot.StorageSnapshot, +) ( + []flow.AccountPublicKey, + error, +) { + env := getScriptEnvironment(ctx, storageSnapshot) + + accountKeys, err := env.GetAccountKeys(address) + if err != nil { + return nil, fmt.Errorf("cannot get account keys: %w", err) + } + return accountKeys, nil +} + +// Helper function to initialize common components. +func getScriptEnvironment(ctx Context, storageSnapshot snapshot.StorageSnapshot) environment.Environment { blockDatabase := storage.NewBlockDatabase( storageSnapshot, 0, @@ -229,16 +285,6 @@ func (vm *VirtualMachine) GetAccount( ctx.TracerSpan, ctx.EnvironmentParams, storageTxn) - account, err := env.GetAccount(address) - if err != nil { - if errors.IsLedgerFailure(err) { - return nil, fmt.Errorf( - "cannot get account, this error usually happens if the "+ - "reference block for this query is not set to a recent "+ - "block: %w", - err) - } - return nil, fmt.Errorf("cannot get account: %w", err) - } - return account, nil + + return env } diff --git a/integration/tests/access/cohort3/grpc_streaming_blocks_test.go b/integration/tests/access/cohort3/grpc_streaming_blocks_test.go index 96c7655406a..a41896e11db 100644 --- a/integration/tests/access/cohort3/grpc_streaming_blocks_test.go +++ b/integration/tests/access/cohort3/grpc_streaming_blocks_test.go @@ -146,11 +146,6 @@ func (s *GrpcBlocksStreamSuite) TestHappyPath() { currentFinalized := s.BlockState.HighestFinalizedHeight() blockA := s.BlockState.WaitForHighestFinalizedProgress(s.T(), currentFinalized) - // Let the network run for this many blocks - blockCount := uint64(5) - // wait for the requested number of sealed blocks - s.BlockState.WaitForSealedHeight(s.T(), blockA.Header.Height+blockCount) - var startValue interface{} txCount := 10 @@ -187,8 +182,7 @@ func (s *GrpcBlocksStreamSuite) TestHappyPath() { foundANTxCount++ case block := <-observerBlocks: s.T().Logf("ON block received: height: %d", block.Header.Height) - r.Add(s.T(), block.Header.Height, "observer", block) - foundONTxCount++ + s.addObserverBlock(block, r, rpc.name, &foundONTxCount) } if foundANTxCount >= txCount && foundONTxCount >= txCount { @@ -201,6 +195,33 @@ func (s *GrpcBlocksStreamSuite) TestHappyPath() { } } +// addObserverBlock adds a block received from the observer node to the response tracker +// and increments the transaction count for that node. +// +// Parameters: +// - block: The block received from the node. +// - responseTracker: The response tracker to which the block should be added. +// - rpcCallName: The name of the rpc subscription call which is testing. +// - txCount: A pointer to an integer that tracks the number of transactions received from the node. +func (s *GrpcBlocksStreamSuite) addObserverBlock( + block *flow.Block, + responseTracker *ResponseTracker[*flow.Block], + rpcCallName string, + txCount *int, +) { + // the response tracker expects to receive data for the same heights from each node. + // when subscribing to the latest block, the specific start height depends on the node's + // local sealed height, so it may vary. + // check only the responses for ON that are also tracked by AN and compare them + isANResponseExist := len(responseTracker.r[block.Header.Height]) > 0 + if rpcCallName == "SubscribeBlocksFromLatest" && !isANResponseExist { + return + } + + responseTracker.Add(s.T(), block.Header.Height, "observer", block) + *txCount++ +} + func blockResponseHandler(msg *accessproto.SubscribeBlocksResponse) (*flow.Block, error) { return convert.MessageToBlock(msg.GetBlock()) } diff --git a/ledger/complete/wal/encoding.go b/ledger/complete/wal/encoding.go index 8bc5f8d6d13..dce4f084f93 100644 --- a/ledger/complete/wal/encoding.go +++ b/ledger/complete/wal/encoding.go @@ -51,6 +51,10 @@ func EncodeDelete(rootHash ledger.RootHash) []byte { return buf } +// Decode decodes the given data into a WAL operation, root hash and trie update. +// It returns (WALDelete, rootHash, nil, nil) if the operation is WALDelete. +// It returns (WALUpdate, hash.DummyHash, update, nil) if the operation is WALUpdate. +// To read the root hash of the trie update, use update.RootHash. func Decode(data []byte) (operation WALOperation, rootHash ledger.RootHash, update *ledger.TrieUpdate, err error) { if len(data) < 4 { // 1 byte op + 2 size + actual data = 4 minimum err = fmt.Errorf("data corrupted, too short to represent operation - hexencoded data: %x", data) diff --git a/module/execution/mock/script_executor.go b/module/execution/mock/script_executor.go index 113032aa4f7..a509489e0f2 100644 --- a/module/execution/mock/script_executor.go +++ b/module/execution/mock/script_executor.go @@ -75,6 +75,64 @@ func (_m *ScriptExecutor) GetAccountAtBlockHeight(ctx context.Context, address f return r0, r1 } +// GetAccountBalance provides a mock function with given fields: ctx, address, height +func (_m *ScriptExecutor) GetAccountBalance(ctx context.Context, address flow.Address, height uint64) (uint64, error) { + ret := _m.Called(ctx, address, height) + + if len(ret) == 0 { + panic("no return value specified for GetAccountBalance") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, uint64) (uint64, error)); ok { + return rf(ctx, address, height) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, uint64) uint64); ok { + r0 = rf(ctx, address, height) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Address, uint64) error); ok { + r1 = rf(ctx, address, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAccountKeys provides a mock function with given fields: ctx, address, height +func (_m *ScriptExecutor) GetAccountKeys(ctx context.Context, address flow.Address, height uint64) ([]flow.AccountPublicKey, error) { + ret := _m.Called(ctx, address, height) + + if len(ret) == 0 { + panic("no return value specified for GetAccountKeys") + } + + var r0 []flow.AccountPublicKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, uint64) ([]flow.AccountPublicKey, error)); ok { + return rf(ctx, address, height) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, uint64) []flow.AccountPublicKey); ok { + r0 = rf(ctx, address, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]flow.AccountPublicKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Address, uint64) error); ok { + r1 = rf(ctx, address, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // NewScriptExecutor creates a new instance of ScriptExecutor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewScriptExecutor(t interface { diff --git a/module/execution/scripts.go b/module/execution/scripts.go index cb7b048de12..8b8dc51204e 100644 --- a/module/execution/scripts.go +++ b/module/execution/scripts.go @@ -3,13 +3,12 @@ package execution import ( "context" - "github.com/onflow/flow-go/fvm/environment" - "github.com/rs/zerolog" "github.com/onflow/flow-go/engine/execution/computation" "github.com/onflow/flow-go/engine/execution/computation/query" "github.com/onflow/flow-go/fvm" + "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/storage/derived" "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/model/flow" @@ -42,6 +41,16 @@ type ScriptExecutor interface { // Expected errors: // - storage.ErrHeightNotIndexed if the data for the block height is not available GetAccountAtBlockHeight(ctx context.Context, address flow.Address, height uint64) (*flow.Account, error) + + // GetAccountBalance returns + // Expected errors: + // - storage.ErrHeightNotIndexed if the data for the block height is not available + GetAccountBalance(ctx context.Context, address flow.Address, height uint64) (uint64, error) + + // GetAccountKeys returns + // Expected errors: + // - storage.ErrHeightNotIndexed if the data for the block height is not available + GetAccountKeys(ctx context.Context, address flow.Address, height uint64) ([]flow.AccountPublicKey, error) } var _ ScriptExecutor = (*Scripts)(nil) @@ -126,6 +135,32 @@ func (s *Scripts) GetAccountAtBlockHeight(ctx context.Context, address flow.Addr return s.executor.GetAccount(ctx, address, header, snap) } +// GetAccountBalance returns a balance of Flow account by the provided address and block height. +// Expected errors: +// - Script execution related errors +// - storage.ErrHeightNotIndexed if the data for the block height is not available +func (s *Scripts) GetAccountBalance(ctx context.Context, address flow.Address, height uint64) (uint64, error) { + snap, header, err := s.snapshotWithBlock(height) + if err != nil { + return 0, err + } + + return s.executor.GetAccountBalance(ctx, address, header, snap) +} + +// GetAccountKeys returns a public keys of Flow account by the provided address and block height. +// Expected errors: +// - Script execution related errors +// - storage.ErrHeightNotIndexed if the data for the block height is not available +func (s *Scripts) GetAccountKeys(ctx context.Context, address flow.Address, height uint64) ([]flow.AccountPublicKey, error) { + snap, header, err := s.snapshotWithBlock(height) + if err != nil { + return nil, err + } + + return s.executor.GetAccountKeys(ctx, address, header, snap) +} + // snapshotWithBlock is a common function for executing scripts and get account functionality. // It creates a storage snapshot that is needed by the FVM to execute scripts. func (s *Scripts) snapshotWithBlock(height uint64) (snapshot.StorageSnapshot, *flow.Header, error) { diff --git a/module/execution/scripts_test.go b/module/execution/scripts_test.go index e950e3e5507..28653047b45 100644 --- a/module/execution/scripts_test.go +++ b/module/execution/scripts_test.go @@ -6,23 +6,23 @@ import ( "os" "testing" - "github.com/onflow/cadence/runtime/stdlib" - - "github.com/onflow/flow-go/fvm/errors" - "github.com/onflow/flow-go/fvm/storage/derived" - "github.com/onflow/cadence" "github.com/onflow/cadence/encoding/ccf" jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/cadence/runtime/stdlib" "github.com/rs/zerolog" mocks "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/onflow/flow-go/engine/execution/computation/query" "github.com/onflow/flow-go/engine/execution/computation/query/mock" "github.com/onflow/flow-go/engine/execution/testutil" "github.com/onflow/flow-go/fvm" + "github.com/onflow/flow-go/fvm/errors" + "github.com/onflow/flow-go/fvm/storage/derived" "github.com/onflow/flow-go/fvm/storage/snapshot" + "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/metrics" "github.com/onflow/flow-go/module/state_synchronization/indexer" @@ -131,6 +131,29 @@ func (s *scriptTestSuite) TestGetAccount() { }) } +func (s *scriptTestSuite) TestGetAccountBalance() { + address := s.createAccount() + var transferAmount uint64 = 100000000 + s.transferTokens(address, transferAmount) + balance, err := s.scripts.GetAccountBalance(context.Background(), address, s.height) + s.Require().NoError(err) + s.Require().Equal(transferAmount, balance) +} + +func (s *scriptTestSuite) TestGetAccountKeys() { + address := s.createAccount() + publicKey := s.addAccountKey(address, accountKeyAPIVersionV2) + + accountKeys, err := s.scripts.GetAccountKeys(context.Background(), address, s.height) + s.Require().NoError(err) + s.Assert().Equal(1, len(accountKeys)) + s.Assert().Equal(publicKey.PublicKey, accountKeys[0].PublicKey) + s.Assert().Equal(publicKey.SignAlgo, accountKeys[0].SignAlgo) + s.Assert().Equal(publicKey.HashAlgo, accountKeys[0].HashAlgo) + s.Assert().Equal(publicKey.Weight, accountKeys[0].Weight) + +} + func (s *scriptTestSuite) SetupTest() { logger := unittest.LoggerForTest(s.Suite.T(), zerolog.InfoLevel) entropyProvider := testutil.EntropyProviderFixture(nil) @@ -264,6 +287,104 @@ func (s *scriptTestSuite) createAccount() flow.Address { ) } +func (s *scriptTestSuite) transferTokens(accountAddress flow.Address, amount uint64) { + transferTx := transferTokensTx(s.chain). + AddArgument(jsoncdc.MustEncode(cadence.UFix64(amount))). + AddArgument(jsoncdc.MustEncode(cadence.Address(accountAddress))). + AddAuthorizer(s.chain.ServiceAddress()) + + executionSnapshot, _, err := s.vm.Run( + s.vmCtx, + fvm.Transaction(transferTx, 0), + s.snapshot, + ) + s.Require().NoError(err) + + s.height++ + err = s.registerIndex.Store(executionSnapshot.UpdatedRegisters(), s.height) + s.Require().NoError(err) + + s.snapshot = s.snapshot.Append(executionSnapshot) +} + +type accountKeyAPIVersion string + +const ( + accountKeyAPIVersionV1 accountKeyAPIVersion = "V1" + accountKeyAPIVersionV2 accountKeyAPIVersion = "V2" +) + +func (s *scriptTestSuite) addAccountKey( + accountAddress flow.Address, + apiVersion accountKeyAPIVersion, +) flow.AccountPublicKey { + const addAccountKeyTransaction = ` +transaction(key: [UInt8]) { + prepare(signer: auth(AddKey) &Account) { + let publicKey = PublicKey( + publicKey: key, + signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 + ) + signer.keys.add( + publicKey: publicKey, + hashAlgorithm: HashAlgorithm.SHA3_256, + weight: 1000.0 + ) + } +} +` + privateKey, err := unittest.AccountKeyDefaultFixture() + s.Require().NoError(err) + + publicKey, encodedCadencePublicKey := newAccountKey(s.T(), privateKey, apiVersion) + + txBody := flow.NewTransactionBody(). + SetScript([]byte(addAccountKeyTransaction)). + AddArgument(encodedCadencePublicKey). + AddAuthorizer(accountAddress) + + executionSnapshot, _, err := s.vm.Run( + s.vmCtx, + fvm.Transaction(txBody, 0), + s.snapshot, + ) + s.Require().NoError(err) + + s.height++ + err = s.registerIndex.Store(executionSnapshot.UpdatedRegisters(), s.height) + s.Require().NoError(err) + + s.snapshot = s.snapshot.Append(executionSnapshot) + + return publicKey +} + +func newAccountKey( + tb testing.TB, + privateKey *flow.AccountPrivateKey, + apiVersion accountKeyAPIVersion, +) ( + publicKey flow.AccountPublicKey, + encodedCadencePublicKey []byte, +) { + publicKey = privateKey.PublicKey(fvm.AccountKeyWeightThreshold) + + var publicKeyBytes []byte + if apiVersion == accountKeyAPIVersionV1 { + var err error + publicKeyBytes, err = flow.EncodeRuntimeAccountPublicKey(publicKey) + require.NoError(tb, err) + } else { + publicKeyBytes = publicKey.PublicKey.Encode() + } + + cadencePublicKey := testutil.BytesToCadenceArray(publicKeyBytes) + encodedCadencePublicKey, err := jsoncdc.Encode(cadencePublicKey) + require.NoError(tb, err) + + return publicKey, encodedCadencePublicKey +} + func newBlockHeadersStorage(blocks []*flow.Block) storage.Headers { blocksByHeight := make(map[uint64]*flow.Block) for _, b := range blocks { @@ -272,3 +393,53 @@ func newBlockHeadersStorage(blocks []*flow.Block) storage.Headers { return synctest.MockBlockHeaderStorage(synctest.WithByHeight(blocksByHeight)) } + +func transferTokensTx(chain flow.Chain) *flow.TransactionBody { + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + + return flow.NewTransactionBody(). + SetScript([]byte(fmt.Sprintf( + ` + // This transaction is a template for a transaction that + // could be used by anyone to send tokens to another account + // that has been set up to receive tokens. + // + // The withdraw amount and the account from getAccount + // would be the parameters to the transaction + + import FungibleToken from 0x%s + import FlowToken from 0x%s + + transaction(amount: UFix64, to: Address) { + + // The Vault resource that holds the tokens that are being transferred + let sentVault: @{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue) &Account) { + + // Get a reference to the signer's stored vault + let vaultRef = signer.storage.borrow(from: /storage/flowTokenVault) + ?? panic("Could not borrow reference to the owner's Vault!") + + // Withdraw tokens from the signer's stored vault + self.sentVault <- vaultRef.withdraw(amount: amount) + } + + execute { + + // Get the recipient's public account object + let recipient = getAccount(to) + + // Get a reference to the recipient's Receiver + let receiverRef = recipient.capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + ?? panic("Could not borrow receiver reference to the recipient's Vault") + + // Deposit the withdrawn tokens in the recipient's receiver + receiverRef.deposit(from: <-self.sentVault) + } + }`, + sc.FungibleToken.Address.Hex(), + sc.FlowToken.Address.Hex(), + )), + ) +} diff --git a/module/trace/constants.go b/module/trace/constants.go index 0b349bc360c..43287e45e93 100644 --- a/module/trace/constants.go +++ b/module/trace/constants.go @@ -162,6 +162,7 @@ const ( FVMEnvGetStorageCapacity SpanName = "fvm.env.getStorageCapacity" FVMEnvGetAccountBalance SpanName = "fvm.env.getAccountBalance" FVMEnvGetAccountAvailableBalance SpanName = "fvm.env.getAccountAvailableBalance" + FVMEnvGetAccountKeys SpanName = "fvm.env.getAccountKeys" FVMEnvResolveLocation SpanName = "fvm.env.resolveLocation" FVMEnvGetCode SpanName = "fvm.env.getCode" FVMEnvGetAccountContractNames SpanName = "fvm.env.getAccountContractNames" @@ -194,5 +195,14 @@ const ( FVMEnvRemoveAccountContractCode SpanName = "fvm.env.removeAccountContractCode" FVMEnvGetSigningAccounts SpanName = "fvm.env.getSigningAccounts" + FVMEVMDeployCOA SpanName = "fvm.evm.deployCOA" + FVMEVMRun SpanName = "fvm.evm.run" + FVMEVMDryRun SpanName = "fvm.evm.dryRun" + FVMEVMBatchRun SpanName = "fvm.evm.batchRun" + FVMEVMDeposit SpanName = "fvm.evm.deposit" + FVMEVMWithdraw SpanName = "fvm.evm.withdraw" + FVMEVMDeploy SpanName = "fvm.evm.deploy" + FVMEVMCall SpanName = "fvm.evm.call" + FVMCadenceTrace SpanName = "fvm.cadence.trace" ) 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/tools/move-checkpoint.sh b/tools/move-checkpoint.sh new file mode 100755 index 00000000000..0b1026206e7 --- /dev/null +++ b/tools/move-checkpoint.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +# Check if exactly two arguments are provided +usage() { + echo "Description: move checkpoint files from one directory to another" + echo "Usage: $0 source_file destination_file [--run]" + echo "Example: $0 /var/flow/from-folder/checkpoint.000010 /var/flow/to-folder/root.checkpoint [--run]" + echo "The above command will preview the checkpoint files to be moved including its 17 subfiles to the destination folder and rename them." + echo "Preview mode is default. Use --run to actually move the files." + exit 1 +} + +# Check if at least two arguments are provided +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + usage +fi + +# Assign arguments to variables +source_file_pattern=$1 +destination_file_base=$2 +run_mode=false + +# Check for run mode +if [ "$#" -eq 3 ] && [ "$3" == "--run" ]; then + run_mode=true +elif [ "$#" -eq 3 ]; then + usage +fi + +# Extract the basename from the source file pattern +source_base=$(basename "$source_file_pattern") + +# Extract the directory and base name from the destination file base +source_directory=$(dirname "$source_file_pattern") +destination_directory=$(dirname "$destination_file_base") +destination_base=$(basename "$destination_file_base") + +# Create the destination directory if it doesn't exist +mkdir -p "$destination_directory" + +# Define the expected files +expected_files=( + "$source_base" + "$source_base.001" + "$source_base.002" + "$source_base.003" + "$source_base.004" + "$source_base.005" + "$source_base.006" + "$source_base.007" + "$source_base.008" + "$source_base.009" + "$source_base.010" + "$source_base.011" + "$source_base.012" + "$source_base.013" + "$source_base.014" + "$source_base.015" + "$source_base.016" +) + +# Check if all expected files are present +missing_files=() +for expected_file in "${expected_files[@]}"; do + full_expected_file="$source_directory/$expected_file" + if [ ! -f "$full_expected_file" ]; then + missing_files+=("$full_expected_file") + fi +done + +if [ "${#missing_files[@]}" -ne 0 ]; then + echo "Error: The following expected files are missing:" + for file in "${missing_files[@]}"; do + echo " $file" + done + exit 1 +fi + +# Loop through the expected files and preview/move them to the destination directory +for file in "${expected_files[@]}"; do + full_source_file="$source_directory/$file" + if [ -f "$full_source_file" ]; then + # Get the file extension (if any) + extension="${file#$source_base}" + # Construct the destination file name + destination_file="$destination_directory/$destination_base$extension" + # Preview or move the file + if [ "$run_mode" = true ]; then + echo "Moving: $(realpath "$full_source_file") -> $(realpath "$destination_file")" + mv "$full_source_file" "$destination_file" + else + echo "Preview: $(realpath "$full_source_file") -> $(realpath "$destination_file")" + fi + fi +done + + +if [ "$run_mode" = true ]; then + echo "Checkpoint files have been moved successfully." +else + echo "Preview complete. No files have been moved. add --run flag to move the files." +fi diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index 48c86d5b1ea..0aca8fb2163 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 = "ede86048a53464cdd3cef060e5e2e1603d91c2d2a0568417501af5ca9455f430" +const GenesisStateCommitmentHex = "49ad25e5716c2e302bc7ec833a19c6f6ad53823a97b4d80d1258445a150b6848" var GenesisStateCommitment flow.StateCommitment @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string { return GenesisStateCommitmentHex } if chainID == flow.Testnet { - return "ac9887d488bc90e33ce97478ca3f59c69795ed66061cf68d60d518be2fa0b658" + return "bc384ae96ee4105df764e810ac6e350b0edb12e346df2ec18939ebbb8cbbd9ce" } if chainID == flow.Sandboxnet { return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1" } - return "eb6a5749ba1a4bd9ef5b3aa804b123e0d257f68735867e661b1a1418b26f9229" + return "fc262bee4ca8a75e857dd17801b209828d5b0a306735f7219719cd64e6a39ba8" }