diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 335b99b25..734667014 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -240,6 +240,18 @@ func newNEO(cfg config.ProtocolConfiguration) *NEO { md = newMethodAndPrice(n.setRegisterPrice, 1<<15, callflag.States) n.AddMethod(md, desc) + n.AddEvent("CandidateStateChanged", + manifest.NewParameter("pubkey", smartcontract.PublicKeyType), + manifest.NewParameter("registered", smartcontract.BoolType), + manifest.NewParameter("votes", smartcontract.IntegerType), + ) + n.AddEvent("Vote", + manifest.NewParameter("account", smartcontract.Hash160Type), + manifest.NewParameter("from", smartcontract.PublicKeyType), + manifest.NewParameter("to", smartcontract.PublicKeyType), + manifest.NewParameter("amount", smartcontract.IntegerType), + ) + return n } @@ -733,6 +745,8 @@ func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stac // RegisterCandidateInternal registers pub as a new candidate. func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { + var emitEvent = true + key := makeValidatorKey(pub) si := ic.DAO.GetStorageItem(n.ID, key) var c *candidate @@ -740,9 +754,18 @@ func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey c = &candidate{Registered: true} } else { c = new(candidate).FromBytes(si) + emitEvent = !c.Registered c.Registered = true } - return putConvertibleToDAO(n.ID, ic.DAO, key, c) + err := putConvertibleToDAO(n.ID, ic.DAO, key, c) + if emitEvent { + ic.AddNotification(n.Hash, "CandidateStateChanged", stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(pub.Bytes()), + stackitem.NewBool(c.Registered), + stackitem.NewBigInteger(&c.Votes), + })) + } + return err } func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -759,6 +782,8 @@ func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) st // UnregisterCandidateInternal unregisters pub as a candidate. func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { + var err error + key := makeValidatorKey(pub) si := ic.DAO.GetStorageItem(n.ID, key) if si == nil { @@ -767,12 +792,20 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) cache.validators = nil c := new(candidate).FromBytes(si) + emitEvent := c.Registered c.Registered = false ok := n.dropCandidateIfZero(ic.DAO, cache, pub, c) - if ok { - return nil + if !ok { + err = putConvertibleToDAO(n.ID, ic.DAO, key, c) } - return putConvertibleToDAO(n.ID, ic.DAO, key, c) + if emitEvent { + ic.AddNotification(n.Hash, "CandidateStateChanged", stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(pub.Bytes()), + stackitem.NewBool(c.Registered), + stackitem.NewBigInteger(&c.Votes), + })) + } + return err } func (n *NEO) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -834,17 +867,33 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance), false); err != nil { return err } + oldVote := acc.VoteTo acc.VoteTo = pub if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance, true); err != nil { return err } ic.DAO.PutStorageItem(n.ID, key, acc.Bytes()) + + ic.AddNotification(n.Hash, "Vote", stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(h.BytesBE()), + keyToStackItem(oldVote), + keyToStackItem(pub), + stackitem.NewBigInteger(&acc.Balance), + })) + if newGas != nil { // Can be if it was already distributed in the same block. n.GAS.mint(ic, h, newGas, true) } return nil } +func keyToStackItem(k *keys.PublicKey) stackitem.Item { + if k == nil { + return stackitem.Null{} + } + return stackitem.NewByteArray(k.Bytes()) +} + // ModifyAccountVotes modifies votes of the specified account by value (can be negative). // typ specifies if this modify is occurring during transfer or vote (with old or new validator). func (n *NEO) ModifyAccountVotes(acc *state.NEOBalance, d *dao.Simple, value *big.Int, isNewVote bool) error { diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 4d6b98e46..26eb69085 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -60,6 +60,61 @@ func TestNEO_RegisterPriceCache(t *testing.T) { testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000), "RegisterPrice", native.DefaultRegisterPrice) } +func TestNEO_CandidateEvents(t *testing.T) { + c := newNativeClient(t, nativenames.Neo) + singleSigner := c.Signers[0].(neotest.MultiSigner).Single(0) + cc := c.WithSigners(c.Signers[0], singleSigner) + e := c.Executor + pkb := singleSigner.Account().PrivateKey().PublicKey().Bytes() + + // Register 1 -> event + tx := cc.Invoke(t, true, "registerCandidate", pkb) + e.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "CandidateStateChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(pkb), + stackitem.NewBool(true), + stackitem.Make(0), + }), + }) + + // Register 2 -> no event + tx = cc.Invoke(t, true, "registerCandidate", pkb) + aer := e.GetTxExecResult(t, tx) + require.Equal(t, 0, len(aer.Events)) + + // Vote -> event + tx = c.Invoke(t, true, "vote", c.Signers[0].ScriptHash().BytesBE(), pkb) + e.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "Vote", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(c.Signers[0].ScriptHash().BytesBE()), + stackitem.Null{}, + stackitem.NewByteArray(pkb), + stackitem.Make(100000000), + }), + }) + + // Unregister 1 -> event + tx = cc.Invoke(t, true, "unregisterCandidate", pkb) + e.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "CandidateStateChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(pkb), + stackitem.NewBool(false), + stackitem.Make(100000000), + }), + }) + + // Unregister 2 -> no event + tx = cc.Invoke(t, true, "unregisterCandidate", pkb) + aer = e.GetTxExecResult(t, tx) + require.Equal(t, 0, len(aer.Events)) +} + func TestNEO_Vote(t *testing.T) { neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index e11755d2c..9f553cc09 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -75,7 +75,7 @@ const ( nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" - block20StateRootLE = "0fbb19143cb782df2c893d448a89ec959bea8bf467faf747638975812d570f72" + block20StateRootLE = "cda0adf452c190700f792bcac29973b85532b7c27192e96172cfa0c8acaa4f9e" ) var (