diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index e8df34de1..a6dad7c95 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -651,7 +651,9 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error return err } bc.contracts.Policy.OnPersistEnd(bc.dao) - bc.contracts.Oracle.OnPersistEnd(bc.dao) + if err := bc.contracts.Designate.OnPersistEnd(bc.dao); err != nil { + return err + } bc.dao.MPT.Flush() // Every persist cycle we also compact our in-memory MPT. persistedHeight := atomic.LoadUint32(&bc.persistedHeight) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 2347f03cc..ba3ea7a11 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -443,8 +443,8 @@ func TestVerifyTx(t *testing.T) { ic := bc.newInteropContext(trigger.All, bc.dao, nil, txSetOracle) ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) - require.NoError(t, bc.contracts.Oracle.SetOracleNodes(ic, oraclePubs)) - bc.contracts.Oracle.OnPersistEnd(ic.DAO) + require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, native.RoleOracle, oraclePubs)) + require.NoError(t, bc.contracts.Designate.OnPersistEnd(ic.DAO)) _, err = ic.DAO.Persist() require.NoError(t, err) diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 45490b7f7..e115d2a0b 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -17,6 +17,7 @@ type Contracts struct { GAS *GAS Policy *Policy Oracle *Oracle + Designate *Designate Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -58,6 +59,13 @@ func NewContracts() *Contracts { oracle.NEO = neo cs.Oracle = oracle cs.Contracts = append(cs.Contracts, oracle) + + desig := newDesignate() + desig.NEO = neo + cs.Designate = desig + cs.Oracle.Desig = desig + cs.Contracts = append(cs.Contracts, desig) + return cs } @@ -71,7 +79,7 @@ func (cs *Contracts) GetPersistScript() []byte { md := cs.Contracts[i].Metadata() // Not every contract is persisted: // https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L90 - if md.ContractID == policyContractID || md.ContractID == oracleContractID { + if md.ContractID == policyContractID || md.ContractID == oracleContractID || md.ContractID == designateContractID { continue } emit.Int(w.BinWriter, 0) @@ -94,7 +102,7 @@ func (cs *Contracts) GetPostPersistScript() []byte { md := cs.Contracts[i].Metadata() // Not every contract is persisted: // https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103 - if md.ContractID == policyContractID || md.ContractID == gasContractID { + if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID { continue } emit.Int(w.BinWriter, 0) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go new file mode 100644 index 000000000..6da4f8481 --- /dev/null +++ b/pkg/core/native/designate.go @@ -0,0 +1,188 @@ +package native + +import ( + "errors" + "math" + "sort" + "sync/atomic" + + "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/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Designate represents designation contract. +type Designate struct { + interop.ContractMD + NEO *NEO + + rolesChangedFlag atomic.Value + oracleNodes atomic.Value + oracleHash atomic.Value +} + +const ( + designateContractID = -5 + designateName = "Designation" +) + +// Role represents type of participant. +type Role byte + +// Role enumeration. +const ( + RoleStateValidator Role = 4 + RoleOracle Role = 8 +) + +// Various errors. +var ( + ErrInvalidRole = errors.New("invalid role") + ErrEmptyNodeList = errors.New("node list is empty") +) + +func isValidRole(r Role) bool { + return r == RoleOracle || r == RoleStateValidator +} + +func newDesignate() *Designate { + s := &Designate{ContractMD: *interop.NewContractMD(designateName)} + s.ContractID = designateContractID + s.Manifest.Features = smartcontract.HasStorage + + desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType, + manifest.NewParameter("role", smartcontract.IntegerType)) + md := newMethodAndPrice(s.getDesignatedByRole, 0, smartcontract.AllowStates) + s.AddMethod(md, desc, false) + + desc = newDescriptor("designateAsRole", smartcontract.VoidType, + manifest.NewParameter("role", smartcontract.IntegerType), + manifest.NewParameter("nodes", smartcontract.ArrayType)) + md = newMethodAndPrice(s.designateAsRole, 0, smartcontract.AllowModifyStates) + s.AddMethod(md, desc, false) + + return s +} + +// Initialize initializes Oracle contract. +func (s *Designate) Initialize(ic *interop.Context) error { + roles := []Role{RoleStateValidator, RoleOracle} + for _, r := range roles { + si := &state.StorageItem{Value: new(NodeList).Bytes()} + if err := ic.DAO.PutStorageItem(s.ContractID, []byte{byte(r)}, si); err != nil { + return err + } + } + + s.oracleNodes.Store(keys.PublicKeys(nil)) + s.rolesChangedFlag.Store(true) + return nil +} + +// OnPersistEnd updates cached values if they've been changed. +func (s *Designate) OnPersistEnd(d dao.DAO) error { + if !s.rolesChanged() { + return nil + } + + var ns NodeList + err := getSerializableFromDAO(s.ContractID, d, []byte{byte(RoleOracle)}, &ns) + if err != nil { + return err + } + + s.oracleNodes.Store(keys.PublicKeys(ns)) + script, _ := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys(ns).Copy()) + s.oracleHash.Store(hash.Hash160(script)) + s.rolesChangedFlag.Store(false) + return nil +} + +// Metadata returns contract metadata. +func (s *Designate) Metadata() *interop.ContractMD { + return &s.ContractMD +} + +func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { + r, ok := getRole(args[0]) + if !ok { + panic(ErrInvalidRole) + } + pubs, err := s.GetDesignatedByRole(ic.DAO, r) + if err != nil { + panic(err) + } + return pubsToArray(pubs) +} + +func (s *Designate) rolesChanged() bool { + rc := s.rolesChangedFlag.Load() + return rc == nil || rc.(bool) +} + +// GetDesignatedByRole returns nodes for role r. +func (s *Designate) GetDesignatedByRole(d dao.DAO, r Role) (keys.PublicKeys, error) { + if !isValidRole(r) { + return nil, ErrInvalidRole + } + if r == RoleOracle && !s.rolesChanged() { + return s.oracleNodes.Load().(keys.PublicKeys), nil + } + var ns NodeList + err := getSerializableFromDAO(s.ContractID, d, []byte{byte(r)}, &ns) + return keys.PublicKeys(ns), err +} + +func (s *Designate) designateAsRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { + r, ok := getRole(args[0]) + if !ok { + panic(ErrInvalidRole) + } + var ns NodeList + if err := ns.fromStackItem(args[1]); err != nil { + panic(err) + } + + err := s.DesignateAsRole(ic, r, keys.PublicKeys(ns)) + if err != nil { + panic(err) + } + return pubsToArray(keys.PublicKeys(ns)) +} + +// DesignateAsRole sets nodes for role r. +func (s *Designate) DesignateAsRole(ic *interop.Context, r Role, pubs keys.PublicKeys) error { + if len(pubs) == 0 { + return ErrEmptyNodeList + } + if !isValidRole(r) { + return ErrInvalidRole + } + h := s.NEO.GetCommitteeAddress() + if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { + return ErrInvalidWitness + } + + sort.Sort(pubs) + s.rolesChangedFlag.Store(true) + si := &state.StorageItem{Value: NodeList(pubs).Bytes()} + return ic.DAO.PutStorageItem(s.ContractID, []byte{byte(r)}, si) +} + +func getRole(item stackitem.Item) (Role, bool) { + bi, err := item.TryInteger() + if err != nil { + return 0, false + } + if !bi.IsUint64() { + return 0, false + } + u := bi.Uint64() + return Role(u), u <= math.MaxUint8 && isValidRole(Role(u)) +} diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 2fa59cb02..4bd88e11f 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -4,14 +4,11 @@ import ( "encoding/binary" "errors" "math/big" - "sort" - "sync/atomic" "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" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "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" @@ -32,12 +29,7 @@ type Oracle struct { GAS *GAS NEO *NEO - // nodesChanged is true if `SetOracleNodes` was called. - nodesChanged atomic.Value - // nodes contains cached list of oracle nodes. - nodes atomic.Value - // oracleHash contains cached oracle script hash. - oracleHash atomic.Value + Desig *Designate } const ( @@ -81,7 +73,6 @@ var ( // Various validation errors. var ( ErrBigArgument = errors.New("some of the arguments are invalid") - ErrEmptyNodeList = errors.New("oracle nodes list is empty") ErrInvalidWitness = errors.New("witness check failed") ErrNotEnoughGas = errors.New("gas limit exceeded") ErrRequestNotFound = errors.New("oracle request not found") @@ -113,14 +104,6 @@ func newOracle() *Oracle { md = newMethodAndPrice(o.finish, 0, smartcontract.AllowModifyStates) o.AddMethod(md, desc, false) - desc = newDescriptor("getOracleNodes", smartcontract.ArrayType) - md = newMethodAndPrice(o.getOracleNodes, 100_0000, smartcontract.AllowStates) - o.AddMethod(md, desc, true) - - desc = newDescriptor("setOracleNodes", smartcontract.VoidType) - md = newMethodAndPrice(o.setOracleNodes, 0, smartcontract.AllowModifyStates) - o.AddMethod(md, desc, false) - desc = newDescriptor("verify", smartcontract.BoolType) md = newMethodAndPrice(o.verify, 100_0000, smartcontract.NoneFlag) o.AddMethod(md, desc, false) @@ -130,9 +113,6 @@ func newOracle() *Oracle { md = newMethodAndPrice(getOnPersistWrapper(pp), 0, smartcontract.AllowModifyStates) o.AddMethod(md, desc, false) - o.nodes.Store(keys.PublicKeys(nil)) - o.nodesChanged.Store(false) - return o } @@ -176,7 +156,10 @@ func (o *Oracle) PostPersist(ic *interop.Context) error { } if nodes == nil { - nodes = o.GetOracleNodes() + nodes, err = o.GetOracleNodes(ic.DAO) + if err != nil { + return err + } reward = make([]big.Int, len(nodes)) } @@ -336,48 +319,18 @@ func (o *Oracle) PutRequestInternal(id uint64, req *OracleRequest, d dao.DAO) er return d.PutStorageItem(o.ContractID, key, si) } -func (o *Oracle) getOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - pubs := o.GetOracleNodes() - return pubsToArray(pubs) -} - -// GetOracleNodes returns public keys of oracle nodes. -func (o *Oracle) GetOracleNodes() keys.PublicKeys { - return o.nodes.Load().(keys.PublicKeys).Copy() -} - // GetScriptHash returns script hash or oracle nodes. func (o *Oracle) GetScriptHash() (util.Uint160, error) { - h := o.oracleHash.Load() + h := o.Desig.oracleHash.Load() if h == nil { return util.Uint160{}, storage.ErrKeyNotFound } return h.(util.Uint160), nil } -func (o *Oracle) setOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - var pubs keys.PublicKeys - err := o.SetOracleNodes(ic, pubs) - if err != nil { - panic(err) - } - return pubsToArray(pubs) -} - -// SetOracleNodes sets oracle node public keys to pubs. -func (o *Oracle) SetOracleNodes(ic *interop.Context, pubs keys.PublicKeys) error { - if len(pubs) == 0 { - return ErrEmptyNodeList - } - h := o.NEO.GetCommitteeAddress() - if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { - return ErrInvalidWitness - } - - sort.Sort(pubs) - o.nodesChanged.Store(true) - si := &state.StorageItem{Value: NodeList(pubs).Bytes()} - return ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si) +// GetOracleNodes returns public keys of oracle nodes. +func (o *Oracle) GetOracleNodes(d dao.DAO) (keys.PublicKeys, error) { + return o.Desig.GetDesignatedByRole(d, RoleOracle) } // GetRequestInternal returns request by ID and key under which it is stored. @@ -421,26 +374,5 @@ func makeIDListKey(url string) []byte { } func (o *Oracle) getSerializableFromDAO(d dao.DAO, key []byte, item io.Serializable) error { - si := d.GetStorageItem(o.ContractID, key) - if si == nil { - return storage.ErrKeyNotFound - } - r := io.NewBinReaderFromBuf(si.Value) - item.DecodeBinary(r) - return r.Err -} - -// OnPersistEnd updates cached Oracle values if they've been changed -func (o *Oracle) OnPersistEnd(d dao.DAO) { - if !o.nodesChanged.Load().(bool) { - return - } - - ns := new(NodeList) - _ = o.getSerializableFromDAO(d, prefixNodeList, ns) - o.nodes.Store(keys.PublicKeys(*ns)) - script, _ := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys(*ns).Copy()) - o.oracleHash.Store(hash.Hash160(script)) - o.nodesChanged.Store(false) - return + return getSerializableFromDAO(o.ContractID, d, key, item) } diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go new file mode 100644 index 000000000..293581c34 --- /dev/null +++ b/pkg/core/native/util.go @@ -0,0 +1,17 @@ +package native + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/io" +) + +func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error { + si := d.GetStorageItem(id, key) + if si == nil { + return storage.ErrKeyNotFound + } + r := io.NewBinReaderFromBuf(si.Value) + item.DecodeBinary(r) + return r.Err +} diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go new file mode 100644 index 000000000..9abe54ba4 --- /dev/null +++ b/pkg/core/native_designate_test.go @@ -0,0 +1,133 @@ +package core + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "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/stretchr/testify/require" +) + +func (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r native.Role, nodes keys.PublicKeys) { + w := io.NewBufBinWriter() + for _, pub := range nodes { + emit.Bytes(w.BinWriter, pub.Bytes()) + } + emit.Int(w.BinWriter, int64(len(nodes))) + emit.Opcode(w.BinWriter, opcode.PACK) + emit.Int(w.BinWriter, int64(r)) + emit.Int(w.BinWriter, 2) + emit.Opcode(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "designateAsRole") + emit.AppCall(w.BinWriter, bc.contracts.Designate.Hash) + require.NoError(t, w.Err) + tx := transaction.New(netmode.UnitTestNet, w.Bytes(), 0) + tx.NetworkFee = 10_000_000 + tx.SystemFee = 10_000_000 + tx.ValidUntilBlock = 100 + tx.Signers = []transaction.Signer{ + { + Account: testchain.MultisigScriptHash(), + Scopes: transaction.FeeOnly, + }, + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.CalledByEntry, + }, + } + require.NoError(t, signTx(bc, tx)) + tx.Scripts = append(tx.Scripts, transaction.Witness{ + InvocationScript: testchain.SignCommittee(tx.GetSignedPart()), + VerificationScript: testchain.CommitteeVerificationScript(), + }) + require.NoError(t, bc.AddBlock(bc.newBlock(tx))) + + aer, err := bc.GetAppExecResult(tx.Hash()) + require.NoError(t, err) + if ok { + require.Equal(t, vm.HaltState, aer.VMState) + } else { + require.Equal(t, vm.FaultState, aer.VMState) + } +} + +func TestDesignate_DesignateAsRoleTx(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pubs := keys.PublicKeys{priv.PublicKey()} + + bc.setNodesByRole(t, false, 0xFF, pubs) + bc.setNodesByRole(t, true, native.RoleOracle, pubs) +} + +func TestDesignate_DesignateAsRole(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + des := bc.contracts.Designate + tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) + ic.VM = vm.New() + ic.VM.LoadScript([]byte{byte(opcode.RET)}) + + pubs, err := des.GetDesignatedByRole(bc.dao, 0xFF) + require.True(t, errors.Is(err, native.ErrInvalidRole), "got: %v", err) + + pubs, err = des.GetDesignatedByRole(bc.dao, native.RoleOracle) + require.NoError(t, err) + require.Equal(t, 0, len(pubs)) + + err = des.DesignateAsRole(ic, native.RoleOracle, keys.PublicKeys{}) + require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err) + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + err = des.DesignateAsRole(ic, 0xFF, keys.PublicKeys{pub}) + require.True(t, errors.Is(err, native.ErrInvalidRole), "got: %v", err) + + err = des.DesignateAsRole(ic, native.RoleOracle, keys.PublicKeys{pub}) + require.True(t, errors.Is(err, native.ErrInvalidWitness), "got: %v", err) + + setSigner(tx, testchain.CommitteeScriptHash()) + err = des.DesignateAsRole(ic, native.RoleOracle, keys.PublicKeys{pub}) + require.NoError(t, err) + require.NoError(t, des.OnPersistEnd(ic.DAO)) + + pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleOracle) + require.NoError(t, err) + require.Equal(t, keys.PublicKeys{pub}, pubs) + + pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleStateValidator) + require.NoError(t, err) + require.Equal(t, 0, len(pubs)) + + // Set another role. + _, err = keys.NewPrivateKey() + require.NoError(t, err) + pub1 := priv.PublicKey() + err = des.DesignateAsRole(ic, native.RoleStateValidator, keys.PublicKeys{pub1}) + require.NoError(t, err) + require.NoError(t, des.OnPersistEnd(ic.DAO)) + + pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleOracle) + require.NoError(t, err) + require.Equal(t, keys.PublicKeys{pub}, pubs) + + pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleStateValidator) + require.NoError(t, err) + require.Equal(t, keys.PublicKeys{pub1}, pubs) +} diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go index 8e21e045b..2248c5400 100644 --- a/pkg/core/native_oracle_test.go +++ b/pkg/core/native_oracle_test.go @@ -17,7 +17,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "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" @@ -141,9 +140,9 @@ func TestOracle_Request(t *testing.T) { ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) - err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) + err = bc.contracts.Designate.DesignateAsRole(ic, native.RoleOracle, keys.PublicKeys{pub}) require.NoError(t, err) - orc.OnPersistEnd(ic.DAO) + require.NoError(t, bc.contracts.Designate.OnPersistEnd(ic.DAO)) tx = transaction.New(netmode.UnitTestNet, native.GetOracleResponseScript(), 0) ic.Tx = tx @@ -215,34 +214,3 @@ func TestOracle_Request(t *testing.T) { require.Error(t, err) }) } - -func TestOracle_SetOracleNodes(t *testing.T) { - bc := newTestChain(t) - defer bc.Close() - - orc := bc.contracts.Oracle - tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) - ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) - ic.VM = vm.New() - ic.VM.LoadScript([]byte{byte(opcode.RET)}) - - pubs := orc.GetOracleNodes() - require.Equal(t, 0, len(pubs)) - - err := orc.SetOracleNodes(ic, keys.PublicKeys{}) - require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err) - - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - - pub := priv.PublicKey() - err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) - require.True(t, errors.Is(err, native.ErrInvalidWitness), "got: %v", err) - - setSigner(tx, testchain.CommitteeScriptHash()) - require.NoError(t, orc.SetOracleNodes(ic, keys.PublicKeys{pub})) - orc.OnPersistEnd(ic.DAO) - - pubs = orc.GetOracleNodes() - require.Equal(t, keys.PublicKeys{pub}, pubs) -}