diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 84d2de535..824627863 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1306,8 +1306,8 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { } return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) case transaction.OracleResponseT: - h, err := bc.contracts.Oracle.GetScriptHash() - if err != nil { + h, err := bc.contracts.Oracle.GetScriptHash(bc.dao) + if err != nil || h.Equals(util.Uint160{}) { return fmt.Errorf("%w: %v", ErrInvalidAttribute, err) } hasOracle := false diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index a5c552e3c..49ab65f97 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -499,7 +499,9 @@ func TestVerifyTx(t *testing.T) { InvocationScript: testchain.SignCommittee(txSetOracle.GetSignedPart()), VerificationScript: testchain.CommitteeVerificationScript(), }} - ic := bc.newInteropContext(trigger.All, bc.dao, nil, txSetOracle) + bl := block.New(netmode.UnitTestNet) + bl.Index = bc.BlockHeight() + 1 + ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetOracle) ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, native.RoleOracle, oraclePubs)) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 6225e8cd1..6c3724dc0 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -1,6 +1,7 @@ package native import ( + "encoding/binary" "errors" "math" "sort" @@ -12,8 +13,10 @@ import ( "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/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -23,8 +26,13 @@ type Designate struct { NEO *NEO rolesChangedFlag atomic.Value - oracleNodes atomic.Value - oracleHash atomic.Value + oracles atomic.Value +} + +type oraclesData struct { + nodes keys.PublicKeys + addr util.Uint160 + height uint32 } const ( @@ -46,9 +54,12 @@ const ( // Various errors. var ( - ErrInvalidRole = errors.New("invalid role") - ErrEmptyNodeList = errors.New("node list is empty") - ErrLargeNodeList = errors.New("node list is too large") + ErrAlreadyDesignated = errors.New("already designated given role at current block") + ErrEmptyNodeList = errors.New("node list is empty") + ErrInvalidIndex = errors.New("invalid index") + ErrInvalidRole = errors.New("invalid role") + ErrLargeNodeList = errors.New("node list is too large") + ErrNoBlock = errors.New("no persisting block in the context") ) func isValidRole(r Role) bool { @@ -61,8 +72,9 @@ func newDesignate() *Designate { s.Manifest.Features = smartcontract.HasStorage desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType, - manifest.NewParameter("role", smartcontract.IntegerType)) - md := newMethodAndPrice(s.getDesignatedByRole, 0, smartcontract.AllowStates) + manifest.NewParameter("role", smartcontract.IntegerType), + manifest.NewParameter("index", smartcontract.IntegerType)) + md := newMethodAndPrice(s.getDesignatedByRole, 1000000, smartcontract.AllowStates) s.AddMethod(md, desc, false) desc = newDescriptor("designateAsRole", smartcontract.VoidType, @@ -80,16 +92,6 @@ func newDesignate() *Designate { // 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 } @@ -99,15 +101,17 @@ func (s *Designate) OnPersistEnd(d dao.DAO) error { return nil } - var ns NodeList - err := getSerializableFromDAO(s.ContractID, d, []byte{byte(RoleOracle)}, &ns) + nodeKeys, height, err := s.GetDesignatedByRole(d, RoleOracle, math.MaxUint32) if err != nil { return err } - s.oracleNodes.Store(keys.PublicKeys(ns)) - script, _ := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys(ns).Copy()) - s.oracleHash.Store(hash.Hash160(script)) + od := &oraclesData{ + nodes: nodeKeys, + addr: oracleHashFromNodes(nodeKeys), + height: height, + } + s.oracles.Store(od) s.rolesChangedFlag.Store(false) return nil } @@ -122,7 +126,15 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It if !ok { panic(ErrInvalidRole) } - pubs, err := s.GetDesignatedByRole(ic.DAO, r) + ind, err := args[1].TryInteger() + if err != nil || !ind.IsUint64() { + panic(ErrInvalidIndex) + } + index := ind.Uint64() + if index > uint64(ic.Chain.BlockHeight()+1) { + panic(ErrInvalidIndex) + } + pubs, _, err := s.GetDesignatedByRole(ic.DAO, r, uint32(index)) if err != nil { panic(err) } @@ -134,17 +146,72 @@ func (s *Designate) rolesChanged() bool { return rc == nil || rc.(bool) } -// GetDesignatedByRole returns nodes for role r. -func (s *Designate) GetDesignatedByRole(d dao.DAO, r Role) (keys.PublicKeys, error) { +func oracleHashFromNodes(nodes keys.PublicKeys) util.Uint160 { + if len(nodes) == 0 { + return util.Uint160{} + } + script, _ := smartcontract.CreateMajorityMultiSigRedeemScript(nodes.Copy()) + return hash.Hash160(script) +} + +func (s *Designate) getLastDesignatedHash(d dao.DAO, r Role) (util.Uint160, error) { if !isValidRole(r) { - return nil, ErrInvalidRole + return util.Uint160{}, ErrInvalidRole } if r == RoleOracle && !s.rolesChanged() { - return s.oracleNodes.Load().(keys.PublicKeys), nil + odVal := s.oracles.Load() + if odVal != nil { + od := odVal.(*oraclesData) + return od.addr, nil + } + } + nodes, _, err := s.GetDesignatedByRole(d, r, math.MaxUint32) + if err != nil { + return util.Uint160{}, err + } + // We only have hashing defined for oracles now. + return oracleHashFromNodes(nodes), nil +} + +// GetDesignatedByRole returns nodes for role r. +func (s *Designate) GetDesignatedByRole(d dao.DAO, r Role, index uint32) (keys.PublicKeys, uint32, error) { + if !isValidRole(r) { + return nil, 0, ErrInvalidRole + } + if r == RoleOracle && !s.rolesChanged() { + odVal := s.oracles.Load() + if odVal != nil { + od := odVal.(*oraclesData) + if od.height <= index { + return od.nodes, od.height, nil + } + } + } + kvs, err := d.GetStorageItemsWithPrefix(s.ContractID, []byte{byte(r)}) + if err != nil { + return nil, 0, err } var ns NodeList - err := getSerializableFromDAO(s.ContractID, d, []byte{byte(r)}, &ns) - return keys.PublicKeys(ns), err + var bestIndex uint32 + var resSi *state.StorageItem + for k, si := range kvs { + if len(k) < 4 { + continue + } + siInd := binary.BigEndian.Uint32([]byte(k)) + if (resSi == nil || siInd > bestIndex) && siInd <= index { + bestIndex = siInd + resSi = si + } + } + if resSi != nil { + reader := io.NewBinReaderFromBuf(resSi.Value) + ns.DecodeBinary(reader) + if reader.Err != nil { + return nil, 0, reader.Err + } + } + return keys.PublicKeys(ns), bestIndex, err } func (s *Designate) designateAsRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -180,11 +247,21 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r Role, pubs keys.Publi if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { return ErrInvalidWitness } + if ic.Block == nil { + return ErrNoBlock + } + var key = make([]byte, 5) + key[0] = byte(r) + binary.BigEndian.PutUint32(key[1:], ic.Block.Index+1) + si := ic.DAO.GetStorageItem(s.ContractID, key) + if si != nil { + return ErrAlreadyDesignated + } sort.Sort(pubs) s.rolesChangedFlag.Store(true) - si := &state.StorageItem{Value: NodeList(pubs).Bytes()} - return ic.DAO.PutStorageItem(s.ContractID, []byte{byte(r)}, si) + si = &state.StorageItem{Value: NodeList(pubs).Bytes()} + return ic.DAO.PutStorageItem(s.ContractID, key, si) } func getRole(item stackitem.Item) (Role, bool) { diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 0a3b475b5..4088899e5 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "errors" "fmt" + "math" "math/big" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -367,17 +368,14 @@ func (o *Oracle) PutRequestInternal(id uint64, req *state.OracleRequest, d dao.D } // GetScriptHash returns script hash or oracle nodes. -func (o *Oracle) GetScriptHash() (util.Uint160, error) { - h := o.Desig.oracleHash.Load() - if h == nil { - return util.Uint160{}, storage.ErrKeyNotFound - } - return h.(util.Uint160), nil +func (o *Oracle) GetScriptHash(d dao.DAO) (util.Uint160, error) { + return o.Desig.getLastDesignatedHash(d, RoleOracle) } // GetOracleNodes returns public keys of oracle nodes. func (o *Oracle) GetOracleNodes(d dao.DAO) (keys.PublicKeys, error) { - return o.Desig.GetDesignatedByRole(d, RoleOracle) + nodes, _, err := o.Desig.GetDesignatedByRole(d, RoleOracle, math.MaxUint32) + return nodes, err } // GetRequestInternal returns request by ID and key under which it is stored. diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index df8cd5f3e..8e7e827ed 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/block" "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" @@ -14,6 +15,7 @@ import ( "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" "github.com/stretchr/testify/require" ) @@ -60,6 +62,37 @@ func (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r native.Role, nodes } } +func (bc *Blockchain) getNodesByRole(t *testing.T, ok bool, r native.Role, index uint32, resLen int) { + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.contracts.Designate.Hash, "getDesignatedByRole", int64(r), int64(index)) + 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.None, + }, + } + require.NoError(t, signTx(bc, tx)) + 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) + require.Equal(t, 1, len(aer.Stack)) + arrItem := aer.Stack[0] + require.Equal(t, stackitem.ArrayT, arrItem.Type()) + arr := arrItem.(*stackitem.Array) + require.Equal(t, resLen, arr.Len()) + } else { + require.Equal(t, vm.FaultState, aer.VMState) + } +} + func TestDesignate_DesignateAsRoleTx(t *testing.T) { bc := newTestChain(t) defer bc.Close() @@ -70,6 +103,11 @@ func TestDesignate_DesignateAsRoleTx(t *testing.T) { bc.setNodesByRole(t, false, 0xFF, pubs) bc.setNodesByRole(t, true, native.RoleOracle, pubs) + index := bc.BlockHeight() + 1 + bc.getNodesByRole(t, false, 0xFF, 0, 0) + bc.getNodesByRole(t, false, native.RoleOracle, 100500, 0) + bc.getNodesByRole(t, true, native.RoleOracle, 0, 0) // returns an empty list + bc.getNodesByRole(t, true, native.RoleOracle, index, 1) // returns pubs } func TestDesignate_DesignateAsRole(t *testing.T) { @@ -78,16 +116,19 @@ func TestDesignate_DesignateAsRole(t *testing.T) { des := bc.contracts.Designate tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) - ic := bc.newInteropContext(trigger.OnPersist, bc.dao, nil, tx) + bl := block.New(netmode.UnitTestNet) + bl.Index = bc.BlockHeight() + 1 + ic := bc.newInteropContext(trigger.OnPersist, bc.dao, bl, tx) ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) - pubs, err := des.GetDesignatedByRole(bc.dao, 0xFF) + pubs, index, err := des.GetDesignatedByRole(bc.dao, 0xFF, 255) require.True(t, errors.Is(err, native.ErrInvalidRole), "got: %v", err) - pubs, err = des.GetDesignatedByRole(bc.dao, native.RoleOracle) + pubs, index, err = des.GetDesignatedByRole(bc.dao, native.RoleOracle, 255) require.NoError(t, err) require.Equal(t, 0, len(pubs)) + require.Equal(t, uint32(0), index) err = des.DesignateAsRole(ic, native.RoleOracle, keys.PublicKeys{}) require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err) @@ -110,13 +151,15 @@ func TestDesignate_DesignateAsRole(t *testing.T) { require.NoError(t, err) require.NoError(t, des.OnPersistEnd(ic.DAO)) - pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleOracle) + pubs, index, err = des.GetDesignatedByRole(ic.DAO, native.RoleOracle, bl.Index+1) require.NoError(t, err) require.Equal(t, keys.PublicKeys{pub}, pubs) + require.Equal(t, bl.Index+1, index) - pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleStateValidator) + pubs, index, err = des.GetDesignatedByRole(ic.DAO, native.RoleStateValidator, 255) require.NoError(t, err) require.Equal(t, 0, len(pubs)) + require.Equal(t, uint32(0), index) // Set another role. _, err = keys.NewPrivateKey() @@ -126,11 +169,13 @@ func TestDesignate_DesignateAsRole(t *testing.T) { require.NoError(t, err) require.NoError(t, des.OnPersistEnd(ic.DAO)) - pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleOracle) + pubs, index, err = des.GetDesignatedByRole(ic.DAO, native.RoleOracle, 255) require.NoError(t, err) require.Equal(t, keys.PublicKeys{pub}, pubs) + require.Equal(t, bl.Index+1, index) - pubs, err = des.GetDesignatedByRole(ic.DAO, native.RoleStateValidator) + pubs, index, err = des.GetDesignatedByRole(ic.DAO, native.RoleStateValidator, 255) require.NoError(t, err) require.Equal(t, keys.PublicKeys{pub1}, pubs) + require.Equal(t, bl.Index+1, index) } diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go index 1b93e6ffd..b1bb18b93 100644 --- a/pkg/core/native_oracle_test.go +++ b/pkg/core/native_oracle_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -136,8 +137,10 @@ func TestOracle_Request(t *testing.T) { pub := priv.PublicKey() tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + bl := block.New(netmode.UnitTestNet) + bl.Index = bc.BlockHeight() + 1 setSigner(tx, testchain.CommitteeScriptHash()) - ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) + ic := bc.newInteropContext(trigger.Application, bc.dao, bl, tx) ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) err = bc.contracts.Designate.DesignateAsRole(ic, native.RoleOracle, keys.PublicKeys{pub})