diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index e9d45a347b..47a2a74336 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -288,7 +288,7 @@ func runNativeTestCases(t *testing.T, ctr interop.ContractMD, name string, nativ }) } -func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []string) interop.MethodAndPrice { +func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []string) interop.HFSpecificMethodAndPrice { paramLen := len(params) switch { @@ -308,8 +308,10 @@ func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []strin name = strings.TrimSuffix(name, "WithData") } - md, ok := ctr.GetMethod(name, paramLen) - require.True(t, ok, ctr.Manifest.Name, name, paramLen) + latestHF := config.LatestHardfork() + cMD := ctr.HFSpecificContractMD(&latestHF) + md, ok := cMD.GetMethod(name, paramLen) + require.True(t, ok, cMD.Manifest.Name, name, paramLen) return md } diff --git a/pkg/config/hardfork.go b/pkg/config/hardfork.go index dd4904f9e5..ab9dbadac3 100644 --- a/pkg/config/hardfork.go +++ b/pkg/config/hardfork.go @@ -36,9 +36,30 @@ func init() { } } +// Cmp returns the result of hardforks comparison. It returns: +// +// -1 if hf < other +// 0 if hf == other +// +1 if hf > other +func (hf Hardfork) Cmp(other Hardfork) int { + switch { + case hf == other: + return 0 + case hf < other: + return -1 + default: + return 1 + } +} + // IsHardforkValid denotes whether the provided string represents a valid // Hardfork name. func IsHardforkValid(s string) bool { _, ok := hardforks[s] return ok } + +// LatestHardfork returns latest known hardfork. +func LatestHardfork() Hardfork { + return hfLast >> 1 +} diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 05a18a88a3..76715bfeae 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -341,16 +341,37 @@ func (bc *Blockchain) GetDesignatedByRole(r noderoles.Role) (keys.PublicKeys, ui return res, h, err } +// getCurrentHF returns latest currently enabled hardfork. In case if no hardforks is enabled, the +// default config.Hardfork(0) value is returned. +func (bc *Blockchain) getCurrentHF() config.Hardfork { + var ( + height = bc.BlockHeight() + current config.Hardfork + ) + // Rely on the fact that hardforks list is continuous. + for _, hf := range config.Hardforks { + enableHeight, ok := bc.config.Hardforks[hf.String()] + if !ok || height < enableHeight { + break + } + current = hf + } + return current +} + // SetOracle sets oracle module. It can safely be called on the running blockchain. // To unregister Oracle service use SetOracle(nil). func (bc *Blockchain) SetOracle(mod native.OracleService) { + // TODO: ensure Oracle script is updated when needed. orc := bc.contracts.Oracle + currentHF := bc.getCurrentHF() if mod != nil { - md, ok := orc.GetMethod(manifest.MethodVerify, -1) + orcMd := orc.HFSpecificContractMD(¤tHF) + md, ok := orcMd.GetMethod(manifest.MethodVerify, -1) if !ok { panic(fmt.Errorf("%s method not found", manifest.MethodVerify)) } - mod.UpdateNativeContract(orc.NEF.Script, orc.GetOracleResponseScript(), + mod.UpdateNativeContract(orcMd.NEF.Script, orc.GetOracleResponseScript(), orc.Hash, md.MD.Offset) keys, _, err := bc.GetDesignatedByRole(noderoles.Oracle) if err != nil { @@ -487,8 +508,10 @@ func (bc *Blockchain) init() error { // Check autogenerated native contracts' manifests and NEFs against the stored ones. // Need to be done after native Management cache initialization to be able to get // contract state from DAO via high-level bc API. + var current = bc.getCurrentHF() for _, c := range bc.contracts.Contracts { md := c.Metadata() + hfMD := md.HFSpecificContractMD(¤t) storedCS := bc.GetContractState(md.Hash) // Check that contract was deployed. if !bc.isHardforkEnabled(c.ActiveIn(), bHeight) { @@ -505,7 +528,7 @@ func (bc *Blockchain) init() error { return fmt.Errorf("failed to check native %s state against autogenerated one: %w", md.Name, err) } autogenCS := &state.Contract{ - ContractBase: md.ContractBase, + ContractBase: hfMD.ContractBase, UpdateCounter: storedCS.UpdateCounter, // it can be restored only from the DB, so use the stored value. } autogenCSBytes, err := stackitem.SerializeConvertible(autogenCS) @@ -2269,8 +2292,12 @@ func (bc *Blockchain) GetNativeContractScriptHash(name string) (util.Uint160, er // GetNatives returns list of native contracts. func (bc *Blockchain) GetNatives() []state.NativeContract { res := make([]state.NativeContract, 0, len(bc.contracts.Contracts)) + current := bc.getCurrentHF() for _, c := range bc.contracts.Contracts { - res = append(res, c.Metadata().NativeContract) + md := c.Metadata().HFSpecificContractMD(¤t) + res = append(res, state.NativeContract{ + ContractBase: md.ContractBase, + }) } return res } diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index cb3d94fc3b..47ce9c02c5 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -7,6 +7,7 @@ import ( "fmt" "sort" "strings" + "sync" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" @@ -140,8 +141,14 @@ type Function struct { // Method is a signature for a native method. type Method = func(ic *Context, args []stackitem.Item) stackitem.Item -// MethodAndPrice is a native-contract method descriptor. +// MethodAndPrice is a generic hardfork-independent native contract method descriptor. type MethodAndPrice struct { + HFSpecificMethodAndPrice + ActiveFrom *config.Hardfork +} + +// HFSpecificMethodAndPrice is a hardfork-specific native contract method descriptor. +type HFSpecificMethodAndPrice struct { Func Method MD *manifest.Method CPUFee int64 @@ -150,10 +157,23 @@ type MethodAndPrice struct { RequiredFlags callflag.CallFlag } +// Event is a generic hardfork-independent native contract event descriptor. +type Event struct { + HFSpecificEvent + ActiveFrom *config.Hardfork +} + +// HFSpecificEvent is a hardfork-specific native contract event descriptor. +type HFSpecificEvent struct { + MD *manifest.Event +} + // Contract is an interface for all native contracts. type Contract interface { - Initialize(*Context) error - // ActiveIn returns the hardfork native contract is active from or nil in case + // Initialize performs native contract initialization on contract deploy or update. + // Active hardfork is passed as the second argument. + Initialize(*Context, *config.Hardfork) error + // ActiveIn returns the hardfork native contract is active starting from or nil in case // it's always active. ActiveIn() *config.Hardfork // InitializeCache aimed to initialize contract's cache when the contract has @@ -161,53 +181,163 @@ type Contract interface { // It should be called each time after node restart iff the contract was // deployed and no Initialize method was called. InitializeCache(blockHeight uint32, d *dao.Simple) error + // Metadata returns generic native contract metadata. Metadata() *ContractMD OnPersist(*Context) error PostPersist(*Context) error } -// ContractMD represents a native contract instance. +// ContractMD represents a generic hardfork-independent native contract instance. type ContractMD struct { - state.NativeContract - Name string + ID int32 + Hash util.Uint160 + Name string + // TODO: remove Methods and Events at all, build HF-specific MD in-place for every hardfork and cache them. + // Methods is a generic set of contract methods with activation hardforks. Any HF-dependent part of included methods + // (offsets, in particular) must not be used, there's a mdCache field for that. Methods []MethodAndPrice -} - -// NewContractMD returns Contract with the specified list of methods. -func NewContractMD(name string, id int32) *ContractMD { + // Events is a generic set of contract events with activation hardforks. Any HF-dependent part of events must not be + // used, there's a mdCache field for that. + Events []Event + // ActiveHFs is a map of hardforks that contract should react to. Contract update or deploy should be called for + // active hardforks. This map is being initialized on contract creation and used as a read-only, hens, not protected + // by mutex. + ActiveHFs map[config.Hardfork]struct{} + + // mdCache contains hardfork-specific ready-to-use contract descriptors. This cache is lazy and thus, protected by + // mdCacheLock. + // TODO: @roman-khimov, we may replace lazy mdCache by instant mdCache initialization performed once during contract's + // creation or initialization. Both ways are OK to me, which one would we prefer? + mdCacheLock sync.RWMutex + mdCache map[config.Hardfork]*HFSpecificContractMD + + // onManifestConstruction is a callback for manifest finalization. + onManifestConstruction func(*manifest.Manifest) +} + +// HFSpecificContractMD is a hardfork-specific native contract descriptor. +type HFSpecificContractMD struct { + state.ContractBase + Methods []HFSpecificMethodAndPrice + Events []HFSpecificEvent +} + +// NewContractMD returns Contract with the specified fields set. onManifestConstruction callback every time +// after hardfork-specific manifest creation and aimed to finalize the manifest. +func NewContractMD(name string, id int32, onManifestConstruction ...func(*manifest.Manifest)) *ContractMD { c := &ContractMD{Name: name} + if len(onManifestConstruction) != 0 { + c.onManifestConstruction = onManifestConstruction[0] + } c.ID = id - - // NEF is now stored in the contract state and affects state dump. - // Therefore, values are taken from C# node. - c.NEF.Header.Compiler = "neo-core-v3.0" - c.NEF.Header.Magic = nef.Magic - c.NEF.Tokens = []nef.MethodToken{} // avoid `nil` result during JSON marshalling c.Hash = state.CreateNativeContractHash(c.Name) - c.Manifest = *manifest.DefaultManifest(name) + c.ActiveHFs = make(map[config.Hardfork]struct{}) + c.mdCache = make(map[config.Hardfork]*HFSpecificContractMD) return c } -// UpdateHash creates a native contract script and updates hash. -func (c *ContractMD) UpdateHash() { +// HFSpecificContractMD returns hardfork-specific native contract metadata, i.e. with methods, events and script +// corresponding to the specified hardfork. If hardfork is not specified, then default metadata will be returned +// (methods, events and script that are always active). +func (c *ContractMD) HFSpecificContractMD(hf *config.Hardfork) *HFSpecificContractMD { + var key config.Hardfork + if hf != nil { + key = *hf + } + c.mdCacheLock.RLock() + if md, ok := c.mdCache[key]; ok { + c.mdCacheLock.RUnlock() + return md + } + c.mdCacheLock.RUnlock() + + md := c.buildHFSpecificMD(hf) + return md +} + +// buildHFSpecificMD builds hardfork-specific contract descriptor that includes methods and events active starting from +// the specified hardfork or older. +func (c *ContractMD) buildHFSpecificMD(hf *config.Hardfork) *HFSpecificContractMD { + // TODO: firstly check if the specified hardfork is included into the list of active hardforks for this particular contract. + // If not, then value from the previous hardfork may safely be reused. + + var ( + abiMethods = make([]manifest.Method, 0, len(c.Methods)) + methods = make([]HFSpecificMethodAndPrice, 0, len(c.Methods)) + abiEvents = make([]manifest.Event, 0, len(c.Events)) + events = make([]HFSpecificEvent, 0, len(c.Events)) + ) w := io.NewBufBinWriter() for i := range c.Methods { - offset := w.Len() - c.Methods[i].MD.Offset = offset - c.Manifest.ABI.Methods[i].Offset = offset + m := c.Methods[i] + if !(m.ActiveFrom == nil || (hf != nil && (*m.ActiveFrom).Cmp(*hf) <= 0)) { + continue + } + + // Perform method descriptor copy to support independent HF-based offset update. + md := *m.MD + m.MD = &md + m.MD.Offset = w.Len() + emit.Int(w.BinWriter, 0) - c.Methods[i].SyscallOffset = w.Len() + m.SyscallOffset = w.Len() emit.Syscall(w.BinWriter, interopnames.SystemContractCallNative) emit.Opcodes(w.BinWriter, opcode.RET) + + abiMethods = append(abiMethods, *m.MD) + methods = append(methods, m.HFSpecificMethodAndPrice) } if w.Err != nil { panic(fmt.Errorf("can't create native contract script: %w", w.Err)) } + for i := range c.Events { + e := c.Events[i] + if !(e.ActiveFrom == nil || (hf != nil && (*e.ActiveFrom).Cmp(*hf) <= 0)) { + continue + } + + abiEvents = append(abiEvents, *e.MD) + events = append(events, e.HFSpecificEvent) + } + + // NEF is now stored in the contract state and affects state dump. + // Therefore, values are taken from C# node. + nf := nef.File{ + Header: nef.Header{ + Magic: nef.Magic, + Compiler: "neo-core-v3.0", + }, + Tokens: []nef.MethodToken{}, // avoid `nil` result during JSON marshalling, + Script: w.Bytes(), + } + nf.Checksum = nf.CalculateChecksum() + m := manifest.DefaultManifest(c.Name) + m.ABI.Methods = abiMethods + m.ABI.Events = abiEvents + if c.onManifestConstruction != nil { + c.onManifestConstruction(m) + } + var key config.Hardfork + if hf != nil { + key = *hf + } + md := &HFSpecificContractMD{ + ContractBase: state.ContractBase{ + ID: c.ID, + Hash: c.Hash, + NEF: nf, + Manifest: *m, + }, + Methods: methods, + Events: events, + } - c.NEF.Script = w.Bytes() - c.NEF.Checksum = c.NEF.CalculateChecksum() + c.mdCacheLock.Lock() + c.mdCache[key] = md + c.mdCacheLock.Unlock() + return md } // AddMethod adds a new method to a native contract. @@ -215,36 +345,35 @@ func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method) { md.MD = desc desc.Safe = md.RequiredFlags&(callflag.All^callflag.ReadOnly) == 0 - index := sort.Search(len(c.Manifest.ABI.Methods), func(i int) bool { - md := c.Manifest.ABI.Methods[i] + index := sort.Search(len(c.Methods), func(i int) bool { + md := c.Methods[i].MD if md.Name != desc.Name { return md.Name >= desc.Name } return len(md.Parameters) > len(desc.Parameters) }) - c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, manifest.Method{}) - copy(c.Manifest.ABI.Methods[index+1:], c.Manifest.ABI.Methods[index:]) - c.Manifest.ABI.Methods[index] = *desc - - // Cache follows the same order. c.Methods = append(c.Methods, MethodAndPrice{}) copy(c.Methods[index+1:], c.Methods[index:]) c.Methods[index] = *md + + if md.ActiveFrom != nil { + c.ActiveHFs[*md.ActiveFrom] = struct{}{} + } } // GetMethodByOffset returns method with the provided offset. // Offset is offset of `System.Contract.CallNative` syscall. -func (c *ContractMD) GetMethodByOffset(offset int) (MethodAndPrice, bool) { +func (c *HFSpecificContractMD) GetMethodByOffset(offset int) (HFSpecificMethodAndPrice, bool) { for k := range c.Methods { if c.Methods[k].SyscallOffset == offset { return c.Methods[k], true } } - return MethodAndPrice{}, false + return HFSpecificMethodAndPrice{}, false } // GetMethod returns method `name` with the specified number of parameters. -func (c *ContractMD) GetMethod(name string, paramCount int) (MethodAndPrice, bool) { +func (c *HFSpecificContractMD) GetMethod(name string, paramCount int) (HFSpecificMethodAndPrice, bool) { index := sort.Search(len(c.Methods), func(i int) bool { md := c.Methods[i] res := strings.Compare(name, md.MD.Name) @@ -261,15 +390,16 @@ func (c *ContractMD) GetMethod(name string, paramCount int) (MethodAndPrice, boo return md, true } } - return MethodAndPrice{}, false + return HFSpecificMethodAndPrice{}, false } // AddEvent adds a new event to the native contract. -func (c *ContractMD) AddEvent(name string, ps ...manifest.Parameter) { - c.Manifest.ABI.Events = append(c.Manifest.ABI.Events, manifest.Event{ - Name: name, - Parameters: ps, - }) +func (c *ContractMD) AddEvent(md Event) { + c.Events = append(c.Events, md) + + if md.ActiveFrom != nil { + c.ActiveHFs[*md.ActiveFrom] = struct{}{} + } } // Sort sorts interop functions by id. diff --git a/pkg/core/native/compatibility_test.go b/pkg/core/native/compatibility_test.go index 77c0878d08..cf75fad7c3 100644 --- a/pkg/core/native/compatibility_test.go +++ b/pkg/core/native/compatibility_test.go @@ -12,12 +12,14 @@ import ( func TestNamesASCII(t *testing.T) { cfg := config.ProtocolConfiguration{P2PSigExtensions: true} cs := NewContracts(cfg) + latestHF := config.HFBasilisk for _, c := range cs.Contracts { require.True(t, isASCII(c.Metadata().Name)) - for _, m := range c.Metadata().Methods { + hfMD := c.Metadata().HFSpecificContractMD(&latestHF) + for _, m := range hfMD.Methods { require.True(t, isASCII(m.MD.Name)) } - for _, e := range c.Metadata().Manifest.ABI.Events { + for _, e := range hfMD.Manifest.ABI.Events { require.True(t, isASCII(e.Name)) } } diff --git a/pkg/core/native/contract_test.go b/pkg/core/native/contract_test.go index 3e61320f19..ba9a3029a5 100644 --- a/pkg/core/native/contract_test.go +++ b/pkg/core/native/contract_test.go @@ -12,10 +12,12 @@ import ( func TestNativeGetMethod(t *testing.T) { cfg := config.ProtocolConfiguration{P2PSigExtensions: true} cs := NewContracts(cfg) + latestHF := config.HFBasilisk for _, c := range cs.Contracts { + hfMD := c.Metadata().HFSpecificContractMD(&latestHF) t.Run(c.Metadata().Name, func(t *testing.T) { - for _, m := range c.Metadata().Methods { - _, ok := c.Metadata().GetMethod(m.MD.Name, len(m.MD.Parameters)) + for _, m := range hfMD.Methods { + _, ok := hfMD.GetMethod(m.MD.Name, len(m.MD.Parameters)) require.True(t, ok) } }) diff --git a/pkg/core/native/crypto.go b/pkg/core/native/crypto.go index fb9d91bd8e..78eb3697ca 100644 --- a/pkg/core/native/crypto.go +++ b/pkg/core/native/crypto.go @@ -41,7 +41,6 @@ const cryptoContractID = -3 func newCrypto() *Crypto { c := &Crypto{ContractMD: *interop.NewContractMD(nativenames.CryptoLib, cryptoContractID)} - defer c.UpdateHash() desc := newDescriptor("sha256", smartcontract.ByteArrayType, manifest.NewParameter("data", smartcontract.ByteArrayType)) @@ -310,7 +309,7 @@ func (c *Crypto) Metadata() *interop.ContractMD { } // Initialize implements the Contract interface. -func (c *Crypto) Initialize(ic *interop.Context) error { +func (c *Crypto) Initialize(ic *interop.Context, hf *config.Hardfork) error { return nil } diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index b8d5cd016d..a6c0809114 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -106,7 +106,6 @@ func newDesignate(p2pSigExtensionsEnabled bool, initialNodeRoles map[noderoles.R s := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation, designateContractID)} s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled s.initialNodeRoles = initialNodeRoles - defer s.UpdateHash() desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType, manifest.NewParameter("role", smartcontract.IntegerType), @@ -120,9 +119,11 @@ func newDesignate(p2pSigExtensionsEnabled bool, initialNodeRoles map[noderoles.R md = newMethodAndPrice(s.designateAsRole, 1<<15, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) - s.AddEvent(DesignationEventName, + eDesc := newEventDescriptor(DesignationEventName, manifest.NewParameter("Role", smartcontract.IntegerType), manifest.NewParameter("BlockIndex", smartcontract.IntegerType)) + eMD := newEvent(eDesc) + s.AddEvent(eMD) return s } @@ -130,7 +131,11 @@ func newDesignate(p2pSigExtensionsEnabled bool, initialNodeRoles map[noderoles.R // Initialize initializes Designation contract. It is called once at native Management's OnPersist // at the genesis block, and we can't properly fill the cache at this point, as there are no roles // data in the storage. -func (s *Designate) Initialize(ic *interop.Context) error { +func (s *Designate) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != s.ActiveIn() { + return nil + } + cache := &DesignationCache{} ic.DAO.SetCache(s.ID, cache) diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index 5d2d1c230e..badf4d91e2 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -32,17 +32,26 @@ func Call(ic *interop.Context) error { return fmt.Errorf("native contract %s (version %d) not found", curr.StringLE(), version) } var ( - meta = c.Metadata() - activeIn = c.ActiveIn() + genericMeta = c.Metadata() + activeIn = c.ActiveIn() + persistedH = ic.BlockHeight() ) if activeIn != nil { height, ok := ic.Hardforks[activeIn.String()] // Persisting block must not be taken into account, native contract can be called // only AFTER its initialization block persist, thus, can't use ic.IsHardforkEnabled. - if !ok || ic.BlockHeight() < height { - return fmt.Errorf("native contract %s is active after hardfork %s", meta.Name, activeIn.String()) + if !ok || persistedH < height { + return fmt.Errorf("native contract %s is active after hardfork %s", genericMeta.Name, activeIn.String()) } } + var current config.Hardfork + for _, hf := range config.Hardforks { + if !ic.IsHardforkEnabled(hf) { + break + } + current = hf + } + meta := genericMeta.HFSpecificContractMD(¤t) m, ok := meta.GetMethodByOffset(ic.VM.Context().IP()) if !ok { return fmt.Errorf("method not found") diff --git a/pkg/core/native/ledger.go b/pkg/core/native/ledger.go index 7fb0266793..0e8892d96f 100644 --- a/pkg/core/native/ledger.go +++ b/pkg/core/native/ledger.go @@ -31,7 +31,6 @@ func newLedger() *Ledger { var l = &Ledger{ ContractMD: *interop.NewContractMD(nativenames.Ledger, ledgerContractID), } - defer l.UpdateHash() desc := newDescriptor("currentHash", smartcontract.Hash256Type) md := newMethodAndPrice(l.currentHash, 1<<15, callflag.ReadStates) @@ -81,7 +80,7 @@ func (l *Ledger) Metadata() *interop.ContractMD { } // Initialize implements the Contract interface. -func (l *Ledger) Initialize(ic *interop.Context) error { +func (l *Ledger) Initialize(ic *interop.Context, hf *config.Hardfork) error { return nil } diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 2335131640..14c6b3dedd 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -101,7 +101,6 @@ func newManagement() *Management { var m = &Management{ ContractMD: *interop.NewContractMD(nativenames.Management, ManagementContractID), } - defer m.UpdateHash() desc := newDescriptor("getContract", smartcontract.ArrayType, manifest.NewParameter("hash", smartcontract.Hash160Type)) @@ -164,9 +163,17 @@ func newManagement() *Management { m.AddMethod(md, desc) hashParam := manifest.NewParameter("Hash", smartcontract.Hash160Type) - m.AddEvent(contractDeployNotificationName, hashParam) - m.AddEvent(contractUpdateNotificationName, hashParam) - m.AddEvent(contractDestroyNotificationName, hashParam) + eDesc := newEventDescriptor(contractDeployNotificationName, hashParam) + eMD := newEvent(eDesc) + m.AddEvent(eMD) + + eDesc = newEventDescriptor(contractUpdateNotificationName, hashParam) + eMD = newEvent(eDesc) + m.AddEvent(eMD) + + eDesc = newEventDescriptor(contractDestroyNotificationName, hashParam) + eMD = newEvent(eDesc) + m.AddEvent(eMD) return m } @@ -220,6 +227,11 @@ func (m *Management) getContractByID(ic *interop.Context, args []stackitem.Item) // GetContract returns a contract with the given hash from the given DAO. func GetContract(d *dao.Simple, hash util.Uint160) (*state.Contract, error) { cache := d.GetROCache(ManagementContractID).(*ManagementCache) + return getContract(cache, hash) +} + +// getContract returns a contract with the given hash from provided RO or RW cache. +func getContract(cache *ManagementCache, hash util.Uint160) (*state.Contract, error) { cs, ok := cache.contracts[hash] if !ok { return nil, storage.ErrKeyNotFound @@ -583,22 +595,54 @@ func updateContractCache(cache *ManagementCache, cs *state.Contract) { func (m *Management) OnPersist(ic *interop.Context) error { var cache *ManagementCache for _, native := range ic.Natives { - activeIn := native.ActiveIn() - if !(activeIn == nil && ic.Block.Index == 0 || - activeIn != nil && ic.IsHardforkActivation(*activeIn)) { + var ( + activeIn = native.ActiveIn() + isDeploy bool + isUpdate bool + ) + isDeploy = activeIn == nil && ic.Block.Index == 0 || + activeIn != nil && ic.IsHardforkActivation(*activeIn) + if !isDeploy { + for hf := range native.Metadata().ActiveHFs { + if ic.IsHardforkActivation(hf) { + isUpdate = true + activeIn = &hf // reuse ActiveIn variable for the current hardfork. + break + } + } + } + if !(isDeploy || isUpdate) { continue } - md := native.Metadata() - cs := &state.Contract{ - ContractBase: md.ContractBase, - } - if err := native.Initialize(ic); err != nil { - return fmt.Errorf("initializing %s native contract: %w", md.Name, err) + base := md.HFSpecificContractMD(activeIn).ContractBase + var cs *state.Contract + switch { + case isDeploy: + cs = &state.Contract{ + ContractBase: base, + } + case isUpdate: + if cache == nil { + cache = ic.DAO.GetRWCache(m.ID).(*ManagementCache) + } + oldcontract, err := getContract(cache, md.Hash) + if err != nil { + return fmt.Errorf("failed to retrieve native %s from cache: %w", md.Name, err) + } + + contract := *oldcontract // Make a copy, don't ruin cached contract and cache. + contract.NEF = base.NEF + contract.Manifest = base.Manifest + contract.UpdateCounter++ + cs = &contract } err := putContractState(ic.DAO, cs, false) // Perform cache update manually. if err != nil { - return err + return fmt.Errorf("failed to put contract state: %w", err) + } + if err := native.Initialize(ic, activeIn); err != nil { + return fmt.Errorf("initializing %s native contract at HF %d: %w", md.Name, activeIn, err) } if cache == nil { cache = ic.DAO.GetRWCache(m.ID).(*ManagementCache) @@ -666,7 +710,11 @@ func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 { } // Initialize implements the Contract interface. -func (m *Management) Initialize(ic *interop.Context) error { +func (m *Management) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != m.ActiveIn() { + return nil + } + setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee) setIntWithKey(m.ID, ic.DAO, keyNextAvailableID, 1) @@ -681,6 +729,7 @@ func (m *Management) Initialize(ic *interop.Context) error { // ActiveIn implements the Contract interface. func (m *Management) ActiveIn() *config.Hardfork { + // TODO: consider replacing nil with config.Hardfork(0) for callers code unification. return nil } diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index c8a13b4a5b..d3fb8872d3 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -20,9 +20,9 @@ func TestDeployGetUpdateDestroyContract(t *testing.T) { mgmt.Policy = newPolicy(false) d := dao.NewSimple(storage.NewMemoryStore(), false) ic := &interop.Context{DAO: d} - err := mgmt.Initialize(ic) + err := mgmt.Initialize(ic, nil) require.NoError(t, err) - require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d})) + require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d}, nil)) script := []byte{byte(opcode.RET)} sender := util.Uint160{1, 2, 3} ne, err := nef.NewFile(script) @@ -97,9 +97,9 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { mgmt := newManagement() mgmt.Policy = newPolicy(false) d := dao.NewSimple(storage.NewMemoryStore(), false) - err := mgmt.Initialize(&interop.Context{DAO: d}) + err := mgmt.Initialize(&interop.Context{DAO: d}, nil) require.NoError(t, err) - require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d})) + require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d}, nil)) err = mgmt.InitializeCache(0, d) require.NoError(t, err) diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 9dfc08af63..e5bde0addd 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -37,7 +37,6 @@ func newGAS(init int64, p2pSigExtensionsEnabled bool) *GAS { initialSupply: init, p2pSigExtensionsEnabled: p2pSigExtensionsEnabled, } - defer g.UpdateHash() nep17 := newNEP17Native(nativenames.Gas, gasContractID) nep17.symbol = "GAS" @@ -83,7 +82,11 @@ func (g *GAS) balanceFromBytes(si *state.StorageItem) (*big.Int, error) { } // Initialize initializes a GAS contract. -func (g *GAS) Initialize(ic *interop.Context) error { +func (g *GAS) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != g.ActiveIn() { + return nil + } + if err := g.nep17TokenNative.Initialize(ic); err != nil { return err } diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 2822716d9f..a90755c2d0 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -172,7 +172,6 @@ func makeValidatorKey(key *keys.PublicKey) []byte { // newNEO returns NEO native contract. func newNEO(cfg config.ProtocolConfiguration) *NEO { n := &NEO{} - defer n.UpdateHash() nep17 := newNEP17Native(nativenames.Neo, neoContractID) nep17.symbol = "NEO" @@ -258,27 +257,39 @@ func newNEO(cfg config.ProtocolConfiguration) *NEO { md = newMethodAndPrice(n.setRegisterPrice, 1<<15, callflag.States) n.AddMethod(md, desc) - n.AddEvent("CandidateStateChanged", + eDesc := newEventDescriptor("CandidateStateChanged", manifest.NewParameter("pubkey", smartcontract.PublicKeyType), manifest.NewParameter("registered", smartcontract.BoolType), manifest.NewParameter("votes", smartcontract.IntegerType), ) - n.AddEvent("Vote", + eMD := newEvent(eDesc) + n.AddEvent(eMD) + + eDesc = newEventDescriptor("Vote", manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("from", smartcontract.PublicKeyType), manifest.NewParameter("to", smartcontract.PublicKeyType), manifest.NewParameter("amount", smartcontract.IntegerType), ) - n.AddEvent("CommitteeChanged", + eMD = newEvent(eDesc) + n.AddEvent(eMD) + + eDesc = newEventDescriptor("CommitteeChanged", manifest.NewParameter("old", smartcontract.ArrayType), manifest.NewParameter("new", smartcontract.ArrayType), ) + eMD = newEvent(eDesc) + n.AddEvent(eMD) return n } // Initialize initializes a NEO contract. -func (n *NEO) Initialize(ic *interop.Context) error { +func (n *NEO) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != n.ActiveIn() { + return nil + } + if err := n.nep17TokenNative.Initialize(ic); err != nil { return err } diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index 0d6d67b14b..ebc6cad29c 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -6,6 +6,7 @@ import ( "math" "math/big" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" @@ -45,8 +46,9 @@ func (c *nep17TokenNative) Metadata() *interop.ContractMD { } func newNEP17Native(name string, id int32) *nep17TokenNative { - n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name, id)} - n.Manifest.SupportedStandards = []string{manifest.NEP17StandardName} + n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name, id, func(m *manifest.Manifest) { + m.SupportedStandards = []string{manifest.NEP17StandardName} + })} desc := newDescriptor("symbol", smartcontract.StringType) md := newMethodAndPrice(n.Symbol, 0, callflag.NoneFlag) @@ -77,7 +79,9 @@ func newNEP17Native(name string, id int32) *nep17TokenNative { md.StorageFee = 50 n.AddMethod(md, desc) - n.AddEvent("Transfer", transferParams...) + eDesc := newEventDescriptor("Transfer", transferParams...) + eMD := newEvent(eDesc) + n.AddEvent(eMD) return n } @@ -319,12 +323,40 @@ func newDescriptor(name string, ret smartcontract.ParamType, ps ...manifest.Para } } -func newMethodAndPrice(f interop.Method, cpuFee int64, flags callflag.CallFlag) *interop.MethodAndPrice { - return &interop.MethodAndPrice{ - Func: f, - CPUFee: cpuFee, - RequiredFlags: flags, +func newMethodAndPrice(f interop.Method, cpuFee int64, flags callflag.CallFlag, activeFrom ...*config.Hardfork) *interop.MethodAndPrice { + md := &interop.MethodAndPrice{ + HFSpecificMethodAndPrice: interop.HFSpecificMethodAndPrice{ + Func: f, + CPUFee: cpuFee, + RequiredFlags: flags, + }, } + if len(activeFrom) != 0 { + md.ActiveFrom = activeFrom[0] + } + return md +} + +func newEventDescriptor(name string, ps ...manifest.Parameter) *manifest.Event { + if len(ps) == 0 { + ps = []manifest.Parameter{} + } + return &manifest.Event{ + Name: name, + Parameters: ps, + } +} + +func newEvent(desc *manifest.Event, activeFrom ...*config.Hardfork) interop.Event { + md := interop.Event{ + HFSpecificEvent: interop.HFSpecificEvent{ + MD: desc, + }, + } + if len(activeFrom) != 0 { + md.ActiveFrom = activeFrom[0] + } + return md } func toBigInt(s stackitem.Item) *big.Int { diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index ed60d3ff8d..c6343499e9 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -73,7 +73,6 @@ func copyNotaryCache(src, dst *NotaryCache) { // newNotary returns Notary native contract. func newNotary() *Notary { n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)} - defer n.UpdateHash() desc := newDescriptor("onNEP17Payment", smartcontract.VoidType, manifest.NewParameter("from", smartcontract.Hash160Type), @@ -127,7 +126,11 @@ func (n *Notary) Metadata() *interop.ContractMD { } // Initialize initializes Notary native contract and implements the Contract interface. -func (n *Notary) Initialize(ic *interop.Context) error { +func (n *Notary) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != n.ActiveIn() { + return nil + } + setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta) cache := &NotaryCache{ diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 012fc3064f..ea46b84198 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -118,7 +118,6 @@ func newOracle() *Oracle { ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID), newRequests: make(map[uint64]*state.OracleRequest), } - defer o.UpdateHash() o.oracleScript = CreateOracleResponseScript(o.Hash) @@ -139,12 +138,17 @@ func newOracle() *Oracle { md = newMethodAndPrice(o.verify, 1<<15, callflag.NoneFlag) o.AddMethod(md, desc) - o.AddEvent("OracleRequest", manifest.NewParameter("Id", smartcontract.IntegerType), + eDesc := newEventDescriptor("OracleRequest", manifest.NewParameter("Id", smartcontract.IntegerType), manifest.NewParameter("RequestContract", smartcontract.Hash160Type), manifest.NewParameter("Url", smartcontract.StringType), manifest.NewParameter("Filter", smartcontract.StringType)) - o.AddEvent("OracleResponse", manifest.NewParameter("Id", smartcontract.IntegerType), + eMD := newEvent(eDesc) + o.AddEvent(eMD) + + eDesc = newEventDescriptor("OracleResponse", manifest.NewParameter("Id", smartcontract.IntegerType), manifest.NewParameter("OriginalTx", smartcontract.Hash256Type)) + eMD = newEvent(eDesc) + o.AddEvent(eMD) desc = newDescriptor("getPrice", smartcontract.IntegerType) md = newMethodAndPrice(o.getPrice, 1<<15, callflag.ReadStates) @@ -241,7 +245,11 @@ func (o *Oracle) Metadata() *interop.ContractMD { } // Initialize initializes an Oracle contract. -func (o *Oracle) Initialize(ic *interop.Context) error { +func (o *Oracle) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != o.ActiveIn() { + return nil + } + setIntWithKey(o.ID, ic.DAO, prefixRequestID, 0) setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, DefaultOracleRequestPrice) diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index b5ebb8a56f..71fd397af7 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -105,7 +105,6 @@ func newPolicy(p2pSigExtensionsEnabled bool) *Policy { ContractMD: *interop.NewContractMD(nativenames.Policy, policyContractID), p2pSigExtensionsEnabled: p2pSigExtensionsEnabled, } - defer p.UpdateHash() desc := newDescriptor("getFeePerByte", smartcontract.IntegerType) md := newMethodAndPrice(p.getFeePerByte, 1<<15, callflag.ReadStates) @@ -169,7 +168,11 @@ func (p *Policy) Metadata() *interop.ContractMD { } // Initialize initializes Policy native contract and implements the Contract interface. -func (p *Policy) Initialize(ic *interop.Context) error { +func (p *Policy) Initialize(ic *interop.Context, hf *config.Hardfork) error { + if hf != p.ActiveIn() { + return nil + } + setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte) setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor) setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice) diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index 90d2c579c5..81e74b638d 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -46,7 +46,6 @@ var ( func newStd() *Std { s := &Std{ContractMD: *interop.NewContractMD(nativenames.StdLib, stdContractID)} - defer s.UpdateHash() desc := newDescriptor("serialize", smartcontract.ByteArrayType, manifest.NewParameter("item", smartcontract.AnyType)) @@ -439,7 +438,7 @@ func (s *Std) Metadata() *interop.ContractMD { } // Initialize implements the Contract interface. -func (s *Std) Initialize(ic *interop.Context) error { +func (s *Std) Initialize(ic *interop.Context, hf *config.Hardfork) error { return nil }