neo-go/pkg/core/interop/context.go
Roman Khimov 9e7fd5180a interop: slightly rephrase BuildHFSpecificMD logic
Technically, we can always buildHFSpecificMD() if contract is active, but we
just optimize the build out in some cases. switch slightly obfuscates this
and requires having the call in two branches.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2024-11-25 14:45:26 +03:00

571 lines
19 KiB
Go

package interop
import (
"cmp"
"context"
"encoding/binary"
"errors"
"fmt"
"slices"
"strings"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"go.uber.org/zap"
)
const (
// DefaultBaseExecFee specifies the default multiplier for opcode and syscall prices.
DefaultBaseExecFee = 30
// ContextNonceDataLen is a length of [Context.NonceData] in bytes.
ContextNonceDataLen = 16
)
// Ledger is the interface to Blockchain required for Context functionality.
type Ledger interface {
BlockHeight() uint32
CurrentBlockHash() util.Uint256
GetBlock(hash util.Uint256) (*block.Block, error)
GetConfig() config.Blockchain
GetHeaderHash(uint32) util.Uint256
}
// Context represents context in which interops are executed.
type Context struct {
Chain Ledger
Container hash.Hashable
Network uint32
Hardforks map[string]uint32
Natives []Contract
Trigger trigger.Type
Block *block.Block
NonceData [ContextNonceDataLen]byte
Tx *transaction.Transaction
DAO *dao.Simple
Notifications []state.NotificationEvent
Log *zap.Logger
VM *vm.VM
Functions []Function
Invocations map[util.Uint160]int
cancelFuncs []context.CancelFunc
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error)
baseExecFee int64
baseStorageFee int64
loadToken func(ic *Context, id int32) error
GetRandomCounter uint32
signers []transaction.Signer
}
// NewContext returns new interop context.
func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, baseStorageFee int64,
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error), natives []Contract,
loadTokenFunc func(ic *Context, id int32) error,
block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context {
dao := d.GetPrivate()
cfg := bc.GetConfig().ProtocolConfiguration
return &Context{
Chain: bc,
Network: uint32(cfg.Magic),
Hardforks: cfg.Hardforks,
Natives: natives,
Trigger: trigger,
Block: block,
Tx: tx,
DAO: dao,
Log: log,
Invocations: make(map[util.Uint160]int),
getContract: getContract,
baseExecFee: baseExecFee,
baseStorageFee: baseStorageFee,
loadToken: loadTokenFunc,
}
}
// InitNonceData initializes nonce to be used in `GetRandom` calculations.
func (ic *Context) InitNonceData() {
if tx, ok := ic.Container.(*transaction.Transaction); ok {
ic.NonceData = [ContextNonceDataLen]byte(tx.Hash().BytesBE())
}
if ic.Block != nil {
nonce := ic.Block.Nonce
nonce ^= binary.LittleEndian.Uint64(ic.NonceData[:])
binary.LittleEndian.PutUint64(ic.NonceData[:], nonce)
}
}
// UseSigners allows overriding signers used in this context.
func (ic *Context) UseSigners(s []transaction.Signer) {
ic.signers = s
}
// Signers returns signers witnessing the current execution context.
func (ic *Context) Signers() []transaction.Signer {
if ic.signers != nil {
return ic.signers
}
if ic.Tx != nil {
return ic.Tx.Signers
}
return nil
}
// Function binds function name, id with the function itself and the price,
// it's supposed to be inited once for all interopContexts, so it doesn't use
// vm.InteropFuncPrice directly.
type Function struct {
ID uint32
Name string
Func func(*Context) error
// ParamCount is a number of function parameters.
ParamCount int
Price int64
// RequiredFlags is a set of flags which must be set during script invocations.
// Default value is NoneFlag i.e. no flags are required.
RequiredFlags callflag.CallFlag
}
// Method is a signature for a native method.
type Method = func(ic *Context, args []stackitem.Item) stackitem.Item
// MethodAndPrice is a generic hardfork-independent native contract method descriptor.
type MethodAndPrice struct {
HFSpecificMethodAndPrice
ActiveFrom *config.Hardfork
ActiveTill *config.Hardfork
}
// HFSpecificMethodAndPrice is a hardfork-specific native contract method descriptor.
type HFSpecificMethodAndPrice struct {
Func Method
MD *manifest.Method
CPUFee int64
StorageFee int64
SyscallOffset int
RequiredFlags callflag.CallFlag
}
// Event is a generic hardfork-independent native contract event descriptor.
type Event struct {
HFSpecificEvent
ActiveFrom *config.Hardfork
ActiveTill *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 performs native contract initialization on contract deploy or update.
// Active hardfork is passed as the second argument.
Initialize(*Context, *config.Hardfork, *HFSpecificContractMD) 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
// been deployed, but in-memory cached data were lost due to the node reset.
// 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 generic hardfork-independent native contract instance.
type ContractMD struct {
ID int32
Hash util.Uint160
Name string
// 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
// 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 should be called for active
// hardforks. Note, that unlike the C# implementation, this map doesn't include contract's activation hardfork.
// This map is being initialized on contract creation and used as a read-only, hence, not protected
// by mutex.
ActiveHFs map[config.Hardfork]struct{}
// mdCache contains hardfork-specific ready-to-use contract descriptors. This cache is initialized in the native
// contracts constructors, and acts as read-only during the whole node lifetime, thus not protected by mutex.
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
c.Hash = state.CreateNativeContractHash(c.Name)
c.ActiveHFs = make(map[config.Hardfork]struct{})
c.mdCache = make(map[config.Hardfork]*HFSpecificContractMD)
return c
}
// 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). Calling this method for hardforks older than the contract
// activation hardfork is a no-op.
func (c *ContractMD) HFSpecificContractMD(hf *config.Hardfork) *HFSpecificContractMD {
var key config.Hardfork
if hf != nil {
key = *hf
}
md, ok := c.mdCache[key]
if !ok {
panic(fmt.Errorf("native contract descriptor cache is not initialized: contract %s, hardfork %s", c.Hash.StringLE(), key))
}
if md == nil {
panic(fmt.Errorf("native contract descriptor cache is nil: contract %s, hardfork %s", c.Hash.StringLE(), key))
}
return md
}
// BuildHFSpecificMD generates and caches contract's descriptor for every known hardfork.
func (c *ContractMD) BuildHFSpecificMD(activeIn *config.Hardfork) {
var start config.Hardfork
if activeIn != nil {
start = *activeIn
}
for _, hf := range append([]config.Hardfork{config.HFDefault}, config.Hardforks...) {
if hf.Cmp(start) < 0 {
continue
}
_, contractHasChanges := c.ActiveHFs[hf]
if hf.Cmp(start) == 0 || contractHasChanges {
c.buildHFSpecificMD(hf)
} else {
// Optimize out MD rebuild, the contract is the same.
// Intentionally omit HFSpecificContractMD structure copying since mdCache is read-only.
c.mdCache[hf] = c.mdCache[hf.Prev()]
}
}
}
// buildHFSpecificMD builds hardfork-specific contract descriptor that includes methods and events active starting from
// the specified hardfork or older. It also updates cache with the received value.
func (c *ContractMD) buildHFSpecificMD(hf config.Hardfork) {
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 {
m := c.methods[i]
if (m.ActiveFrom != nil && (*m.ActiveFrom).Cmp(hf) > 0) ||
(m.ActiveTill != nil && (*m.ActiveTill).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)
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 && (*e.ActiveFrom).Cmp(hf) > 0) ||
(e.ActiveTill != nil && (*e.ActiveTill).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)
}
md := &HFSpecificContractMD{
ContractBase: state.ContractBase{
ID: c.ID,
Hash: c.Hash,
NEF: nf,
Manifest: *m,
},
Methods: methods,
Events: events,
}
c.mdCache[hf] = md
}
// AddMethod adds a new method to a native contract.
func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method) {
md.MD = desc
desc.Safe = md.RequiredFlags&(callflag.All^callflag.ReadOnly) == 0
index, _ := slices.BinarySearchFunc(c.methods, *md, func(e, t MethodAndPrice) int {
return cmp.Or(
cmp.Compare(e.MD.Name, t.MD.Name),
cmp.Compare(len(e.MD.Parameters), len(t.MD.Parameters)),
)
})
c.methods = slices.Insert(c.methods, index, *md)
if md.ActiveFrom != nil {
c.ActiveHFs[*md.ActiveFrom] = struct{}{}
}
if md.ActiveTill != nil {
c.ActiveHFs[*md.ActiveTill] = struct{}{}
}
}
// GetMethodByOffset returns method with the provided offset.
// Offset is offset of `System.Contract.CallNative` syscall.
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 HFSpecificMethodAndPrice{}, false
}
// GetMethod returns method `name` with the specified number of parameters.
func (c *HFSpecificContractMD) GetMethod(name string, paramCount int) (HFSpecificMethodAndPrice, bool) {
index, ok := slices.BinarySearchFunc(c.Methods, HFSpecificMethodAndPrice{}, func(a, _ HFSpecificMethodAndPrice) int {
res := strings.Compare(a.MD.Name, name)
if res != 0 {
return res
}
return cmp.Compare(len(a.MD.Parameters), paramCount)
})
// Exact match is possible only for specific paramCount, but if we're
// searching for _some_ method with this name (-1) we're taking the
// first one.
if ok || (index < len(c.Methods) && c.Methods[index].MD.Name == name && paramCount == -1) {
return c.Methods[index], true
}
return HFSpecificMethodAndPrice{}, false
}
// AddEvent adds a new event to the native contract.
func (c *ContractMD) AddEvent(md Event) {
c.events = append(c.events, md)
if md.ActiveFrom != nil {
c.ActiveHFs[*md.ActiveFrom] = struct{}{}
}
if md.ActiveTill != nil {
c.ActiveHFs[*md.ActiveTill] = struct{}{}
}
}
// Sort sorts interop functions by id.
func Sort(fs []Function) {
slices.SortFunc(fs, func(a, b Function) int { return cmp.Compare(a.ID, b.ID) })
}
// GetContract returns a contract by its hash in the current interop context.
func (ic *Context) GetContract(hash util.Uint160) (*state.Contract, error) {
return ic.getContract(ic.DAO, hash)
}
// GetFunction returns metadata for interop with the specified id.
func (ic *Context) GetFunction(id uint32) *Function {
n, ok := slices.BinarySearchFunc(ic.Functions, Function{}, func(a, _ Function) int {
return cmp.Compare(a.ID, id)
})
if !ok {
return nil
}
return &ic.Functions[n]
}
// BaseExecFee represents factor to multiply syscall prices with.
func (ic *Context) BaseExecFee() int64 {
return ic.baseExecFee
}
// BaseStorageFee represents price for storing one byte of data in the contract storage.
func (ic *Context) BaseStorageFee() int64 {
return ic.baseStorageFee
}
// LoadToken wraps externally provided load-token loading function providing it with context,
// this function can then be easily used by VM.
func (ic *Context) LoadToken(id int32) error {
return ic.loadToken(ic, id)
}
// SyscallHandler handles syscall with id.
func (ic *Context) SyscallHandler(_ *vm.VM, id uint32) error {
f := ic.GetFunction(id)
if f == nil {
return errors.New("syscall not found")
}
cf := ic.VM.Context().GetCallFlags()
if !cf.Has(f.RequiredFlags) {
return fmt.Errorf("missing call flags: %05b vs %05b", cf, f.RequiredFlags)
}
if !ic.VM.AddGas(f.Price * ic.BaseExecFee()) {
return errors.New("insufficient amount of gas")
}
return f.Func(ic)
}
// SpawnVM spawns a new VM with the specified gas limit and set context.VM field.
func (ic *Context) SpawnVM() *vm.VM {
v := vm.NewWithTrigger(ic.Trigger)
ic.initVM(v)
return v
}
func (ic *Context) initVM(v *vm.VM) {
v.LoadToken = ic.LoadToken
v.GasLimit = -1
v.SyscallHandler = ic.SyscallHandler
v.SetPriceGetter(ic.GetPrice)
ic.VM = v
}
// ReuseVM resets given VM and allows to reuse it in the current context.
func (ic *Context) ReuseVM(v *vm.VM) {
v.Reset(ic.Trigger)
ic.initVM(v)
}
// RegisterCancelFunc adds the given function to the list of functions to be called after the VM
// finishes script execution.
func (ic *Context) RegisterCancelFunc(f context.CancelFunc) {
if f != nil {
ic.cancelFuncs = append(ic.cancelFuncs, f)
}
}
// Finalize calls all registered cancel functions to release the occupied resources.
func (ic *Context) Finalize() {
for _, f := range ic.cancelFuncs {
f()
}
ic.cancelFuncs = nil
}
// Exec executes loaded VM script and calls registered finalizers to release the occupied resources.
func (ic *Context) Exec() error {
defer ic.Finalize()
return ic.VM.Run()
}
// BlockHeight returns the latest persisted and stored block height/index.
// Persisting block index is not taken into account. If Context's block is set,
// then BlockHeight calculations relies on persisting block index.
func (ic *Context) BlockHeight() uint32 {
if ic.Block != nil {
return ic.Block.Index - 1 // Persisting block is not yet stored.
}
return ic.Chain.BlockHeight()
}
// CurrentBlockHash returns current block hash got from Context's block if it's set.
func (ic *Context) CurrentBlockHash() util.Uint256 {
if ic.Block != nil {
return ic.Chain.GetHeaderHash(ic.Block.Index - 1) // Persisting block is not yet stored.
}
return ic.Chain.CurrentBlockHash()
}
// GetBlock returns block if it exists and available at the current Context's height.
func (ic *Context) GetBlock(hash util.Uint256) (*block.Block, error) {
block, err := ic.Chain.GetBlock(hash)
if err != nil {
return nil, err
}
if block.Index > ic.BlockHeight() { // persisting block is not reachable.
return nil, storage.ErrKeyNotFound
}
return block, nil
}
// IsHardforkEnabled tells whether specified hard-fork enabled at the current context height.
func (ic *Context) IsHardforkEnabled(hf config.Hardfork) bool {
height, ok := ic.Hardforks[hf.String()]
if ok {
return (ic.BlockHeight() + 1) >= height // persisting block should be taken into account.
}
// Completely rely on proper hardforks initialisation made by core.NewBlockchain.
return false
}
// IsHardforkActivation denotes whether current block height is the height of
// specified hardfork activation.
func (ic *Context) IsHardforkActivation(hf config.Hardfork) bool {
// Completely rely on proper hardforks initialisation made by core.NewBlockchain.
height, ok := ic.Hardforks[hf.String()]
return ok && ic.Block.Index == height
}
// AddNotification creates notification event and appends it to the notification list.
func (ic *Context) AddNotification(hash util.Uint160, name string, item *stackitem.Array) {
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
ScriptHash: hash,
Name: name,
Item: item,
})
}