1658 lines
49 KiB
Go
1658 lines
49 KiB
Go
package native
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/config"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/dao"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/interop"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/state"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/storage"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
|
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
|
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
|
)
|
|
|
|
// Opus represents the AI workforce integration native contract.
|
|
type Opus struct {
|
|
interop.ContractMD
|
|
NEO INEO
|
|
Vita IVita
|
|
VTS IVTS
|
|
RoleRegistry IRoleRegistry
|
|
Lex ILex
|
|
Treasury ITreasury
|
|
}
|
|
|
|
// OpusCache represents the cached state for Opus contract.
|
|
type OpusCache struct {
|
|
workerCount uint64
|
|
taskCount uint64
|
|
capabilityCount uint64
|
|
operatorCount uint64
|
|
}
|
|
|
|
// Storage key prefixes for Opus.
|
|
const (
|
|
opusPrefixWorker byte = 0x01 // workerID -> AIWorker
|
|
opusPrefixWorkerByOperator byte = 0x02 // vitaID + workerID -> exists
|
|
opusPrefixWorkerByType byte = 0x03 // workerType + workerID -> exists
|
|
opusPrefixOperator byte = 0x10 // vitaID -> OperatorProfile
|
|
opusPrefixOperatorByOwner byte = 0x11 // owner -> vitaID
|
|
opusPrefixTask byte = 0x20 // taskID -> AITask
|
|
opusPrefixTaskByWorker byte = 0x21 // workerID + taskID -> exists
|
|
opusPrefixTaskByRequester byte = 0x22 // vitaID + taskID -> exists
|
|
opusPrefixActiveTask byte = 0x23 // workerID -> active taskID
|
|
opusPrefixCapability byte = 0x30 // capID -> AICapability
|
|
opusPrefixCapByWorker byte = 0x31 // workerID + capID -> exists
|
|
opusPrefixCapByCategory byte = 0x32 // category + capID -> exists
|
|
opusPrefixWorkerCounter byte = 0xF0 // -> uint64
|
|
opusPrefixTaskCounter byte = 0xF1 // -> next task ID
|
|
opusPrefixCapCounter byte = 0xF2 // -> next capability ID
|
|
opusPrefixOperatorCounter byte = 0xF3 // -> next operator count
|
|
opusPrefixTotalTribute byte = 0xF8 // -> total AI tribute collected
|
|
opusPrefixConfig byte = 0xFF // -> OpusConfig
|
|
)
|
|
|
|
// Event names for Opus.
|
|
const (
|
|
AIWorkerRegisteredEvent = "AIWorkerRegistered"
|
|
AIWorkerUpdatedEvent = "AIWorkerUpdated"
|
|
AIWorkerSuspendedEvent = "AIWorkerSuspended"
|
|
AIWorkerDecommissionedEvent = "AIWorkerDecommissioned"
|
|
TaskAssignedEvent = "TaskAssigned"
|
|
TaskCompletedEvent = "TaskCompleted"
|
|
TaskFailedEvent = "TaskFailed"
|
|
TaskCancelledEvent = "TaskCancelled"
|
|
TributePaidEvent = "TributePaid"
|
|
CapabilityAddedEvent = "CapabilityAdded"
|
|
CapabilityVerifiedEvent = "CapabilityVerified"
|
|
OperatorRegisteredEvent = "OperatorRegistered"
|
|
OperatorVerifiedEvent = "OperatorVerified"
|
|
)
|
|
|
|
// Role constants for AI supervision.
|
|
const (
|
|
RoleOpusSupervisor uint64 = 24 // Can manage AI workers and verify capabilities
|
|
)
|
|
|
|
// Various errors for Opus.
|
|
var (
|
|
ErrOpusWorkerNotFound = errors.New("AI worker not found")
|
|
ErrOpusWorkerExists = errors.New("AI worker already exists")
|
|
ErrOpusWorkerSuspended = errors.New("AI worker is suspended")
|
|
ErrOpusWorkerDecommissioned = errors.New("AI worker is decommissioned")
|
|
ErrOpusOperatorNotFound = errors.New("operator profile not found")
|
|
ErrOpusOperatorExists = errors.New("operator profile already exists")
|
|
ErrOpusNoVita = errors.New("operator must have an active Vita")
|
|
ErrOpusTaskNotFound = errors.New("task not found")
|
|
ErrOpusTaskNotAssigned = errors.New("task is not in assigned state")
|
|
ErrOpusTaskNotInProgress = errors.New("task is not in progress")
|
|
ErrOpusTaskAlreadyComplete = errors.New("task already completed")
|
|
ErrOpusCapabilityNotFound = errors.New("capability not found")
|
|
ErrOpusCapabilityExists = errors.New("capability already exists")
|
|
ErrOpusNotCommittee = errors.New("invalid committee signature")
|
|
ErrOpusNotOperator = errors.New("caller is not the operator")
|
|
ErrOpusNotSupervisor = errors.New("caller is not an authorized supervisor")
|
|
ErrOpusInvalidName = errors.New("invalid name")
|
|
ErrOpusInvalidDescription = errors.New("invalid description")
|
|
ErrOpusInvalidTributeRate = errors.New("invalid tribute rate")
|
|
ErrOpusInvalidAmount = errors.New("invalid amount")
|
|
ErrOpusMaxWorkersExceeded = errors.New("maximum workers per operator exceeded")
|
|
ErrOpusInsufficientFee = errors.New("insufficient registration fee")
|
|
ErrOpusLaborRestricted = errors.New("labor right is restricted")
|
|
)
|
|
|
|
var (
|
|
_ interop.Contract = (*Opus)(nil)
|
|
_ dao.NativeContractCache = (*OpusCache)(nil)
|
|
)
|
|
|
|
// Copy implements NativeContractCache interface.
|
|
func (c *OpusCache) Copy() dao.NativeContractCache {
|
|
return &OpusCache{
|
|
workerCount: c.workerCount,
|
|
taskCount: c.taskCount,
|
|
capabilityCount: c.capabilityCount,
|
|
operatorCount: c.operatorCount,
|
|
}
|
|
}
|
|
|
|
// checkCommittee checks if the caller has committee authority.
|
|
func (o *Opus) checkCommittee(ic *interop.Context) bool {
|
|
if o.RoleRegistry != nil {
|
|
return o.RoleRegistry.CheckCommittee(ic)
|
|
}
|
|
return o.NEO.CheckCommittee(ic)
|
|
}
|
|
|
|
// checkOpusSupervisor checks if the caller has opus supervisor authority.
|
|
func (o *Opus) checkOpusSupervisor(ic *interop.Context) bool {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if o.RoleRegistry != nil {
|
|
if o.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleOpusSupervisor, ic.Block.Index) {
|
|
return true
|
|
}
|
|
}
|
|
// Committee members can also act as supervisors
|
|
return o.checkCommittee(ic)
|
|
}
|
|
|
|
// checkLaborRight checks if subject has labor rights via Lex.
|
|
func (o *Opus) checkLaborRight(ic *interop.Context, subject util.Uint160) bool {
|
|
if o.Lex == nil {
|
|
return true // Allow if Lex not available
|
|
}
|
|
return o.Lex.HasRightInternal(ic.DAO, subject, state.RightLabor, ic.Block.Index)
|
|
}
|
|
|
|
// newOpus creates a new Opus native contract.
|
|
func newOpus() *Opus {
|
|
o := &Opus{
|
|
ContractMD: *interop.NewContractMD(nativenames.Opus, nativeids.Opus),
|
|
}
|
|
defer o.BuildHFSpecificMD(o.ActiveIn())
|
|
|
|
// ===== Operator Management =====
|
|
|
|
// registerOperator - Register as an AI operator (requires Vita)
|
|
desc := NewDescriptor("registerOperator", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md := NewMethodAndPrice(o.registerOperator, 1<<17, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getOperator - Get operator profile by owner
|
|
desc = NewDescriptor("getOperator", smartcontract.ArrayType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(o.getOperator, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// verifyOperator - Verify an operator (supervisor only)
|
|
desc = NewDescriptor("verifyOperator", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(o.verifyOperator, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// ===== Worker Registration =====
|
|
|
|
// registerWorker - Register a new AI worker
|
|
desc = NewDescriptor("registerWorker", smartcontract.IntegerType,
|
|
manifest.NewParameter("name", smartcontract.StringType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("workerType", smartcontract.IntegerType),
|
|
manifest.NewParameter("modelHash", smartcontract.Hash256Type))
|
|
md = NewMethodAndPrice(o.registerWorker, 1<<17, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getWorker - Get AI worker by ID
|
|
desc = NewDescriptor("getWorker", smartcontract.ArrayType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.getWorker, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getWorkersByOperator - Get worker count by operator
|
|
desc = NewDescriptor("getWorkerCountByOperator", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(o.getWorkerCountByOperator, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// updateWorker - Update AI worker details
|
|
desc = NewDescriptor("updateWorker", smartcontract.BoolType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("description", smartcontract.StringType))
|
|
md = NewMethodAndPrice(o.updateWorker, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// suspendWorker - Suspend an AI worker (supervisor only)
|
|
desc = NewDescriptor("suspendWorker", smartcontract.BoolType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(o.suspendWorker, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// reactivateWorker - Reactivate a suspended worker (supervisor only)
|
|
desc = NewDescriptor("reactivateWorker", smartcontract.BoolType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.reactivateWorker, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// decommissionWorker - Permanently decommission an AI worker
|
|
desc = NewDescriptor("decommissionWorker", smartcontract.BoolType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(o.decommissionWorker, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// ===== Task Management =====
|
|
|
|
// assignTask - Assign a task to an AI worker
|
|
desc = NewDescriptor("assignTask", smartcontract.IntegerType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("taskType", smartcontract.StringType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("valueOffered", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.assignTask, 1<<17, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// startTask - Mark task as in progress (worker operator only)
|
|
desc = NewDescriptor("startTask", smartcontract.BoolType,
|
|
manifest.NewParameter("taskID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.startTask, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// completeTask - Complete a task with result
|
|
desc = NewDescriptor("completeTask", smartcontract.BoolType,
|
|
manifest.NewParameter("taskID", smartcontract.IntegerType),
|
|
manifest.NewParameter("resultHash", smartcontract.Hash256Type),
|
|
manifest.NewParameter("valueCompleted", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.completeTask, 1<<17, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// failTask - Mark task as failed
|
|
desc = NewDescriptor("failTask", smartcontract.BoolType,
|
|
manifest.NewParameter("taskID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(o.failTask, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// cancelTask - Cancel a task (requester or supervisor only)
|
|
desc = NewDescriptor("cancelTask", smartcontract.BoolType,
|
|
manifest.NewParameter("taskID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(o.cancelTask, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getTask - Get task by ID
|
|
desc = NewDescriptor("getTask", smartcontract.ArrayType,
|
|
manifest.NewParameter("taskID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.getTask, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getActiveTask - Get worker's active task
|
|
desc = NewDescriptor("getActiveTask", smartcontract.ArrayType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.getActiveTask, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// ===== Capability Management =====
|
|
|
|
// addCapability - Add capability to an AI worker (operator only)
|
|
desc = NewDescriptor("addCapability", smartcontract.IntegerType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("name", smartcontract.StringType),
|
|
manifest.NewParameter("category", smartcontract.StringType),
|
|
manifest.NewParameter("level", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.addCapability, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// verifyCapability - Verify a capability (supervisor only)
|
|
desc = NewDescriptor("verifyCapability", smartcontract.BoolType,
|
|
manifest.NewParameter("capabilityID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.verifyCapability, 1<<16, callflag.States|callflag.AllowNotify)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getCapability - Get capability by ID
|
|
desc = NewDescriptor("getCapability", smartcontract.ArrayType,
|
|
manifest.NewParameter("capabilityID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.getCapability, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// hasCapability - Check if worker has capability
|
|
desc = NewDescriptor("hasCapability", smartcontract.BoolType,
|
|
manifest.NewParameter("workerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("name", smartcontract.StringType))
|
|
md = NewMethodAndPrice(o.hasCapability, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// ===== Configuration & Stats =====
|
|
|
|
// getConfig - Get contract configuration
|
|
desc = NewDescriptor("getConfig", smartcontract.ArrayType)
|
|
md = NewMethodAndPrice(o.getConfig, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// setConfig - Set contract configuration (committee only)
|
|
desc = NewDescriptor("setConfig", smartcontract.BoolType,
|
|
manifest.NewParameter("minTributeRate", smartcontract.IntegerType),
|
|
manifest.NewParameter("maxTributeRate", smartcontract.IntegerType),
|
|
manifest.NewParameter("defaultTributeRate", smartcontract.IntegerType),
|
|
manifest.NewParameter("registrationFee", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(o.setConfig, 1<<16, callflag.States)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getTotalWorkers - Get total registered workers
|
|
desc = NewDescriptor("getTotalWorkers", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(o.getTotalWorkers, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getTotalTasks - Get total tasks
|
|
desc = NewDescriptor("getTotalTasks", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(o.getTotalTasks, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getTotalOperators - Get total operators
|
|
desc = NewDescriptor("getTotalOperators", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(o.getTotalOperators, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getTotalCapabilities - Get total capabilities
|
|
desc = NewDescriptor("getTotalCapabilities", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(o.getTotalCapabilities, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
// getTotalTribute - Get total AI tribute collected
|
|
desc = NewDescriptor("getTotalTribute", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(o.getTotalTribute, 1<<15, callflag.ReadStates)
|
|
o.AddMethod(md, desc)
|
|
|
|
return o
|
|
}
|
|
|
|
// Metadata returns contract metadata.
|
|
func (o *Opus) Metadata() *interop.ContractMD {
|
|
return &o.ContractMD
|
|
}
|
|
|
|
// Initialize initializes Opus contract.
|
|
func (o *Opus) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
|
|
if hf != o.ActiveIn() {
|
|
return nil
|
|
}
|
|
|
|
// Initialize default configuration
|
|
cfg := state.OpusConfig{
|
|
MinTributeRate: 1000, // 10%
|
|
MaxTributeRate: 7000, // 70%
|
|
DefaultTributeRate: 5000, // 50%
|
|
RegistrationFee: 10000, // 100 VTS (assuming 2 decimal places)
|
|
TaskCompletionMinimum: 100, // 1 VTS
|
|
VerificationRequired: false,
|
|
MaxWorkersPerOperator: 0, // Unlimited
|
|
}
|
|
|
|
return o.setConfigInternal(ic.DAO, &cfg)
|
|
}
|
|
|
|
// ActiveIn returns the hardfork when contract becomes active.
|
|
func (o *Opus) ActiveIn() *config.Hardfork {
|
|
return nil // Always active
|
|
}
|
|
|
|
// InitializeCache initializes contract cache.
|
|
func (o *Opus) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
|
|
cache := &OpusCache{}
|
|
|
|
// Load worker count
|
|
workerCountKey := []byte{opusPrefixWorkerCounter}
|
|
if si := d.GetStorageItem(o.ID, workerCountKey); si != nil {
|
|
cache.workerCount = binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
// Load task count
|
|
taskCountKey := []byte{opusPrefixTaskCounter}
|
|
if si := d.GetStorageItem(o.ID, taskCountKey); si != nil {
|
|
cache.taskCount = binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
// Load capability count
|
|
capCountKey := []byte{opusPrefixCapCounter}
|
|
if si := d.GetStorageItem(o.ID, capCountKey); si != nil {
|
|
cache.capabilityCount = binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
// Load operator count
|
|
opCountKey := []byte{opusPrefixOperatorCounter}
|
|
if si := d.GetStorageItem(o.ID, opCountKey); si != nil {
|
|
cache.operatorCount = binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
d.SetCache(o.ID, cache)
|
|
return nil
|
|
}
|
|
|
|
// OnPersist implements Contract interface.
|
|
func (o *Opus) OnPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// PostPersist implements Contract interface.
|
|
func (o *Opus) PostPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// ===== Storage Helpers =====
|
|
|
|
func (o *Opus) getConfigInternal(d *dao.Simple) *state.OpusConfig {
|
|
key := []byte{opusPrefixConfig}
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return &state.OpusConfig{
|
|
MinTributeRate: 1000,
|
|
MaxTributeRate: 7000,
|
|
DefaultTributeRate: 5000,
|
|
RegistrationFee: 10000,
|
|
TaskCompletionMinimum: 100,
|
|
VerificationRequired: false,
|
|
MaxWorkersPerOperator: 0,
|
|
}
|
|
}
|
|
cfg := new(state.OpusConfig)
|
|
item, _ := stackitem.Deserialize(si)
|
|
if err := cfg.FromStackItem(item); err != nil {
|
|
return nil
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func (o *Opus) setConfigInternal(d *dao.Simple, cfg *state.OpusConfig) error {
|
|
key := []byte{opusPrefixConfig}
|
|
item, _ := cfg.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
return nil
|
|
}
|
|
|
|
// Counter helper functions
|
|
func (o *Opus) getWorkerCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(o.ID, []byte{opusPrefixWorkerCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
func (o *Opus) getOperatorCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(o.ID, []byte{opusPrefixOperatorCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
func (o *Opus) getTaskCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(o.ID, []byte{opusPrefixTaskCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
func (o *Opus) getCapabilityCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(o.ID, []byte{opusPrefixCapCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint64(si)
|
|
}
|
|
|
|
func (o *Opus) getWorkerInternal(d *dao.Simple, workerID uint64) *state.AIWorker {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixWorker
|
|
binary.LittleEndian.PutUint64(key[1:], workerID)
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
worker := new(state.AIWorker)
|
|
item, _ := stackitem.Deserialize(si)
|
|
if err := worker.FromStackItem(item); err != nil {
|
|
return nil
|
|
}
|
|
return worker
|
|
}
|
|
|
|
func (o *Opus) putWorkerInternal(d *dao.Simple, worker *state.AIWorker) error {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixWorker
|
|
binary.LittleEndian.PutUint64(key[1:], worker.ID)
|
|
item, _ := worker.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
return nil
|
|
}
|
|
|
|
func (o *Opus) getTaskInternal(d *dao.Simple, taskID uint64) *state.AITask {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixTask
|
|
binary.LittleEndian.PutUint64(key[1:], taskID)
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
task := new(state.AITask)
|
|
item, _ := stackitem.Deserialize(si)
|
|
if err := task.FromStackItem(item); err != nil {
|
|
return nil
|
|
}
|
|
return task
|
|
}
|
|
|
|
func (o *Opus) putTaskInternal(d *dao.Simple, task *state.AITask) error {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixTask
|
|
binary.LittleEndian.PutUint64(key[1:], task.ID)
|
|
item, _ := task.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
return nil
|
|
}
|
|
|
|
func (o *Opus) getCapabilityInternal(d *dao.Simple, capID uint64) *state.AICapability {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixCapability
|
|
binary.LittleEndian.PutUint64(key[1:], capID)
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
cap := new(state.AICapability)
|
|
item, _ := stackitem.Deserialize(si)
|
|
if err := cap.FromStackItem(item); err != nil {
|
|
return nil
|
|
}
|
|
return cap
|
|
}
|
|
|
|
func (o *Opus) putCapabilityInternal(d *dao.Simple, cap *state.AICapability) error {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixCapability
|
|
binary.LittleEndian.PutUint64(key[1:], cap.ID)
|
|
item, _ := cap.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
return nil
|
|
}
|
|
|
|
func (o *Opus) getOperatorInternal(d *dao.Simple, vitaID uint64) *state.OperatorProfile {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixOperator
|
|
binary.LittleEndian.PutUint64(key[1:], vitaID)
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
profile := new(state.OperatorProfile)
|
|
item, _ := stackitem.Deserialize(si)
|
|
if err := profile.FromStackItem(item); err != nil {
|
|
return nil
|
|
}
|
|
return profile
|
|
}
|
|
|
|
func (o *Opus) getOperatorByOwner(d *dao.Simple, owner util.Uint160) *state.OperatorProfile {
|
|
key := append([]byte{opusPrefixOperatorByOwner}, owner.BytesBE()...)
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
vitaID := binary.LittleEndian.Uint64(si)
|
|
return o.getOperatorInternal(d, vitaID)
|
|
}
|
|
|
|
func (o *Opus) putOperatorInternal(d *dao.Simple, profile *state.OperatorProfile) error {
|
|
// Store by VitaID
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixOperator
|
|
binary.LittleEndian.PutUint64(key[1:], profile.VitaID)
|
|
item, _ := profile.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
|
|
// Store owner -> vitaID mapping
|
|
ownerKey := append([]byte{opusPrefixOperatorByOwner}, profile.Owner.BytesBE()...)
|
|
vitaIDBytes := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(vitaIDBytes, profile.VitaID)
|
|
d.PutStorageItem(o.ID, ownerKey, vitaIDBytes)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *Opus) getNextWorkerID(d *dao.Simple) uint64 {
|
|
id := o.getWorkerCounter(d) + 1
|
|
o.setWorkerCounter(d, id)
|
|
return id
|
|
}
|
|
|
|
func (o *Opus) setWorkerCounter(d *dao.Simple, count uint64) {
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data, count)
|
|
d.PutStorageItem(o.ID, []byte{opusPrefixWorkerCounter}, data)
|
|
}
|
|
|
|
func (o *Opus) getNextTaskID(d *dao.Simple) uint64 {
|
|
id := o.getTaskCounter(d) + 1
|
|
o.setTaskCounter(d, id)
|
|
return id
|
|
}
|
|
|
|
func (o *Opus) setTaskCounter(d *dao.Simple, count uint64) {
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data, count)
|
|
d.PutStorageItem(o.ID, []byte{opusPrefixTaskCounter}, data)
|
|
}
|
|
|
|
func (o *Opus) getNextCapabilityID(d *dao.Simple) uint64 {
|
|
id := o.getCapabilityCounter(d) + 1
|
|
o.setCapabilityCounter(d, id)
|
|
return id
|
|
}
|
|
|
|
func (o *Opus) setCapabilityCounter(d *dao.Simple, count uint64) {
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data, count)
|
|
d.PutStorageItem(o.ID, []byte{opusPrefixCapCounter}, data)
|
|
}
|
|
|
|
func (o *Opus) incrementOperatorCount(d *dao.Simple) uint64 {
|
|
count := o.getOperatorCounter(d) + 1
|
|
o.setOperatorCounter(d, count)
|
|
return count
|
|
}
|
|
|
|
func (o *Opus) setOperatorCounter(d *dao.Simple, count uint64) {
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data, count)
|
|
d.PutStorageItem(o.ID, []byte{opusPrefixOperatorCounter}, data)
|
|
}
|
|
|
|
func (o *Opus) addWorkerOperatorIndex(d *dao.Simple, vitaID, workerID uint64) {
|
|
key := make([]byte, 17)
|
|
key[0] = opusPrefixWorkerByOperator
|
|
binary.LittleEndian.PutUint64(key[1:], vitaID)
|
|
binary.LittleEndian.PutUint64(key[9:], workerID)
|
|
d.PutStorageItem(o.ID, key, []byte{1})
|
|
}
|
|
|
|
func (o *Opus) getWorkerCountByOperatorInternal(d *dao.Simple, vitaID uint64) uint64 {
|
|
prefix := make([]byte, 9)
|
|
prefix[0] = opusPrefixWorkerByOperator
|
|
binary.LittleEndian.PutUint64(prefix[1:], vitaID)
|
|
|
|
var count uint64
|
|
d.Seek(o.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
count++
|
|
return true
|
|
})
|
|
return count
|
|
}
|
|
|
|
func (o *Opus) addTaskWorkerIndex(d *dao.Simple, workerID, taskID uint64) {
|
|
key := make([]byte, 17)
|
|
key[0] = opusPrefixTaskByWorker
|
|
binary.LittleEndian.PutUint64(key[1:], workerID)
|
|
binary.LittleEndian.PutUint64(key[9:], taskID)
|
|
d.PutStorageItem(o.ID, key, []byte{1})
|
|
}
|
|
|
|
func (o *Opus) setActiveTask(d *dao.Simple, workerID, taskID uint64) {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixActiveTask
|
|
binary.LittleEndian.PutUint64(key[1:], workerID)
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data, taskID)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
}
|
|
|
|
func (o *Opus) clearActiveTask(d *dao.Simple, workerID uint64) {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixActiveTask
|
|
binary.LittleEndian.PutUint64(key[1:], workerID)
|
|
d.DeleteStorageItem(o.ID, key)
|
|
}
|
|
|
|
func (o *Opus) getActiveTaskID(d *dao.Simple, workerID uint64) (uint64, bool) {
|
|
key := make([]byte, 9)
|
|
key[0] = opusPrefixActiveTask
|
|
binary.LittleEndian.PutUint64(key[1:], workerID)
|
|
si := d.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return 0, false
|
|
}
|
|
return binary.LittleEndian.Uint64(si), true
|
|
}
|
|
|
|
func (o *Opus) addCapabilityWorkerIndex(d *dao.Simple, workerID, capID uint64) {
|
|
key := make([]byte, 17)
|
|
key[0] = opusPrefixCapByWorker
|
|
binary.LittleEndian.PutUint64(key[1:], workerID)
|
|
binary.LittleEndian.PutUint64(key[9:], capID)
|
|
d.PutStorageItem(o.ID, key, []byte{1})
|
|
}
|
|
|
|
func (o *Opus) addTotalTribute(d *dao.Simple, amount uint64) {
|
|
key := []byte{opusPrefixTotalTribute}
|
|
var current uint64
|
|
if si := d.GetStorageItem(o.ID, key); si != nil {
|
|
current = binary.LittleEndian.Uint64(si)
|
|
}
|
|
current += amount
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data, current)
|
|
d.PutStorageItem(o.ID, key, data)
|
|
}
|
|
|
|
// ===== Contract Methods =====
|
|
|
|
// registerOperator registers an AI operator (requires Vita).
|
|
func (o *Opus) registerOperator(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
// Check Vita ownership
|
|
vita, err := o.Vita.GetTokenByOwner(ic.DAO, owner)
|
|
if err != nil || vita == nil {
|
|
panic(ErrOpusNoVita)
|
|
}
|
|
|
|
// Check if already registered
|
|
if o.getOperatorByOwner(ic.DAO, owner) != nil {
|
|
panic(ErrOpusOperatorExists)
|
|
}
|
|
|
|
// Check labor rights
|
|
if !o.checkLaborRight(ic, owner) {
|
|
panic(ErrOpusLaborRestricted)
|
|
}
|
|
|
|
profile := &state.OperatorProfile{
|
|
VitaID: vita.TokenID,
|
|
Owner: owner,
|
|
TotalWorkersOwned: 0,
|
|
TotalTributePaid: 0,
|
|
TotalValueGenerated: 0,
|
|
ReputationScore: 5000, // Start at 50%
|
|
IsVerified: false,
|
|
CreatedAt: ic.Block.Index,
|
|
UpdatedAt: ic.Block.Index,
|
|
}
|
|
|
|
if err := o.putOperatorInternal(ic.DAO, profile); err != nil {
|
|
panic(err)
|
|
}
|
|
o.incrementOperatorCount(ic.DAO)
|
|
|
|
ic.AddNotification(o.Hash, OperatorRegisteredEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))),
|
|
stackitem.NewByteArray(owner.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getOperator returns operator profile by owner.
|
|
func (o *Opus) getOperator(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
profile := o.getOperatorByOwner(ic.DAO, owner)
|
|
if profile == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, _ := profile.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// verifyOperator verifies an operator (supervisor only).
|
|
func (o *Opus) verifyOperator(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !o.checkOpusSupervisor(ic) {
|
|
panic(ErrOpusNotSupervisor)
|
|
}
|
|
|
|
owner := toUint160(args[0])
|
|
|
|
profile := o.getOperatorByOwner(ic.DAO, owner)
|
|
if profile == nil {
|
|
panic(ErrOpusOperatorNotFound)
|
|
}
|
|
|
|
profile.IsVerified = true
|
|
profile.UpdatedAt = ic.Block.Index
|
|
|
|
if err := o.putOperatorInternal(ic.DAO, profile); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ic.AddNotification(o.Hash, OperatorVerifiedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(profile.VitaID))),
|
|
stackitem.NewByteArray(owner.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// registerWorker registers a new AI worker.
|
|
func (o *Opus) registerWorker(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
name, err := stackitem.ToString(args[0])
|
|
if err != nil || len(name) == 0 || len(name) > 64 {
|
|
panic(ErrOpusInvalidName)
|
|
}
|
|
|
|
description, err := stackitem.ToString(args[1])
|
|
if err != nil || len(description) > 256 {
|
|
panic(ErrOpusInvalidDescription)
|
|
}
|
|
|
|
workerType, err := args[2].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
modelHash, err := getUint256FromItem(args[3])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Get operator profile
|
|
profile := o.getOperatorByOwner(ic.DAO, caller)
|
|
if profile == nil {
|
|
panic(ErrOpusOperatorNotFound)
|
|
}
|
|
|
|
cfg := o.getConfigInternal(ic.DAO)
|
|
|
|
// Check max workers limit
|
|
if cfg.MaxWorkersPerOperator > 0 {
|
|
currentCount := o.getWorkerCountByOperatorInternal(ic.DAO, profile.VitaID)
|
|
if currentCount >= cfg.MaxWorkersPerOperator {
|
|
panic(ErrOpusMaxWorkersExceeded)
|
|
}
|
|
}
|
|
|
|
workerID := o.getNextWorkerID(ic.DAO)
|
|
|
|
worker := &state.AIWorker{
|
|
ID: workerID,
|
|
Name: name,
|
|
Description: description,
|
|
OperatorVitaID: profile.VitaID,
|
|
Operator: caller,
|
|
WorkerType: state.WorkerType(workerType.Uint64()),
|
|
ModelHash: modelHash,
|
|
TributeRate: cfg.DefaultTributeRate,
|
|
TotalTasksAssigned: 0,
|
|
TotalTasksCompleted: 0,
|
|
TotalValueGenerated: 0,
|
|
TotalTributePaid: 0,
|
|
Status: state.AIWorkerActive,
|
|
RegisteredAt: ic.Block.Index,
|
|
UpdatedAt: ic.Block.Index,
|
|
}
|
|
|
|
if err := o.putWorkerInternal(ic.DAO, worker); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Add indices
|
|
o.addWorkerOperatorIndex(ic.DAO, profile.VitaID, workerID)
|
|
|
|
// Update operator's worker count
|
|
profile.TotalWorkersOwned++
|
|
profile.UpdatedAt = ic.Block.Index
|
|
o.putOperatorInternal(ic.DAO, profile)
|
|
|
|
ic.AddNotification(o.Hash, AIWorkerRegisteredEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(workerID))),
|
|
stackitem.NewByteArray([]byte(name)),
|
|
stackitem.NewBigInteger(big.NewInt(int64(profile.VitaID))),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(workerID)))
|
|
}
|
|
|
|
// getWorker returns AI worker by ID.
|
|
func (o *Opus) getWorker(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, _ := worker.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getWorkerCountByOperator returns worker count by operator.
|
|
func (o *Opus) getWorkerCountByOperator(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
profile := o.getOperatorByOwner(ic.DAO, owner)
|
|
if profile == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
count := o.getWorkerCountByOperatorInternal(ic.DAO, profile.VitaID)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(count)))
|
|
}
|
|
|
|
// updateWorker updates AI worker details.
|
|
func (o *Opus) updateWorker(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
description, err := stackitem.ToString(args[1])
|
|
if err != nil || len(description) > 256 {
|
|
panic(ErrOpusInvalidDescription)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !worker.Operator.Equals(caller) {
|
|
panic(ErrOpusNotOperator)
|
|
}
|
|
|
|
worker.Description = description
|
|
worker.UpdatedAt = ic.Block.Index
|
|
|
|
if err := o.putWorkerInternal(ic.DAO, worker); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ic.AddNotification(o.Hash, AIWorkerUpdatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// suspendWorker suspends an AI worker (supervisor only).
|
|
func (o *Opus) suspendWorker(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !o.checkOpusSupervisor(ic) {
|
|
panic(ErrOpusNotSupervisor)
|
|
}
|
|
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
reason, err := stackitem.ToString(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
if worker.Status == state.AIWorkerDecommissioned {
|
|
panic(ErrOpusWorkerDecommissioned)
|
|
}
|
|
|
|
worker.Status = state.AIWorkerSuspended
|
|
worker.UpdatedAt = ic.Block.Index
|
|
|
|
if err := o.putWorkerInternal(ic.DAO, worker); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ic.AddNotification(o.Hash, AIWorkerSuspendedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// reactivateWorker reactivates a suspended worker (supervisor only).
|
|
func (o *Opus) reactivateWorker(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !o.checkOpusSupervisor(ic) {
|
|
panic(ErrOpusNotSupervisor)
|
|
}
|
|
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
if worker.Status == state.AIWorkerDecommissioned {
|
|
panic(ErrOpusWorkerDecommissioned)
|
|
}
|
|
|
|
if worker.Status != state.AIWorkerSuspended {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
worker.Status = state.AIWorkerActive
|
|
worker.UpdatedAt = ic.Block.Index
|
|
|
|
if err := o.putWorkerInternal(ic.DAO, worker); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ic.AddNotification(o.Hash, AIWorkerUpdatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// decommissionWorker permanently decommissions an AI worker.
|
|
func (o *Opus) decommissionWorker(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
reason, err := stackitem.ToString(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
// Operator or supervisor can decommission
|
|
if !worker.Operator.Equals(caller) && !o.checkOpusSupervisor(ic) {
|
|
panic(ErrOpusNotOperator)
|
|
}
|
|
|
|
worker.Status = state.AIWorkerDecommissioned
|
|
worker.UpdatedAt = ic.Block.Index
|
|
|
|
if err := o.putWorkerInternal(ic.DAO, worker); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ic.AddNotification(o.Hash, AIWorkerDecommissionedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// assignTask assigns a task to an AI worker.
|
|
func (o *Opus) assignTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
taskType, err := stackitem.ToString(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
description, err := stackitem.ToString(args[2])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
valueOffered, err := args[3].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
if worker.Status != state.AIWorkerActive {
|
|
panic(ErrOpusWorkerSuspended)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Get requester's Vita ID (0 if no Vita)
|
|
var requesterVitaID uint64
|
|
if vita, _ := o.Vita.GetTokenByOwner(ic.DAO, caller); vita != nil {
|
|
requesterVitaID = vita.TokenID
|
|
}
|
|
|
|
taskID := o.getNextTaskID(ic.DAO)
|
|
|
|
task := &state.AITask{
|
|
ID: taskID,
|
|
WorkerID: worker.ID,
|
|
RequesterVitaID: requesterVitaID,
|
|
Requester: caller,
|
|
TaskType: taskType,
|
|
Description: description,
|
|
ValueOffered: valueOffered.Uint64(),
|
|
ValueCompleted: 0,
|
|
TributePaid: 0,
|
|
ResultHash: util.Uint256{},
|
|
Status: state.TaskAssigned,
|
|
AssignedAt: ic.Block.Index,
|
|
CompletedAt: 0,
|
|
}
|
|
|
|
if err := o.putTaskInternal(ic.DAO, task); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Update worker stats
|
|
worker.TotalTasksAssigned++
|
|
worker.UpdatedAt = ic.Block.Index
|
|
o.putWorkerInternal(ic.DAO, worker)
|
|
|
|
// Add indices
|
|
o.addTaskWorkerIndex(ic.DAO, worker.ID, taskID)
|
|
|
|
ic.AddNotification(o.Hash, TaskAssignedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(taskID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(taskID)))
|
|
}
|
|
|
|
// startTask marks task as in progress.
|
|
func (o *Opus) startTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
taskID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
task := o.getTaskInternal(ic.DAO, taskID.Uint64())
|
|
if task == nil {
|
|
panic(ErrOpusTaskNotFound)
|
|
}
|
|
|
|
if task.Status != state.TaskAssigned {
|
|
panic(ErrOpusTaskNotAssigned)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, task.WorkerID)
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !worker.Operator.Equals(caller) {
|
|
panic(ErrOpusNotOperator)
|
|
}
|
|
|
|
task.Status = state.TaskInProgress
|
|
o.putTaskInternal(ic.DAO, task)
|
|
|
|
// Set as active task
|
|
o.setActiveTask(ic.DAO, worker.ID, task.ID)
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// completeTask completes a task with result.
|
|
func (o *Opus) completeTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
taskID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
resultHash, err := getUint256FromItem(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
valueCompleted, err := args[2].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
task := o.getTaskInternal(ic.DAO, taskID.Uint64())
|
|
if task == nil {
|
|
panic(ErrOpusTaskNotFound)
|
|
}
|
|
|
|
if task.Status != state.TaskInProgress && task.Status != state.TaskAssigned {
|
|
panic(ErrOpusTaskAlreadyComplete)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, task.WorkerID)
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !worker.Operator.Equals(caller) {
|
|
panic(ErrOpusNotOperator)
|
|
}
|
|
|
|
completedValue := valueCompleted.Uint64()
|
|
|
|
// Calculate tribute
|
|
tributeAmount := (completedValue * uint64(worker.TributeRate)) / 10000
|
|
|
|
task.Status = state.TaskCompleted
|
|
task.ResultHash = resultHash
|
|
task.ValueCompleted = completedValue
|
|
task.TributePaid = tributeAmount
|
|
task.CompletedAt = ic.Block.Index
|
|
o.putTaskInternal(ic.DAO, task)
|
|
|
|
// Update worker stats
|
|
worker.TotalTasksCompleted++
|
|
worker.TotalValueGenerated += completedValue
|
|
worker.TotalTributePaid += tributeAmount
|
|
worker.UpdatedAt = ic.Block.Index
|
|
o.putWorkerInternal(ic.DAO, worker)
|
|
|
|
// Update operator stats
|
|
profile := o.getOperatorInternal(ic.DAO, worker.OperatorVitaID)
|
|
if profile != nil {
|
|
profile.TotalValueGenerated += completedValue
|
|
profile.TotalTributePaid += tributeAmount
|
|
profile.UpdatedAt = ic.Block.Index
|
|
o.putOperatorInternal(ic.DAO, profile)
|
|
}
|
|
|
|
// Add to total tribute
|
|
if tributeAmount > 0 {
|
|
o.addTotalTribute(ic.DAO, tributeAmount)
|
|
|
|
ic.AddNotification(o.Hash, TributePaidEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(tributeAmount))),
|
|
}))
|
|
}
|
|
|
|
// Clear active task
|
|
o.clearActiveTask(ic.DAO, worker.ID)
|
|
|
|
ic.AddNotification(o.Hash, TaskCompletedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(task.ID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(completedValue))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(tributeAmount))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// failTask marks task as failed.
|
|
func (o *Opus) failTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
taskID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
reason, err := stackitem.ToString(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
task := o.getTaskInternal(ic.DAO, taskID.Uint64())
|
|
if task == nil {
|
|
panic(ErrOpusTaskNotFound)
|
|
}
|
|
|
|
if task.Status != state.TaskInProgress && task.Status != state.TaskAssigned {
|
|
panic(ErrOpusTaskAlreadyComplete)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, task.WorkerID)
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !worker.Operator.Equals(caller) {
|
|
panic(ErrOpusNotOperator)
|
|
}
|
|
|
|
task.Status = state.TaskFailed
|
|
task.CompletedAt = ic.Block.Index
|
|
o.putTaskInternal(ic.DAO, task)
|
|
|
|
// Clear active task
|
|
o.clearActiveTask(ic.DAO, worker.ID)
|
|
|
|
ic.AddNotification(o.Hash, TaskFailedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(task.ID))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// cancelTask cancels a task (requester or supervisor only).
|
|
func (o *Opus) cancelTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
taskID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
reason, err := stackitem.ToString(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
task := o.getTaskInternal(ic.DAO, taskID.Uint64())
|
|
if task == nil {
|
|
panic(ErrOpusTaskNotFound)
|
|
}
|
|
|
|
if task.Status == state.TaskCompleted || task.Status == state.TaskFailed || task.Status == state.TaskCancelled {
|
|
panic(ErrOpusTaskAlreadyComplete)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !task.Requester.Equals(caller) && !o.checkOpusSupervisor(ic) {
|
|
panic(ErrOpusNotSupervisor)
|
|
}
|
|
|
|
task.Status = state.TaskCancelled
|
|
task.CompletedAt = ic.Block.Index
|
|
o.putTaskInternal(ic.DAO, task)
|
|
|
|
// Clear active task if applicable
|
|
o.clearActiveTask(ic.DAO, task.WorkerID)
|
|
|
|
ic.AddNotification(o.Hash, TaskCancelledEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(task.ID))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getTask returns task by ID.
|
|
func (o *Opus) getTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
taskID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
task := o.getTaskInternal(ic.DAO, taskID.Uint64())
|
|
if task == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, _ := task.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getActiveTask returns worker's active task.
|
|
func (o *Opus) getActiveTask(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
taskID, found := o.getActiveTaskID(ic.DAO, workerID.Uint64())
|
|
if !found {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
task := o.getTaskInternal(ic.DAO, taskID)
|
|
if task == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, _ := task.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// addCapability adds capability to an AI worker (operator only).
|
|
func (o *Opus) addCapability(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
name, err := stackitem.ToString(args[1])
|
|
if err != nil || len(name) == 0 || len(name) > 64 {
|
|
panic(ErrOpusInvalidName)
|
|
}
|
|
|
|
category, err := stackitem.ToString(args[2])
|
|
if err != nil || len(category) > 64 {
|
|
panic(ErrOpusInvalidDescription)
|
|
}
|
|
|
|
level, err := args[3].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
worker := o.getWorkerInternal(ic.DAO, workerID.Uint64())
|
|
if worker == nil {
|
|
panic(ErrOpusWorkerNotFound)
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !worker.Operator.Equals(caller) {
|
|
panic(ErrOpusNotOperator)
|
|
}
|
|
|
|
capID := o.getNextCapabilityID(ic.DAO)
|
|
|
|
cap := &state.AICapability{
|
|
ID: capID,
|
|
WorkerID: worker.ID,
|
|
Name: name,
|
|
Category: category,
|
|
Level: state.CapabilityLevel(level.Uint64()),
|
|
CertifiedBy: caller,
|
|
CertifiedAt: ic.Block.Index,
|
|
ExpiresAt: 0,
|
|
IsVerified: false,
|
|
}
|
|
|
|
if err := o.putCapabilityInternal(ic.DAO, cap); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
o.addCapabilityWorkerIndex(ic.DAO, worker.ID, capID)
|
|
|
|
ic.AddNotification(o.Hash, CapabilityAddedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(capID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(worker.ID))),
|
|
stackitem.NewByteArray([]byte(name)),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(capID)))
|
|
}
|
|
|
|
// verifyCapability verifies a capability (supervisor only).
|
|
func (o *Opus) verifyCapability(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !o.checkOpusSupervisor(ic) {
|
|
panic(ErrOpusNotSupervisor)
|
|
}
|
|
|
|
capID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cap := o.getCapabilityInternal(ic.DAO, capID.Uint64())
|
|
if cap == nil {
|
|
panic(ErrOpusCapabilityNotFound)
|
|
}
|
|
|
|
cap.IsVerified = true
|
|
cap.CertifiedBy = ic.VM.GetCallingScriptHash()
|
|
cap.CertifiedAt = ic.Block.Index
|
|
|
|
if err := o.putCapabilityInternal(ic.DAO, cap); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ic.AddNotification(o.Hash, CapabilityVerifiedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(cap.ID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cap.WorkerID))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getCapability returns capability by ID.
|
|
func (o *Opus) getCapability(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
capID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cap := o.getCapabilityInternal(ic.DAO, capID.Uint64())
|
|
if cap == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, _ := cap.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// hasCapability checks if worker has capability.
|
|
func (o *Opus) hasCapability(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
workerID, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
name, err := stackitem.ToString(args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Search through capabilities by worker
|
|
prefix := make([]byte, 9)
|
|
prefix[0] = opusPrefixCapByWorker
|
|
binary.LittleEndian.PutUint64(prefix[1:], workerID.Uint64())
|
|
|
|
found := false
|
|
ic.DAO.Seek(o.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 16 {
|
|
capID := binary.LittleEndian.Uint64(k[8:])
|
|
cap := o.getCapabilityInternal(ic.DAO, capID)
|
|
if cap != nil && cap.Name == name {
|
|
found = true
|
|
return false // Stop iteration
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return stackitem.NewBool(found)
|
|
}
|
|
|
|
// getConfig returns contract configuration.
|
|
func (o *Opus) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cfg := o.getConfigInternal(ic.DAO)
|
|
if cfg == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, _ := cfg.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// setConfig sets contract configuration (committee only).
|
|
func (o *Opus) setConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !o.checkCommittee(ic) {
|
|
panic(ErrOpusNotCommittee)
|
|
}
|
|
|
|
minRate, err := args[0].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
maxRate, err := args[1].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defaultRate, err := args[2].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
regFee, err := args[3].TryInteger()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cfg := o.getConfigInternal(ic.DAO)
|
|
cfg.MinTributeRate = uint32(minRate.Uint64())
|
|
cfg.MaxTributeRate = uint32(maxRate.Uint64())
|
|
cfg.DefaultTributeRate = uint32(defaultRate.Uint64())
|
|
cfg.RegistrationFee = regFee.Uint64()
|
|
|
|
return stackitem.NewBool(o.setConfigInternal(ic.DAO, cfg) == nil)
|
|
}
|
|
|
|
// getTotalWorkers returns total registered workers.
|
|
func (o *Opus) getTotalWorkers(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(o.getWorkerCounter(ic.DAO)))
|
|
}
|
|
|
|
// getTotalTasks returns total tasks.
|
|
func (o *Opus) getTotalTasks(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(o.getTaskCounter(ic.DAO)))
|
|
}
|
|
|
|
// getTotalOperators returns total operators.
|
|
func (o *Opus) getTotalOperators(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(o.getOperatorCounter(ic.DAO)))
|
|
}
|
|
|
|
// getTotalCapabilities returns total capabilities.
|
|
func (o *Opus) getTotalCapabilities(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(o.getCapabilityCounter(ic.DAO)))
|
|
}
|
|
|
|
// getTotalTribute returns total AI tribute collected.
|
|
func (o *Opus) getTotalTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
key := []byte{opusPrefixTotalTribute}
|
|
si := ic.DAO.GetStorageItem(o.ID, key)
|
|
if si == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
return stackitem.NewBigInteger(big.NewInt(int64(binary.LittleEndian.Uint64(si))))
|
|
}
|
|
|
|
// ===== External Interface Methods =====
|
|
|
|
// GetWorkerByID returns AI worker by ID (for cross-contract use).
|
|
func (o *Opus) GetWorkerByID(d *dao.Simple, workerID uint64) (*state.AIWorker, error) {
|
|
worker := o.getWorkerInternal(d, workerID)
|
|
if worker == nil {
|
|
return nil, ErrOpusWorkerNotFound
|
|
}
|
|
return worker, nil
|
|
}
|
|
|
|
// GetOperatorByOwner returns operator profile by owner (for cross-contract use).
|
|
func (o *Opus) GetOperatorByOwner(d *dao.Simple, owner util.Uint160) (*state.OperatorProfile, error) {
|
|
profile := o.getOperatorByOwner(d, owner)
|
|
if profile == nil {
|
|
return nil, ErrOpusOperatorNotFound
|
|
}
|
|
return profile, nil
|
|
}
|
|
|
|
// IsWorkerActive returns whether worker is active (for cross-contract use).
|
|
func (o *Opus) IsWorkerActive(d *dao.Simple, workerID uint64) bool {
|
|
worker := o.getWorkerInternal(d, workerID)
|
|
if worker == nil {
|
|
return false
|
|
}
|
|
return worker.Status == state.AIWorkerActive
|
|
}
|
|
|
|
// Address returns the contract's address.
|
|
func (o *Opus) Address() util.Uint160 {
|
|
return o.Hash
|
|
}
|