diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 71440c91d..4e0e426a1 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -265,6 +265,10 @@ func newNEO(cfg config.ProtocolConfiguration) *NEO { manifest.NewParameter("to", smartcontract.PublicKeyType), manifest.NewParameter("amount", smartcontract.IntegerType), ) + n.AddEvent("CommitteeChanged", + manifest.NewParameter("old", smartcontract.ArrayType), + manifest.NewParameter("new", smartcontract.ArrayType), + ) return n } @@ -425,6 +429,16 @@ func (n *NEO) OnPersist(ic *interop.Context) error { cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) // Cached newEpoch* values always have proper value set (either by PostPersist // during the last epoch block handling or by initialization code). + + var oldCommittee, newCommittee stackitem.Item + for i := 0; i < len(cache.committee); i++ { + if cache.newEpochCommittee[i].Key != cache.committee[i].Key || + (i == 0 && len(cache.newEpochCommittee) != len(cache.committee)) { + oldCommittee, newCommittee = cache.committee.toNotificationItem(), cache.newEpochCommittee.toNotificationItem() + break + } + } + cache.nextValidators = cache.newEpochNextValidators cache.committee = cache.newEpochCommittee cache.committeeHash = cache.newEpochCommitteeHash @@ -432,6 +446,12 @@ func (n *NEO) OnPersist(ic *interop.Context) error { // We need to put in storage anyway, as it affects dumps ic.DAO.PutStorageItem(n.ID, prefixCommittee, cache.committee.Bytes(ic.DAO.GetItemCtx())) + + if oldCommittee != nil { + ic.AddNotification(n.Hash, "CommitteeChanged", stackitem.NewArray([]stackitem.Item{ + oldCommittee, newCommittee, + })) + } } return nil } diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 55c07f8bd..ac2ece158 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -28,6 +28,7 @@ import ( "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/assert" "github.com/stretchr/testify/require" ) @@ -118,6 +119,70 @@ func TestNEO_CandidateEvents(t *testing.T) { require.Equal(t, 0, len(aer.Events)) } +func TestNEO_CommitteeEvents(t *testing.T) { + neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) + neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) + e := neoCommitteeInvoker.Executor + + cfg := e.Chain.GetConfig() + committeeSize := cfg.GetCommitteeSize(0) + + voters := make([]neotest.Signer, committeeSize) + candidates := make([]neotest.Signer, committeeSize) + for i := 0; i < committeeSize; i++ { + voters[i] = e.NewAccount(t, 10_0000_0000) + candidates[i] = e.NewAccount(t, 2000_0000_0000) // enough for one registration + } + txes := make([]*transaction.Transaction, 0, committeeSize*3) + for i := 0; i < committeeSize; i++ { + transferTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), voters[i].(neotest.SingleSigner).Account().PrivateKey().GetScriptHash(), int64(committeeSize-i)*1000000, nil) + txes = append(txes, transferTx) + + registerTx := neoValidatorsInvoker.WithSigners(candidates[i]).PrepareInvoke(t, "registerCandidate", candidates[i].(neotest.SingleSigner).Account().PublicKey().Bytes()) + txes = append(txes, registerTx) + + voteTx := neoValidatorsInvoker.WithSigners(voters[i]).PrepareInvoke(t, "vote", voters[i].(neotest.SingleSigner).Account().PrivateKey().GetScriptHash(), candidates[i].(neotest.SingleSigner).Account().PublicKey().Bytes()) + txes = append(txes, voteTx) + } + block := neoValidatorsInvoker.AddNewBlock(t, txes...) + for _, tx := range txes { + e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) + } + + // Advance the chain to trigger committee recalculation and potential change. + for (block.Index)%uint32(committeeSize) != 0 { + block = neoCommitteeInvoker.AddNewBlock(t) + } + + // Check for CommitteeChanged event in the last persisted block's AER. + blockHash := e.Chain.CurrentBlockHash() + aer, err := e.Chain.GetAppExecResults(blockHash, trigger.OnPersist) + require.NoError(t, err) + require.Equal(t, 1, len(aer)) + + require.Equal(t, aer[0].Events[0].Name, "CommitteeChanged") + require.Equal(t, 2, len(aer[0].Events[0].Item.Value().([]stackitem.Item))) + + expectedOldCommitteePublicKeys, err := keys.NewPublicKeysFromStrings(cfg.StandbyCommittee) + require.NoError(t, err) + expectedOldCommitteeStackItems := make([]stackitem.Item, len(expectedOldCommitteePublicKeys)) + for i, pubKey := range expectedOldCommitteePublicKeys { + expectedOldCommitteeStackItems[i] = stackitem.NewByteArray(pubKey.Bytes()) + } + oldCommitteeStackItem := aer[0].Events[0].Item.Value().([]stackitem.Item)[0].(*stackitem.Array) + for i, item := range oldCommitteeStackItem.Value().([]stackitem.Item) { + assert.Equal(t, expectedOldCommitteeStackItems[i].(*stackitem.ByteArray).Value().([]byte), item.Value().([]byte)) + } + expectedNewCommitteeStackItems := make([]stackitem.Item, 0, committeeSize) + for _, candidate := range candidates { + expectedNewCommitteeStackItems = append(expectedNewCommitteeStackItems, stackitem.NewByteArray(candidate.(neotest.SingleSigner).Account().PublicKey().Bytes())) + } + newCommitteeStackItem := aer[0].Events[0].Item.Value().([]stackitem.Item)[1].(*stackitem.Array) + for i, item := range newCommitteeStackItem.Value().([]stackitem.Item) { + assert.Equal(t, expectedNewCommitteeStackItems[i].(*stackitem.ByteArray).Value().([]byte), item.Value().([]byte)) + } +} + func TestNEO_Vote(t *testing.T) { neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) diff --git a/pkg/core/native/neo_types.go b/pkg/core/native/neo_types.go index 530f7b935..66469f255 100644 --- a/pkg/core/native/neo_types.go +++ b/pkg/core/native/neo_types.go @@ -52,6 +52,16 @@ func (k keysWithVotes) toStackItem() stackitem.Item { return stackitem.NewArray(arr) } +// toNotificationItem converts keysWithVotes to a stackitem.Item suitable for use in a notification, +// including public keys only. +func (k keysWithVotes) toNotificationItem() stackitem.Item { + arr := make([]stackitem.Item, len(k)) + for i := range k { + arr[i] = stackitem.NewByteArray([]byte(k[i].Key)) + } + return stackitem.NewArray(arr) +} + func (k *keysWithVotes) fromStackItem(item stackitem.Item) error { arr, ok := item.Value().([]stackitem.Item) if !ok { diff --git a/pkg/rpcclient/neo/neo.go b/pkg/rpcclient/neo/neo.go index 3da8b47a0..8817e3658 100644 --- a/pkg/rpcclient/neo/neo.go +++ b/pkg/rpcclient/neo/neo.go @@ -75,6 +75,12 @@ type CandidateStateEvent struct { Votes *big.Int } +// CommitteeChangedEvent represents a CommitteeChanged NEO event. +type CommitteeChangedEvent struct { + Old []keys.PublicKey + New []keys.PublicKey +} + // VoteEvent represents a Vote NEO event. type VoteEvent struct { Account util.Uint160 diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 1b74763ad..e374ae181 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -87,7 +87,7 @@ const ( faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60" faultedTxBlock uint32 = 23 invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" - block20StateRootLE = "858c873539d6d24a70f2be13f9dafc61aef2b63c2aa16bb440676de6e44e3cf1" + block20StateRootLE = "397c69adbc0201d59623fa913bfff4a2da25c792c484d1d278c061709f2c21cf" ) var (