diff --git a/cmd/util/ledger/migrations/cadence.go b/cmd/util/ledger/migrations/cadence.go index 8b795b1082d..d78538b8d9a 100644 --- a/cmd/util/ledger/migrations/cadence.go +++ b/cmd/util/ledger/migrations/cadence.go @@ -41,7 +41,10 @@ func NewInterfaceTypeConversionRules(chainID flow.ChainID) StaticTypeMigrationRu } } -func NewCompositeTypeConversionRules(chainID flow.ChainID) StaticTypeMigrationRules { +func NewCompositeTypeConversionRules( + chainID flow.ChainID, + legacyTypeRequirements *LegacyTypeRequirements, +) StaticTypeMigrationRules { systemContracts := systemcontracts.SystemContractsForChain(chainID) oldFungibleTokenVaultCompositeType, newFungibleTokenVaultType := @@ -51,21 +54,42 @@ func NewCompositeTypeConversionRules(chainID flow.ChainID) StaticTypeMigrationRu oldNonFungibleTokenCollectionCompositeType, newNonFungibleTokenCollectionType := nonFungibleTokenCompositeToInterfaceRule(systemContracts, "Collection") - return StaticTypeMigrationRules{ + rules := StaticTypeMigrationRules{ oldFungibleTokenVaultCompositeType.ID(): newFungibleTokenVaultType, oldNonFungibleTokenNFTCompositeType.ID(): newNonFungibleTokenNFTType, oldNonFungibleTokenCollectionCompositeType.ID(): newNonFungibleTokenCollectionType, } + + for _, typeRequirement := range legacyTypeRequirements.typeRequirements { + oldType, newType := compositeToInterfaceRule( + typeRequirement.Address, + typeRequirement.ContractName, + typeRequirement.TypeName, + ) + + rules[oldType.ID()] = newType + } + + return rules } func NewCadence1InterfaceStaticTypeConverter(chainID flow.ChainID) statictypes.InterfaceTypeConverterFunc { - rules := NewInterfaceTypeConversionRules(chainID) - return NewStaticTypeMigration[*interpreter.InterfaceStaticType](rules) + return NewStaticTypeMigration[*interpreter.InterfaceStaticType]( + func() StaticTypeMigrationRules { + return NewInterfaceTypeConversionRules(chainID) + }, + ) } -func NewCadence1CompositeStaticTypeConverter(chainID flow.ChainID) statictypes.CompositeTypeConverterFunc { - rules := NewCompositeTypeConversionRules(chainID) - return NewStaticTypeMigration[*interpreter.CompositeStaticType](rules) +func NewCadence1CompositeStaticTypeConverter( + chainID flow.ChainID, + legacyTypeRequirements *LegacyTypeRequirements, +) statictypes.CompositeTypeConverterFunc { + return NewStaticTypeMigration[*interpreter.CompositeStaticType]( + func() StaticTypeMigrationRules { + return NewCompositeTypeConversionRules(chainID, legacyTypeRequirements) + }, + ) } func nonFungibleTokenCompositeToInterfaceRule( @@ -77,11 +101,26 @@ func nonFungibleTokenCompositeToInterfaceRule( ) { contract := systemContracts.NonFungibleToken - qualifiedIdentifier := fmt.Sprintf("%s.%s", contract.Name, identifier) + return compositeToInterfaceRule( + common.Address(contract.Address), + contract.Name, + identifier, + ) +} + +func compositeToInterfaceRule( + address common.Address, + contractName string, + typeName string, +) ( + *interpreter.CompositeStaticType, + *interpreter.IntersectionStaticType, +) { + qualifiedIdentifier := fmt.Sprintf("%s.%s", contractName, typeName) location := common.AddressLocation{ - Address: common.Address(contract.Address), - Name: contract.Name, + Address: address, + Name: contractName, } nftTypeID := location.TypeID(nil, qualifiedIdentifier) @@ -388,6 +427,7 @@ func NewCadence1ValueMigrations( log zerolog.Logger, rwf reporters.ReportWriterFactory, importantLocations map[common.AddressLocation]struct{}, + legacyTypeRequirements *LegacyTypeRequirements, opts Options, ) (migs []NamedMigration) { @@ -444,7 +484,7 @@ func NewCadence1ValueMigrations( rwf, errorMessageHandler, programs, - NewCadence1CompositeStaticTypeConverter(opts.ChainID), + NewCadence1CompositeStaticTypeConverter(opts.ChainID, legacyTypeRequirements), NewCadence1InterfaceStaticTypeConverter(opts.ChainID), storageDomainCapabilities, opts, @@ -535,10 +575,11 @@ const stagedContractUpdateMigrationName = "staged-contracts-update-migration" func NewCadence1ContractsMigrations( log zerolog.Logger, rwf reporters.ReportWriterFactory, + importantLocations map[common.AddressLocation]struct{}, + legacyTypeRequirements *LegacyTypeRequirements, opts Options, ) ( migs []NamedMigration, - importantLocations map[common.AddressLocation]struct{}, ) { stagedContractsMigrationOptions := StagedContractsMigrationOptions{ @@ -552,10 +593,10 @@ func NewCadence1ContractsMigrations( Burner: opts.BurnerContractChange, } - var systemContractsMigration *StagedContractsMigration - systemContractsMigration, importantLocations = NewSystemContractsMigration( + systemContractsMigration := NewSystemContractsMigration( log, rwf, + importantLocations, systemContractsMigrationOptions, ) @@ -564,6 +605,7 @@ func NewCadence1ContractsMigrations( "staged-contracts-migration", log, rwf, + legacyTypeRequirements, stagedContractsMigrationOptions, ).WithContractUpdateValidation(). WithStagedContractUpdates(opts.StagedContracts) @@ -621,7 +663,7 @@ func NewCadence1ContractsMigrations( }, ) - return migs, importantLocations + return migs } var testnetAccountsWithBrokenSlabReferences = func() map[common.Address]struct{} { @@ -748,9 +790,29 @@ func NewCadence1Migrations( } } - cadence1ContractsMigrations, importantLocations := NewCadence1ContractsMigrations( + importantLocations := make(map[common.AddressLocation]struct{}) + legacyTypeRequirements := &LegacyTypeRequirements{} + + cadenceTypeRequirementsExtractor := NewTypeRequirementsExtractingMigration( + log, + rwf, + importantLocations, + legacyTypeRequirements, + ) + + migs = append( + migs, + NamedMigration{ + Name: "extract-type-requirements", + Migrate: cadenceTypeRequirementsExtractor, + }, + ) + + cadence1ContractsMigrations := NewCadence1ContractsMigrations( log, rwf, + importantLocations, + legacyTypeRequirements, opts, ) @@ -765,6 +827,7 @@ func NewCadence1Migrations( log, rwf, importantLocations, + legacyTypeRequirements, opts, )..., ) diff --git a/cmd/util/ledger/migrations/cadence_values_migration_test.go b/cmd/util/ledger/migrations/cadence_values_migration_test.go index 449d6b4ad51..6cf03c38c51 100644 --- a/cmd/util/ledger/migrations/cadence_values_migration_test.go +++ b/cmd/util/ledger/migrations/cadence_values_migration_test.go @@ -2692,6 +2692,246 @@ func TestStorageCapConsInferredBorrowTypeEntry_MarshalJSON(t *testing.T) { ) } +func TestTypeRequirementRemovalMigration(t *testing.T) { + t.Parallel() + + rwf := &testReportWriterFactory{} + + logWriter := &writer{} + logger := zerolog.New(logWriter).Level(zerolog.ErrorLevel) + + const nWorker = 2 + + const chainID = flow.Testnet + + payloads, err := newBootstrapPayloads(chainID) + require.NoError(t, err) + + registersByAccount, err := registers.NewByAccountFromPayloads(payloads) + require.NoError(t, err) + + storedAddress := common.Address(chainID.Chain().ServiceAddress()) + tiblesAddress := mustHexToAddress("e93c412c964bdf40") + + // Store a contract in `addressC`. + + migrationRuntime, err := NewInterpreterMigrationRuntime( + registersByAccount, + chainID, + InterpreterMigrationRuntimeConfig{}, + ) + require.NoError(t, err) + + storage := migrationRuntime.Storage + storageDomain := common.PathDomainStorage.Identifier() + + storageMap := storage.GetStorageMap( + storedAddress, + storageDomain, + true, + ) + + contractName := "TiblesProducer" + + // Store a value with the actual `TiblesProducer.Minter` type + storageMap.WriteValue( + migrationRuntime.Interpreter, + interpreter.StringStorageMapKey("a"), + interpreter.NewTypeValue( + nil, + interpreter.NewCompositeStaticTypeComputeTypeID( + nil, + common.AddressLocation{ + Name: contractName, + Address: tiblesAddress, + }, + "TiblesProducer.Minter", + ), + ), + ) + + // Store a value with a random `TiblesProducer.Minter` type (different address) + storageMap.WriteValue( + migrationRuntime.Interpreter, + interpreter.StringStorageMapKey("b"), + interpreter.NewTypeValue( + nil, + interpreter.NewCompositeStaticTypeComputeTypeID( + nil, + common.AddressLocation{ + Name: contractName, + Address: storedAddress, + }, + "TiblesProducer.Minter", + ), + ), + ) + + // Commit + + err = storage.NondeterministicCommit(migrationRuntime.Interpreter, false) + require.NoError(t, err) + + // finalize the transaction + result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() + require.NoError(t, err) + + // Merge the changes into the registers + + expectedAddresses := map[flow.Address]struct{}{ + flow.Address(storedAddress): {}, + } + + err = registers.ApplyChanges( + registersByAccount, + result.WriteSet, + expectedAddresses, + logger, + ) + require.NoError(t, err) + + // Set contract code + + oldCode := ` + pub contract interface TiblesProducer { + + pub struct ContentLocation {} + pub struct interface IContentLocation {} + + pub resource interface IContent { + access(contract) let contentIdsToPaths: {String: TiblesProducer.ContentLocation} + pub fun getMetadata(contentId: String): {String: AnyStruct}? + } + + pub resource interface IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource Producer: IContent, IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource interface IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + + pub resource Minter: IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + } + ` + + err = registersByAccount.Set( + string(tiblesAddress[:]), + flow.ContractKey(contractName), + []byte(oldCode), + ) + require.NoError(t, err) + + encodedContractNames, err := environment.EncodeContractNames([]string{contractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(tiblesAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + + // Migrate + + // TODO: EVM contract is not deployed in snapshot yet, so can't update it + const evmContractChange = EVMContractChangeNone + + const burnerContractChange = BurnerContractChangeUpdate + + migrations := NewCadence1Migrations( + logger, + t.TempDir(), + rwf, + Options{ + NWorker: nWorker, + ChainID: chainID, + EVMContractChange: evmContractChange, + BurnerContractChange: burnerContractChange, + VerboseErrorOutput: true, + }, + ) + + for _, migration := range migrations { + err = migration.Migrate(registersByAccount) + require.NoError( + t, + err, + "migration `%s` failed, logs: %v", + migration.Name, + logWriter.logs, + ) + } + + // Check reporters + + reporter := rwf.reportWriters[typeRequirementExtractingReporterName] + require.NotNil(t, reporter) + require.Len(t, reporter.entries, 3) + + assert.Equal( + t, + []any{ + typeRequirementRemovalEntry{ + TypeRequirement{ + Address: tiblesAddress, + ContractName: contractName, + TypeName: "ContentLocation", + }, + }, + typeRequirementRemovalEntry{ + TypeRequirement{ + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Producer", + }, + }, + typeRequirementRemovalEntry{ + TypeRequirement{ + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Minter", + }, + }, + }, + reporter.entries, + ) + + // Check account + + _, err = runScript( + chainID, + registersByAccount, + fmt.Sprintf( + //language=Cadence + ` + access(all) + fun main() { + let storage = getAuthAccount(%s).storage + assert(storage.copy(from: /storage/a)!.identifier == "{A.%s.TiblesProducer.Minter}") + assert(storage.copy(from: /storage/b)!.identifier == "A.%s.TiblesProducer.Minter") + } + `, + storedAddress.HexWithPrefix(), + tiblesAddress.Hex(), + storedAddress.Hex(), + ), + ) + require.NoError(t, err) +} + func runScript(chainID flow.ChainID, registersByAccount *registers.ByAccount, script string) (cadence.Value, error) { options := computation.DefaultFVMOptions(chainID, false, false) options = append(options, diff --git a/cmd/util/ledger/migrations/change_contract_code_migration.go b/cmd/util/ledger/migrations/change_contract_code_migration.go index a809228f467..6deefd278fd 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration.go @@ -286,25 +286,24 @@ type SystemContractsMigrationOptions struct { func NewSystemContractsMigration( log zerolog.Logger, rwf reporters.ReportWriterFactory, + locations map[common.AddressLocation]struct{}, options SystemContractsMigrationOptions, ) ( migration *StagedContractsMigration, - locations map[common.AddressLocation]struct{}, ) { migration = NewStagedContractsMigration( "SystemContractsMigration", "system-contracts-migration", log, rwf, + &LegacyTypeRequirements{}, // This is empty for system contracts options.StagedContractsMigrationOptions, ) - locations = make(map[common.AddressLocation]struct{}) - for _, change := range SystemContractChanges(options.ChainID, options) { migration.registerContractChange(change) locations[change.AddressLocation()] = struct{}{} } - return migration, locations + return migration } diff --git a/cmd/util/ledger/migrations/change_contract_code_migration_test.go b/cmd/util/ledger/migrations/change_contract_code_migration_test.go index 0e89df23f0a..e9bf4c7d0bd 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration_test.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration_test.go @@ -81,7 +81,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options) + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ) registersByAccount := registers.NewByAccount() @@ -113,7 +120,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options) + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ) registersByAccount, err := registersForStagedContracts( StagedContract{ @@ -165,7 +179,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -226,7 +247,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -298,7 +326,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -377,7 +412,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -452,7 +494,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -514,7 +563,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address2), diff --git a/cmd/util/ledger/migrations/contract_checking_migration.go b/cmd/util/ledger/migrations/contract_checking_migration.go index 5f9b9fe524b..5140cc4a424 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration.go +++ b/cmd/util/ledger/migrations/contract_checking_migration.go @@ -51,70 +51,15 @@ func NewContractCheckingMigration( return fmt.Errorf("failed to create interpreter migration runtime: %w", err) } - // Gather all contracts - - log.Info().Msg("Gathering contracts ...") - - contractsForPrettyPrinting := make(map[common.Location][]byte, contractCountEstimate) - - contracts := make([]AddressContract, 0, contractCountEstimate) - - err = registersByAccount.ForEachAccount(func(accountRegisters *registers.AccountRegisters) error { - owner := accountRegisters.Owner() - - encodedContractNames, err := accountRegisters.Get(owner, flow.ContractNamesKey) - if err != nil { - return err - } - - contractNames, err := environment.DecodeContractNames(encodedContractNames) - if err != nil { - return err - } - - for _, contractName := range contractNames { - - contractKey := flow.ContractKey(contractName) - - code, err := accountRegisters.Get(owner, contractKey) - if err != nil { - return err - } - - if len(bytes.TrimSpace(code)) == 0 { - continue - } - - 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 - }) + contracts, err := gatherContractsFromRegisters(registersByAccount, log) if err != nil { - return fmt.Errorf("failed to get contracts of accounts: %w", err) + return err } - sort.Slice(contracts, func(i, j int) bool { - a := contracts[i] - b := contracts[j] - return a.location.ID() < b.location.ID() - }) - - log.Info().Msgf("Gathered all contracts (%d)", len(contracts)) + contractsForPrettyPrinting := make(map[common.Location][]byte, len(contracts)) + for _, contract := range contracts { + contractsForPrettyPrinting[contract.location] = contract.code + } // Check all contracts @@ -135,6 +80,68 @@ func NewContractCheckingMigration( } } +func gatherContractsFromRegisters(registersByAccount *registers.ByAccount, log zerolog.Logger) ([]AddressContract, error) { + log.Info().Msg("Gathering contracts ...") + + contracts := make([]AddressContract, 0, contractCountEstimate) + + err := registersByAccount.ForEachAccount(func(accountRegisters *registers.AccountRegisters) error { + owner := accountRegisters.Owner() + + encodedContractNames, err := accountRegisters.Get(owner, flow.ContractNamesKey) + if err != nil { + return err + } + + contractNames, err := environment.DecodeContractNames(encodedContractNames) + if err != nil { + return err + } + + for _, contractName := range contractNames { + + contractKey := flow.ContractKey(contractName) + + code, err := accountRegisters.Get(owner, contractKey) + if err != nil { + return err + } + + if len(bytes.TrimSpace(code)) == 0 { + continue + } + + address := common.Address([]byte(owner)) + location := common.AddressLocation{ + Address: address, + Name: contractName, + } + + contracts = append( + contracts, + AddressContract{ + location: location, + code: code, + }, + ) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to get contracts of accounts: %w", err) + } + + sort.Slice(contracts, func(i, j int) bool { + a := contracts[i] + b := contracts[j] + return a.location.ID() < b.location.ID() + }) + + log.Info().Msgf("Gathered all contracts (%d)", len(contracts)) + return contracts, nil +} + func checkContract( contract AddressContract, log zerolog.Logger, diff --git a/cmd/util/ledger/migrations/staged_contracts_migration.go b/cmd/util/ledger/migrations/staged_contracts_migration.go index 72987e7adf3..54725538726 100644 --- a/cmd/util/ledger/migrations/staged_contracts_migration.go +++ b/cmd/util/ledger/migrations/staged_contracts_migration.go @@ -42,6 +42,7 @@ type StagedContractsMigration struct { contractNamesProvider stdlib.AccountContractNamesProvider reporter reporters.ReportWriter verboseErrorOutput bool + legacyTypeRequirements *LegacyTypeRequirements } type StagedContract struct { @@ -73,22 +74,23 @@ func NewStagedContractsMigration( reporterName string, log zerolog.Logger, rwf reporters.ReportWriterFactory, + legacyTypeRequirements *LegacyTypeRequirements, options StagedContractsMigrationOptions, ) *StagedContractsMigration { return &StagedContractsMigration{ - name: name, - log: log, - chainID: options.ChainID, - stagedContracts: map[common.Address]map[string]Contract{}, - contractsByLocation: map[common.Location][]byte{}, - reporter: rwf.ReportWriter(reporterName), - verboseErrorOutput: options.VerboseErrorOutput, + name: name, + log: log, + chainID: options.ChainID, + legacyTypeRequirements: legacyTypeRequirements, + stagedContracts: map[common.Address]map[string]Contract{}, + contractsByLocation: map[common.Location][]byte{}, + reporter: rwf.ReportWriter(reporterName), + verboseErrorOutput: options.VerboseErrorOutput, } } func (m *StagedContractsMigration) WithContractUpdateValidation() *StagedContractsMigration { m.enableUpdateValidation = true - m.userDefinedTypeChangeCheckFunc = NewUserDefinedTypeChangeCheckerFunc(m.chainID) return m } @@ -179,6 +181,12 @@ func (m *StagedContractsMigration) InitMigration( m.contractAdditionHandler = mr.ContractAdditionHandler m.contractNamesProvider = mr.ContractNamesProvider + // `legacyTypeRequirements` are populated by a previous migration. + // So it should only be used once the previous migrations are complete. + if m.enableUpdateValidation { + m.userDefinedTypeChangeCheckFunc = NewUserDefinedTypeChangeCheckerFunc(m.chainID, m.legacyTypeRequirements) + } + return nil } @@ -646,11 +654,12 @@ func StagedContractsFromCSV(path string) ([]StagedContract, error) { func NewUserDefinedTypeChangeCheckerFunc( chainID flow.ChainID, + legacyTypeRequirements *LegacyTypeRequirements, ) func(oldTypeID common.TypeID, newTypeID common.TypeID) (checked, valid bool) { typeChangeRules := map[common.TypeID]common.TypeID{} - compositeTypeRules := NewCompositeTypeConversionRules(chainID) + compositeTypeRules := NewCompositeTypeConversionRules(chainID, legacyTypeRequirements) for typeID, newStaticType := range compositeTypeRules { typeChangeRules[typeID] = newStaticType.ID() } diff --git a/cmd/util/ledger/migrations/staged_contracts_migration_test.go b/cmd/util/ledger/migrations/staged_contracts_migration_test.go index bb253248893..972177e634f 100644 --- a/cmd/util/ledger/migrations/staged_contracts_migration_test.go +++ b/cmd/util/ledger/migrations/staged_contracts_migration_test.go @@ -96,7 +96,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -161,7 +168,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation() migration.WithStagedContractUpdates(stagedContracts) @@ -228,7 +242,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -309,7 +330,14 @@ func TestStagedContractsMigration(t *testing.T) { } const reporterName = "test" - migration := NewStagedContractsMigration("test", reporterName, log, rwf, options). + migration := NewStagedContractsMigration( + "test", + reporterName, + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -405,7 +433,14 @@ func TestStagedContractsMigration(t *testing.T) { } const reporterName = "test" - migration := NewStagedContractsMigration("test", reporterName, log, rwf, options). + migration := NewStagedContractsMigration( + "test", + reporterName, + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -514,7 +549,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -581,7 +623,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) // NOTE: no payloads @@ -736,7 +785,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options) + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ) accountOwner := string(accountAddress[:]) @@ -846,7 +902,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -945,7 +1008,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1060,7 +1130,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1180,7 +1257,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1431,7 +1515,14 @@ func TestStagedContractsWithUpdateValidator(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1548,7 +1639,14 @@ func TestStagedContractsWithUpdateValidator(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1654,7 +1752,14 @@ func TestStagedContractsWithUpdateValidator(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1786,7 +1891,14 @@ func TestStagedContractConformanceChanges(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1919,7 +2031,14 @@ func TestStagedContractConformanceChanges(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -2189,7 +2308,14 @@ func TestStagedContractsUpdateValidationErrors(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -2305,7 +2431,14 @@ func TestStagedContractsUpdateValidationErrors(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -2407,3 +2540,387 @@ func TestContractUpdateFailureEntry_MarshalJSON(t *testing.T) { string(actual), ) } + +func TestTypeRequirementRemoval(t *testing.T) { + t.Parallel() + + const chainID = flow.Testnet + + t.Run("TiblesProducer contract", func(t *testing.T) { + t.Parallel() + + tiblesAddress := mustHexToAddress("e93c412c964bdf40") + + oldCode := ` + pub contract interface TiblesProducer { + + pub struct ContentLocation {} + pub struct interface IContentLocation {} + + pub resource interface IContent { + access(contract) let contentIdsToPaths: {String: TiblesProducer.ContentLocation} + pub fun getMetadata(contentId: String): {String: AnyStruct}? + } + + pub resource interface IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource Producer: IContent, IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource interface IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + + pub resource Minter: IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + } + ` + + newCode := ` + access(all) contract interface TiblesProducer { + + access(all) struct interface ContentLocation {} + access(all) struct interface IContentLocation {} + + access(all) resource interface IContent { + access(contract) let contentIdsToPaths: {String: {TiblesProducer.ContentLocation}} + access(all) fun getMetadata(contentId: String): {String: AnyStruct}? + } + + access(all) resource interface IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface Producer: IContent, IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + + access(all) resource interface Minter: IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + } + ` + + contractName := "TiblesProducer" + + stagedContracts := []StagedContract{ + { + Address: tiblesAddress, + Contract: Contract{ + Name: contractName, + Code: []byte(newCode), + }, + }, + } + + logWriter := &logWriter{} + log := zerolog.New(logWriter) + + rwf := &testReportWriterFactory{} + + options := StagedContractsMigrationOptions{ + ChainID: chainID, + VerboseErrorOutput: true, + } + + registersByAccount, err := registersForStagedContracts( + StagedContract{ + Address: tiblesAddress, + Contract: Contract{ + Name: contractName, + Code: []byte(oldCode), + }, + }, + ) + require.NoError(t, err) + + encodedContractNames, err := environment.EncodeContractNames([]string{contractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(tiblesAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + + // Run type-requirement extractor + + legacyTypeRequirements := &LegacyTypeRequirements{} + + cadenceTypeRequirementsExtractor := NewTypeRequirementsExtractingMigration( + log, + rwf, + nil, + legacyTypeRequirements, + ) + err = cadenceTypeRequirementsExtractor(registersByAccount) + require.NoError(t, err) + + require.Equal( + t, + []TypeRequirement{ + { + Address: tiblesAddress, + ContractName: contractName, + TypeName: "ContentLocation", + }, + { + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Producer", + }, + { + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Minter", + }, + }, + legacyTypeRequirements.typeRequirements, + ) + + // Run staged contract migration + + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + legacyTypeRequirements, + options, + ). + WithStagedContractUpdates(stagedContracts). + WithContractUpdateValidation() + + err = migration.InitMigration(log, registersByAccount, 1) + require.NoError(t, err) + + owner := string(tiblesAddress[:]) + accountRegisters := registersByAccount.AccountRegisters(owner) + + err = migration.MigrateAccount( + context.Background(), + tiblesAddress, + accountRegisters, + ) + require.NoError(t, err) + + err = migration.Close() + require.NoError(t, err) + + require.Empty(t, logWriter.logs) + + require.Equal(t, 2, accountRegisters.Count()) + assert.Equal(t, newCode, contractCode(t, registersByAccount, owner, contractName)) + }) + + t.Run("random contract", func(t *testing.T) { + t.Parallel() + + addressGenerator := chainID.Chain().NewAddressGenerator() + randomAddress, err := addressGenerator.NextAddress() + require.NoError(t, err) + + oldCode := ` + pub contract interface Foo { + + pub struct ContentLocation {} + pub struct interface IContentLocation {} + + pub resource interface IContent { + access(contract) let contentIdsToPaths: {String: Foo.ContentLocation} + pub fun getMetadata(contentId: String): {String: AnyStruct}? + } + + pub resource interface IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource Producer: IContent, IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource interface IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + + pub resource Minter: IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + } + ` + + newCode := ` + access(all) contract interface Foo { + + access(all) struct interface ContentLocation {} + access(all) struct interface IContentLocation {} + + access(all) resource interface IContent { + access(contract) let contentIdsToPaths: {String: {Foo.ContentLocation}} + access(all) fun getMetadata(contentId: String): {String: AnyStruct}? + } + + access(all) resource interface IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface Producer: IContent, IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + + access(all) resource interface Minter: IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + } + ` + + fooContractName := "Foo" + + stagedContracts := []StagedContract{ + { + Address: common.Address(randomAddress), + Contract: Contract{ + Name: fooContractName, + Code: []byte(newCode), + }, + }, + } + + logWriter := &logWriter{} + log := zerolog.New(logWriter) + + rwf := &testReportWriterFactory{} + + options := StagedContractsMigrationOptions{ + ChainID: chainID, + VerboseErrorOutput: true, + } + + registersByAccount, err := registersForStagedContracts( + StagedContract{ + Address: common.Address(randomAddress), + Contract: Contract{ + Name: fooContractName, + Code: []byte(oldCode), + }, + }, + ) + require.NoError(t, err) + + encodedContractNames, err := environment.EncodeContractNames([]string{fooContractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(randomAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + + // Run type-requirement extractor + + legacyTypeRequirements := &LegacyTypeRequirements{} + + cadenceTypeRequirementsExtractor := NewTypeRequirementsExtractingMigration( + log, + rwf, + nil, + legacyTypeRequirements, + ) + err = cadenceTypeRequirementsExtractor(registersByAccount) + require.NoError(t, err) + + require.Equal( + t, + []TypeRequirement{ + { + Address: common.Address(randomAddress), + ContractName: fooContractName, + TypeName: "ContentLocation", + }, + { + Address: common.Address(randomAddress), + ContractName: fooContractName, + TypeName: "Producer", + }, + { + Address: common.Address(randomAddress), + ContractName: fooContractName, + TypeName: "Minter", + }, + }, + legacyTypeRequirements.typeRequirements, + ) + + // Run staged contract migration + + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + legacyTypeRequirements, + options, + ). + WithStagedContractUpdates(stagedContracts). + WithContractUpdateValidation() + + err = migration.InitMigration(log, registersByAccount, 1) + require.NoError(t, err) + + owner := string(randomAddress[:]) + accountRegisters := registersByAccount.AccountRegisters(owner) + + err = migration.MigrateAccount( + context.Background(), + common.Address(randomAddress), + accountRegisters, + ) + require.NoError(t, err) + + err = migration.Close() + require.NoError(t, err) + + require.Empty(t, logWriter.logs) + + require.Equal(t, 2, accountRegisters.Count()) + assert.Equal(t, newCode, contractCode(t, registersByAccount, owner, fooContractName)) + }) +} diff --git a/cmd/util/ledger/migrations/static_type_migration.go b/cmd/util/ledger/migrations/static_type_migration.go index 11c7e5c95a8..c34164cd755 100644 --- a/cmd/util/ledger/migrations/static_type_migration.go +++ b/cmd/util/ledger/migrations/static_type_migration.go @@ -7,19 +7,29 @@ import ( type StaticTypeMigrationRules map[common.TypeID]interpreter.StaticType +// NewStaticTypeMigration returns a type converter function. +// Accepts a `rulesGetter` which return +// This is because this constructor is called at the time of constructing the migrations (e.g: Cadence value migration), +// but the rules can only be finalized after running previous TypeRequirementsExtractingMigration migration. +// i.e: the LegacyTypeRequirements list used by NewCompositeTypeConversionRules is lazily populated. +// So we need to delay the construction of the rules, until after the execution of previous migration. func NewStaticTypeMigration[T interpreter.StaticType]( - rules StaticTypeMigrationRules, + rulesGetter func() StaticTypeMigrationRules, ) func(staticType T) interpreter.StaticType { - // Returning `nil` form the callback indicates the type wasn't converted. + var rules StaticTypeMigrationRules - if rules == nil { - return func(original T) interpreter.StaticType { + return func(original T) interpreter.StaticType { + // Initialize only once + if rules == nil { + rules = rulesGetter() + } + + // Returning `nil` form the callback indicates the type wasn't converted. + if rules == nil { return nil } - } - return func(original T) interpreter.StaticType { if replacement, ok := rules[original.ID()]; ok { return replacement } diff --git a/cmd/util/ledger/migrations/type_requirements_extractor.go b/cmd/util/ledger/migrations/type_requirements_extractor.go new file mode 100644 index 00000000000..d4965e8eb87 --- /dev/null +++ b/cmd/util/ledger/migrations/type_requirements_extractor.go @@ -0,0 +1,135 @@ +package migrations + +import ( + "encoding/json" + + "github.com/rs/zerolog" + + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/old_parser" + + "github.com/onflow/flow-go/cmd/util/ledger/reporters" + "github.com/onflow/flow-go/cmd/util/ledger/util/registers" +) + +const typeRequirementExtractingReporterName = "type-requirements-extracting" + +type LegacyTypeRequirements struct { + typeRequirements []TypeRequirement +} + +type TypeRequirement struct { + Address common.Address + ContractName string + TypeName string +} + +func NewTypeRequirementsExtractingMigration( + log zerolog.Logger, + rwf reporters.ReportWriterFactory, + importantLocations map[common.AddressLocation]struct{}, + legacyTypeRequirements *LegacyTypeRequirements, +) RegistersMigration { + return func(registersByAccount *registers.ByAccount) error { + + reporter := rwf.ReportWriter(typeRequirementExtractingReporterName) + defer reporter.Close() + + // Gather all contracts + + contracts, err := gatherContractsFromRegisters(registersByAccount, log) + if err != nil { + return err + } + + // Extract type requirements from all contracts + + for _, contract := range contracts { + if _, isSystemContract := importantLocations[contract.location]; isSystemContract { + // System contracts have their own type-changing rules. + // So do not add them here. + continue + } + + extractTypeRequirements( + contract, + log, + reporter, + legacyTypeRequirements, + ) + } + + return nil + } +} + +func extractTypeRequirements( + contract AddressContract, + log zerolog.Logger, + reporter reporters.ReportWriter, + legacyTypeRequirements *LegacyTypeRequirements, +) { + + // must be parsed with the old parser. + program, err := old_parser.ParseProgram( + nil, + contract.code, + old_parser.Config{}, + ) + + if err != nil { + // If the old contract cannot be parsed, then ignore + return + } + + contractInterface := program.SoleContractInterfaceDeclaration() + if contractInterface == nil { + // Type requirements can only reside inside contract interfaces. + // Ignore all other programs. + return + } + + for _, composites := range contractInterface.DeclarationMembers().Composites() { + typeRequirement := TypeRequirement{ + Address: contract.location.Address, + ContractName: contractInterface.Identifier.Identifier, + TypeName: composites.Identifier.Identifier, + } + + legacyTypeRequirements.typeRequirements = append(legacyTypeRequirements.typeRequirements, typeRequirement) + + reporter.Write(typeRequirementRemovalEntry{ + TypeRequirement: typeRequirement, + }) + } + + log.Info().Msgf("Collected %d type-requirements", len(legacyTypeRequirements.typeRequirements)) +} + +// cadenceValueMigrationFailureEntry + +type typeRequirementRemovalEntry struct { + TypeRequirement +} + +var _ valueMigrationReportEntry = typeRequirementRemovalEntry{} + +func (e typeRequirementRemovalEntry) accountAddress() common.Address { + return e.Address +} + +var _ json.Marshaler = typeRequirementRemovalEntry{} + +func (e typeRequirementRemovalEntry) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + AccountAddress string `json:"account_address"` + ContractName string `json:"contract_name"` + TypeName string `json:"type_name"` + }{ + Kind: "cadence-type-requirement-remove", + AccountAddress: e.Address.HexWithPrefix(), + ContractName: e.ContractName, + TypeName: e.TypeName, + }) +} diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index f2ec6d76b67..6fa5ef66470 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("1383e01cdba9bb1df08d413f89c3a252f1081f2ebb0c58850676194a01cfc4c4") + expectedStateCommitmentBytes, _ := hex.DecodeString("e8b4b48a5c4eb510e28ecc9623271d53ef9915c98d333939b448516fa25e5a8f") expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes) require.NoError(t, err) diff --git a/fvm/evm/events/events.go b/fvm/evm/events/events.go index b59f34bd41c..edda7c78253 100644 --- a/fvm/evm/events/events.go +++ b/fvm/evm/events/events.go @@ -127,6 +127,7 @@ func (p *blockEvent) ToCadence(chainID flow.ChainID) (cadence.Event, error) { hashToCadenceArrayValue(p.ParentBlockHash), hashToCadenceArrayValue(p.ReceiptRoot), hashToCadenceArrayValue(p.TransactionHashRoot), + hashToCadenceArrayValue(p.PrevRandao), }).WithType(eventType), nil } @@ -139,6 +140,7 @@ type BlockEventPayload struct { ParentBlockHash gethCommon.Hash `cadence:"parentHash"` ReceiptRoot gethCommon.Hash `cadence:"receiptRoot"` TransactionHashRoot gethCommon.Hash `cadence:"transactionHashRoot"` + PrevRandao gethCommon.Hash `cadence:"prevrandao"` } // DecodeBlockEventPayload decodes Cadence event into block event payload. diff --git a/fvm/evm/events/events_test.go b/fvm/evm/events/events_test.go index e30819ca31e..4b5d3420f6b 100644 --- a/fvm/evm/events/events_test.go +++ b/fvm/evm/events/events_test.go @@ -34,6 +34,7 @@ func TestEVMBlockExecutedEventCCFEncodingDecoding(t *testing.T) { ReceiptRoot: gethCommon.Hash{}, TotalGasUsed: 15, TransactionHashRoot: gethCommon.HexToHash("0x70b67ce6710355acf8d69b2ea013d34e212bc4824926c5d26f189c1ca9667246"), + PrevRandao: testutils.RandomCommonHash(t), } event := events.NewBlockEvent(block) @@ -55,6 +56,7 @@ func TestEVMBlockExecutedEventCCFEncodingDecoding(t *testing.T) { assert.Equal(t, bep.ParentBlockHash, block.ParentBlockHash) assert.Equal(t, bep.ReceiptRoot, block.ReceiptRoot) assert.Equal(t, bep.TransactionHashRoot, block.TransactionHashRoot) + assert.Equal(t, bep.PrevRandao, block.PrevRandao) v, err := ccf.Encode(ev) require.NoError(t, err) diff --git a/fvm/evm/handler/blockstore.go b/fvm/evm/handler/blockstore.go index c1dcf77cb49..a8c5975f193 100644 --- a/fvm/evm/handler/blockstore.go +++ b/fvm/evm/handler/blockstore.go @@ -47,7 +47,19 @@ func (bs *BlockStore) BlockProposal() (*types.BlockProposal, error) { if len(data) != 0 { return types.NewBlockProposalFromBytes(data) } + bp, err := bs.constructBlockProposal() + if err != nil { + return nil, err + } + // store block proposal + err = bs.UpdateBlockProposal(bp) + if err != nil { + return nil, err + } + return bp, nil +} +func (bs *BlockStore) constructBlockProposal() (*types.BlockProposal, error) { // if available construct a new one cadenceHeight, err := bs.backend.GetCurrentBlockHeight() if err != nil { @@ -76,12 +88,21 @@ func (bs *BlockStore) BlockProposal() (*types.BlockProposal, error) { // expect timestamps in unix seconds so we convert here timestamp := uint64(cadenceBlock.Timestamp / int64(time.Second)) + // read a random value for block proposal + prevrandao := gethCommon.Hash{} + err = bs.backend.ReadRandom(prevrandao[:]) + if err != nil { + return nil, err + } + blockProposal := types.NewBlockProposal( parentHash, lastExecutedBlock.Height+1, timestamp, lastExecutedBlock.TotalSupply, + prevrandao, ) + return blockProposal, nil } @@ -99,14 +120,6 @@ func (bs *BlockStore) UpdateBlockProposal(bp *types.BlockProposal) error { ) } -func (bs *BlockStore) ResetBlockProposal() error { - return bs.backend.SetValue( - bs.rootAddress[:], - []byte(BlockStoreLatestBlockProposalKey), - nil, - ) -} - // CommitBlockProposal commits the block proposal to the chain func (bs *BlockStore) CommitBlockProposal(bp *types.BlockProposal) error { bp.PopulateRoots() @@ -135,7 +148,12 @@ func (bs *BlockStore) CommitBlockProposal(bp *types.BlockProposal) error { return err } - err = bs.ResetBlockProposal() + // construct a new block proposal and store + newBP, err := bs.constructBlockProposal() + if err != nil { + return err + } + err = bs.UpdateBlockProposal(newBP) if err != nil { return err } diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index dcc86ea56b7..e6c86e16a2e 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -404,7 +404,10 @@ func (h *ContractHandler) run( } // step 9 - emit transaction event - err = h.emitEvent(events.NewTransactionEvent(res, rlpEncodedTx, bp.Height)) + err = h.emitEvent( + events.NewTransactionEvent(res, rlpEncodedTx, bp.Height), + ) + if err != nil { return nil, err } @@ -516,11 +519,6 @@ func (h *ContractHandler) getBlockContext() (types.BlockContext, error) { if err != nil { return types.BlockContext{}, err } - rand := gethCommon.Hash{} - err = h.backend.ReadRandom(rand[:]) - if err != nil { - return types.BlockContext{}, err - } return types.BlockContext{ ChainID: types.EVMChainIDFromFlowChainID(h.flowChainID), @@ -533,7 +531,7 @@ func (h *ContractHandler) getBlockContext() (types.BlockContext, error) { return hash }, ExtraPrecompiledContracts: h.precompiledContracts, - Random: rand, + Random: bp.PrevRandao, Tracer: h.tracer.TxTracer(), TxCountSoFar: uint(len(bp.TxHashes)), TotalGasUsedSoFar: bp.TotalGasUsed, diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index e5a075fcc13..21ed5a0af78 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -34,6 +34,8 @@ contract EVM { receiptRoot: [UInt8; 32], // root hash of all the transaction hashes transactionHashRoot: [UInt8; 32], + /// value returned for PREVRANDAO opcode + prevrandao: [UInt8; 32], ) /// Transaction executed event is emitted every time a transaction diff --git a/fvm/evm/types/block.go b/fvm/evm/types/block.go index bd2ab4e8408..458e2961bc3 100644 --- a/fvm/evm/types/block.go +++ b/fvm/evm/types/block.go @@ -44,8 +44,11 @@ type Block struct { // values as node values. Proofs are still compatible but might require an extra hashing step. TransactionHashRoot gethCommon.Hash - // stores gas used by all transactions included in the block. + // TotalGasUsed stores gas used by all transactions included in the block. TotalGasUsed uint64 + + // PrevRandao is the value returned for block.prevrandao opcode + PrevRandao gethCommon.Hash } // ToBytes encodes the block into bytes @@ -65,6 +68,7 @@ func NewBlock( height uint64, timestamp uint64, totalSupply *big.Int, + prevRandao gethCommon.Hash, ) *Block { return &Block{ ParentBlockHash: parentBlockHash, @@ -73,13 +77,21 @@ func NewBlock( TotalSupply: totalSupply, ReceiptRoot: gethTypes.EmptyReceiptsHash, TransactionHashRoot: gethTypes.EmptyRootHash, + PrevRandao: prevRandao, } } // NewBlockFromBytes constructs a new block from encoded data func NewBlockFromBytes(encoded []byte) (*Block, error) { res := &Block{} - return res, gethRLP.DecodeBytes(encoded, res) + err := gethRLP.DecodeBytes(encoded, res) + if err != nil { + res = decodeBlockBreakingChanges(encoded) + if res == nil { + return nil, err + } + } + return res, nil } // GenesisTimestamp returns the block time stamp for EVM genesis block @@ -104,6 +116,7 @@ func GenesisBlock(chainID flow.ChainID) *Block { ReceiptRoot: gethTypes.EmptyRootHash, TransactionHashRoot: gethTypes.EmptyRootHash, TotalGasUsed: 0, + PrevRandao: gethCommon.Hash{}, } } @@ -182,7 +195,14 @@ func (b *BlockProposal) ToBytes() ([]byte, error) { // NewBlockProposalFromBytes constructs a new block proposal from encoded data func NewBlockProposalFromBytes(encoded []byte) (*BlockProposal, error) { res := &BlockProposal{} - return res, gethRLP.DecodeBytes(encoded, res) + err := gethRLP.DecodeBytes(encoded, res) + if err != nil { + res = decodeBlockProposalBreakingChanges(encoded) + if res == nil { + return nil, err + } + } + return res, nil } func NewBlockProposal( @@ -190,6 +210,7 @@ func NewBlockProposal( height uint64, timestamp uint64, totalSupply *big.Int, + prevRandao gethCommon.Hash, ) *BlockProposal { return &BlockProposal{ Block: Block{ @@ -198,6 +219,7 @@ func NewBlockProposal( Timestamp: timestamp, TotalSupply: totalSupply, ReceiptRoot: gethTypes.EmptyRootHash, + PrevRandao: prevRandao, }, Receipts: make([]LightReceipt, 0), TxHashes: make([]gethCommon.Hash, 0), @@ -217,3 +239,70 @@ func (t TransactionHashes) EncodeIndex(index int, buffer *bytes.Buffer) { func (t TransactionHashes) RootHash() gethCommon.Hash { return gethTypes.DeriveSha(t, gethTrie.NewStackTrie(nil)) } + +// Below block type section, defines earlier block types, +// this is being used to decode blocks that were stored +// before block type changes. It allows us to still decode +// a block that would otherwise be invalid if decoded into +// latest version of the above Block type. + +// before adding PrevRandao to the block +type BlockV0 struct { + ParentBlockHash gethCommon.Hash + Height uint64 + Timestamp uint64 + TotalSupply *big.Int + ReceiptRoot gethCommon.Hash + TransactionHashRoot gethCommon.Hash + TotalGasUsed uint64 +} + +type BlockProposalV0 struct { + BlockV0 + Receipts []LightReceipt + TxHashes TransactionHashes +} + +// decodeBlockBreakingChanges will try to decode the bytes into all +// previous versions of block type, if it succeeds it will return the +// migrated block, otherwise it will return nil. +func decodeBlockBreakingChanges(encoded []byte) *Block { + b0 := &BlockV0{} + if err := gethRLP.DecodeBytes(encoded, b0); err == nil { + return &Block{ + ParentBlockHash: b0.ParentBlockHash, + Height: b0.Height, + Timestamp: b0.Timestamp, + TotalSupply: b0.TotalSupply, + ReceiptRoot: b0.ReceiptRoot, + TransactionHashRoot: b0.TransactionHashRoot, + TotalGasUsed: b0.TotalGasUsed, + PrevRandao: gethCommon.Hash{}, + } + } + return nil +} + +// decodeBlockProposalBreakingChanges will try to decode the bytes into all +// previous versions of block proposal type, if it succeeds it will return the +// migrated block, otherwise it will return nil. +func decodeBlockProposalBreakingChanges(encoded []byte) *BlockProposal { + bp0 := &BlockProposalV0{} + if err := gethRLP.DecodeBytes(encoded, bp0); err == nil { + return &BlockProposal{ + Block: Block{ + ParentBlockHash: bp0.ParentBlockHash, + Height: bp0.Height, + Timestamp: bp0.Timestamp, + TotalSupply: bp0.TotalSupply, + ReceiptRoot: bp0.ReceiptRoot, + TransactionHashRoot: bp0.TransactionHashRoot, + TotalGasUsed: bp0.TotalGasUsed, + PrevRandao: gethCommon.Hash{}, + }, + Receipts: bp0.Receipts, + TxHashes: bp0.TxHashes, + } + } + return nil +} diff --git a/fvm/evm/types/block_test.go b/fvm/evm/types/block_test.go index aa9ee1d260c..2ccb0ddcd65 100644 --- a/fvm/evm/types/block_test.go +++ b/fvm/evm/types/block_test.go @@ -6,6 +6,7 @@ import ( gethCommon "github.com/onflow/go-ethereum/common" gethTypes "github.com/onflow/go-ethereum/core/types" + gethRLP "github.com/onflow/go-ethereum/rlp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -53,7 +54,7 @@ func Test_BlockHash(t *testing.T) { } func Test_BlockProposal(t *testing.T) { - bp := NewBlockProposal(gethCommon.Hash{1}, 1, 0, nil) + bp := NewBlockProposal(gethCommon.Hash{1}, 1, 0, nil, gethCommon.Hash{1, 2, 3}) bp.AppendTransaction(nil) require.Empty(t, bp.TxHashes) @@ -76,3 +77,52 @@ func Test_BlockProposal(t *testing.T) { bp.PopulateRoots() require.NotEqual(t, gethTypes.EmptyReceiptsHash, bp.ReceiptRoot) } + +func Test_DecodeHistoricBlocks(t *testing.T) { + bv0 := BlockV0{ + ParentBlockHash: gethCommon.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + Height: 1, + Timestamp: 2, + TotalSupply: big.NewInt(3), + ReceiptRoot: gethCommon.Hash{0x04}, + TransactionHashRoot: gethCommon.Hash{0x05}, + TotalGasUsed: 0, + } + b0, err := gethRLP.EncodeToBytes(bv0) + require.NoError(t, err) + + b := decodeBlockBreakingChanges(b0) + require.Equal(t, b.ParentBlockHash, bv0.ParentBlockHash) + require.Equal(t, b.Height, bv0.Height) + require.Equal(t, b.Timestamp, bv0.Timestamp) + require.Equal(t, b.TotalSupply.Uint64(), bv0.TotalSupply.Uint64()) + require.Equal(t, b.ReceiptRoot, bv0.ReceiptRoot) + require.Equal(t, b.TransactionHashRoot, bv0.TransactionHashRoot) + require.Equal(t, b.TotalGasUsed, bv0.TotalGasUsed) + require.Empty(t, b.PrevRandao) + + bpv0 := BlockProposalV0{ + BlockV0: bv0, + Receipts: []LightReceipt{ + {CumulativeGasUsed: 10}, + {CumulativeGasUsed: 2}, + }, + TxHashes: []gethCommon.Hash{{1, 2}, {3, 4}, {5, 6}}, + } + + bp0, err := gethRLP.EncodeToBytes(bpv0) + require.NoError(t, err) + + bp, err := NewBlockProposalFromBytes(bp0) + require.NoError(t, err) + require.Equal(t, bp.ParentBlockHash, bpv0.ParentBlockHash) + require.Equal(t, bp.Height, bpv0.Height) + require.Equal(t, bp.Timestamp, bpv0.Timestamp) + require.Equal(t, bp.TotalSupply.Uint64(), bpv0.TotalSupply.Uint64()) + require.Equal(t, bp.ReceiptRoot, bpv0.ReceiptRoot) + require.Equal(t, bp.TransactionHashRoot, bpv0.TransactionHashRoot) + require.Equal(t, bp.TotalGasUsed, bpv0.TotalGasUsed) + require.Empty(t, bp.PrevRandao) + require.Len(t, bp.Receipts, 2) + require.Len(t, bp.TxHashes, 3) +} diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index ea7278b1e6e..ee6da7b082d 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 = "f0eeeadd231fcc1668d4bcc8df8488a35e126600e3c4b88b4120b35e4ea9ee8c" +const GenesisStateCommitmentHex = "ba479ddabd34159a9d6326ea78c659aa6dd71db63791714bdccdbea859ba1b8e" var GenesisStateCommitment flow.StateCommitment @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string { return GenesisStateCommitmentHex } if chainID == flow.Testnet { - return "b41ab049fc2f2f5f419357f2e4dc9d2181a18f419e3b2e96d7f908511d5e0aa1" + return "9485c620254319da8ea93978909d7ed8327e9dd1f4cb9ec74c816919166b5a2c" } if chainID == flow.Sandboxnet { return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1" } - return "922fca1ecf717e724328d24a9e3cb147426973405fe0d7ba87f67e0f5347887b" + return "66df5ceb8ca13532a5004bb6014677b0ff72199a5ea42388d5e0947368739c94" }