diff --git a/docs/node-configuration.md b/docs/node-configuration.md index d2da0d774..fa3010878 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -470,7 +470,7 @@ in development and can change in an incompatible way. | `Basilisk` | Enables strict smart contract script check against a set of JMP instructions and against method boundaries enabled on contract deploy or update. Increases `stackitem.Integer` JSON parsing precision up to the maximum value supported by the NeoVM. Enables strict check for notifications emitted by a contract to precisely match the events specified in the contract manifest. | https://github.com/nspcc-dev/neo-go/pull/3056
https://github.com/neo-project/neo/pull/2881
https://github.com/nspcc-dev/neo-go/pull/3080
https://github.com/neo-project/neo/pull/2883
https://github.com/nspcc-dev/neo-go/pull/3085
https://github.com/neo-project/neo/pull/2810 | | `Cockatrice` | Introduces the ability to update native contracts. Includes a couple of new native smart contract APIs: `keccak256` of native CryptoLib contract and `getCommitteeAddress` of native NeoToken contract. | https://github.com/nspcc-dev/neo-go/pull/3402
https://github.com/neo-project/neo/pull/2942
https://github.com/nspcc-dev/neo-go/pull/3301
https://github.com/neo-project/neo/pull/2925
https://github.com/nspcc-dev/neo-go/pull/3362
https://github.com/neo-project/neo/pull/3154 | | `Domovoi` | Makes node use executing contract state for the contract call permissions check instead of the state stored in the native Management contract. In C# also makes System.Runtime.GetNotifications interop properly count stack references of notification parameters which prevents users from creating objects that exceed MaxStackSize constraint, but NeoGo has never had this bug, thus proper behaviour is preserved even before HFDomovoi. It results in the fact that some T5 testnet transactions have different ApplicationLogs compared to the C# node, but the node states match. | https://github.com/nspcc-dev/neo-go/pull/3476
https://github.com/neo-project/neo/pull/3290
https://github.com/nspcc-dev/neo-go/pull/3473
https://github.com/neo-project/neo/pull/3290
https://github.com/neo-project/neo/pull/3301
https://github.com/nspcc-dev/neo-go/pull/3485 | -| `Echidna` | No changes for now | https://github.com/nspcc-dev/neo-go/pull/3554 | +| `Echidna` | Enables onNEP17Payment method of NEO contract for candidate registration. | https://github.com/nspcc-dev/neo-go/pull/3554
https://github.com/neo-project/neo/pull/3597
https://github.com/nspcc-dev/neo-go/pull/3700 | ## DB compatibility diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 9978e0b97..245c26632 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -121,6 +121,8 @@ var ( bigVoterRewardFactor = big.NewInt(voterRewardFactor) bigEffectiveVoterTurnout = big.NewInt(effectiveVoterTurnout) big100 = big.NewInt(100) + + errRegistrationNotWitnessed = errors.New("not witnessed by the key owner") ) var ( @@ -200,6 +202,13 @@ func newNEO(cfg config.ProtocolConfiguration) *NEO { md = newMethodAndPrice(n.unregisterCandidate, 1<<16, callflag.States) n.AddMethod(md, desc) + desc = newDescriptor("onNEP17Payment", smartcontract.VoidType, + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("data", smartcontract.AnyType)) + md = newMethodAndPrice(n.onNEP17Payment, 1<<15, callflag.States|callflag.AllowNotify, config.HFEchidna) + n.AddMethod(md, desc) + desc = newDescriptor("vote", smartcontract.BoolType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("voteTo", smartcontract.PublicKeyType)) @@ -815,19 +824,54 @@ func (n *NEO) CalculateNEOHolderReward(d *dao.Simple, value *big.Int, start, end func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { pub := toPublicKey(args[0]) - ok, err := runtime.CheckKeyedWitness(ic, pub) - if err != nil { - panic(err) - } else if !ok { - return stackitem.NewBool(false) + if !ic.IsHardforkEnabled(config.HFEchidna) { + ok, err := runtime.CheckKeyedWitness(ic, pub) + if err != nil { + panic(err) + } else if !ok { + return stackitem.NewBool(false) + } } if !ic.VM.AddGas(n.getRegisterPriceInternal(ic.DAO)) { panic("insufficient gas") } - err = n.RegisterCandidateInternal(ic, pub) + var err = n.RegisterCandidateInternal(ic, pub) return stackitem.NewBool(err == nil) } +func (n *NEO) checkRegisterCandidate(ic *interop.Context, pub *keys.PublicKey) error { + ok, err := runtime.CheckKeyedWitness(ic, pub) + if err != nil { + panic(err) + } else if !ok { + return errRegistrationNotWitnessed + } + return n.RegisterCandidateInternal(ic, pub) +} + +func (n *NEO) onNEP17Payment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + var ( + caller = ic.VM.GetCallingScriptHash() + _ = toUint160(args[0]) + amount = toBigInt(args[1]) + pub = toPublicKey(args[2]) + regPrice = n.getRegisterPriceInternal(ic.DAO) + ) + + if caller != n.GAS.Hash { + panic("only GAS is accepted") + } + if !amount.IsInt64() || amount.Int64() != regPrice { + panic(fmt.Errorf("incorrect GAS amount for registration (expected %d)", regPrice)) + } + var err = n.checkRegisterCandidate(ic, pub) + if err != nil { + panic(err) + } + n.GAS.burn(ic, n.Hash, amount) + return stackitem.Null{} +} + // RegisterCandidateInternal registers pub as a new candidate. func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { var emitEvent = true diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 616208c9a..6e8372a67 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/config" "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/native/nativenames" @@ -34,8 +35,8 @@ import ( "github.com/stretchr/testify/require" ) -func newNeoCommitteeClient(t *testing.T, expectedGASBalance int) *neotest.ContractInvoker { - bc, validators, committee := chain.NewMulti(t) +func newNeoCommitteeClient(t *testing.T, expectedGASBalance int, cfg func(c *config.Blockchain)) *neotest.ContractInvoker { + bc, validators, committee := chain.NewMultiWithCustomConfig(t, cfg) e := neotest.NewExecutor(t, bc, validators, committee) if expectedGASBalance > 0 { @@ -46,24 +47,24 @@ func newNeoCommitteeClient(t *testing.T, expectedGASBalance int) *neotest.Contra } func newNeoValidatorsClient(t *testing.T) *neotest.ContractInvoker { - c := newNeoCommitteeClient(t, 100_0000_0000) + c := newNeoCommitteeClient(t, 100_0000_0000, nil) return c.ValidatorInvoker(c.NativeHash(t, nativenames.Neo)) } func TestNEO_GasPerBlock(t *testing.T) { - testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000), "GasPerBlock", 5*native.GASFactor, 0, 10*native.GASFactor) + testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000, nil), "GasPerBlock", 5*native.GASFactor, 0, 10*native.GASFactor) } func TestNEO_GasPerBlockCache(t *testing.T) { - testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000), "GasPerBlock", 5*native.GASFactor) + testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000, nil), "GasPerBlock", 5*native.GASFactor) } func TestNEO_RegisterPrice(t *testing.T) { - testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000), "RegisterPrice", native.DefaultRegisterPrice, 1, math.MaxInt64) + testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000, nil), "RegisterPrice", native.DefaultRegisterPrice, 1, math.MaxInt64) } func TestNEO_RegisterPriceCache(t *testing.T) { - testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000), "RegisterPrice", native.DefaultRegisterPrice) + testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000, nil), "RegisterPrice", native.DefaultRegisterPrice) } func TestNEO_CandidateEvents(t *testing.T) { @@ -122,7 +123,7 @@ func TestNEO_CandidateEvents(t *testing.T) { } func TestNEO_CommitteeEvents(t *testing.T) { - neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) + neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000, nil) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) e := neoCommitteeInvoker.Executor @@ -186,7 +187,7 @@ func TestNEO_CommitteeEvents(t *testing.T) { } func TestNEO_Vote(t *testing.T) { - neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) + neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000, nil) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) policyInvoker := neoCommitteeInvoker.CommitteeInvoker(neoCommitteeInvoker.NativeHash(t, nativenames.Policy)) e := neoCommitteeInvoker.Executor @@ -371,7 +372,7 @@ func TestNEO_Vote(t *testing.T) { // TestNEO_RecursiveDistribution is a test for https://github.com/nspcc-dev/neo-go/pull/2181. func TestNEO_RecursiveGASMint(t *testing.T) { - neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) + neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000, nil) neoValidatorInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) e := neoCommitteeInvoker.Executor gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) @@ -545,7 +546,7 @@ func TestNEO_GetAccountStateInteropAPI(t *testing.T) { } func TestNEO_CommitteeBountyOnPersist(t *testing.T) { - neoCommitteeInvoker := newNeoCommitteeClient(t, 0) + neoCommitteeInvoker := newNeoCommitteeClient(t, 0, nil) e := neoCommitteeInvoker.Executor hs, err := keys.NewPublicKeysFromStrings(e.Chain.GetConfig().StandbyCommittee) @@ -732,7 +733,7 @@ func TestNEO_TransferNonZeroWithZeroBalance(t *testing.T) { } func TestNEO_CalculateBonus(t *testing.T) { - neoCommitteeInvoker := newNeoCommitteeClient(t, 10_0000_0000) + neoCommitteeInvoker := newNeoCommitteeClient(t, 10_0000_0000, nil) e := neoCommitteeInvoker.Executor neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(e.Validator) @@ -817,7 +818,7 @@ func TestNEO_UnclaimedGas(t *testing.T) { } func TestNEO_GetCandidates(t *testing.T) { - neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) + neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000, nil) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) policyInvoker := neoCommitteeInvoker.CommitteeInvoker(neoCommitteeInvoker.NativeHash(t, nativenames.Policy)) e := neoCommitteeInvoker.Executor @@ -900,3 +901,88 @@ func TestNEO_GetCandidates(t *testing.T) { neoCommitteeInvoker.Invoke(t, expected, "getCandidates") checkGetAllCandidates(t, expected) } + +func TestNEO_RegisterViaNEP27(t *testing.T) { + const echidnaHeight = 12 // Same as Domovoi in UT config. + + neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000, func(c *config.Blockchain) { + c.ProtocolConfiguration.Hardforks[config.HFEchidna.String()] = echidnaHeight + }) + neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) + e := neoCommitteeInvoker.Executor + neoHash := e.NativeHash(t, nativenames.Neo) + + cfg := e.Chain.GetConfig() + candidatesCount := cfg.GetCommitteeSize(0) - 1 + + // Register a set of candidates and vote for them. + voters := make([]neotest.Signer, candidatesCount) + candidates := make([]neotest.Signer, candidatesCount) + for i := range candidatesCount { + voters[i] = e.NewAccount(t, 2000_0000_0000) // enough for one registration + candidates[i] = e.NewAccount(t, 2000_0000_0000) + } + + stack, err := neoCommitteeInvoker.TestInvoke(t, "getRegisterPrice") + require.NoError(t, err) + registrationPrice, err := stack.Pop().Item().TryInteger() + require.NoError(t, err) + + for range echidnaHeight { + neoValidatorsInvoker.AddNewBlock(t) // Ensure Echidna is active. + } + gasValidatorsInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas)) + txes := make([]*transaction.Transaction, 0, candidatesCount*3) + for i := range candidatesCount { + transferTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), voters[i].(neotest.SingleSigner).Account().PrivateKey().GetScriptHash(), int64(candidatesCount+1-i)*1000000, nil) + txes = append(txes, transferTx) + registerTx := gasValidatorsInvoker.WithSigners(candidates[i]).PrepareInvoke(t, "transfer", candidates[i].(neotest.SingleSigner).Account().ScriptHash(), neoHash, registrationPrice, 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) + } + + neoValidatorsInvoker.AddNewBlock(t, txes...) + for _, tx := range txes { + e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) // luckily, both `transfer` and `vote` return boolean values + } + + // Ensure NEO holds no GAS. + stack, err = gasValidatorsInvoker.TestInvoke(t, "balanceOf", neoHash) + require.NoError(t, err) + balance, err := stack.Pop().Item().TryInteger() + require.NoError(t, err) + require.Equal(t, 0, balance.Sign()) + + var expected = make([]stackitem.Item, candidatesCount) + for i := range expected { + pub := candidates[i].(neotest.SingleSigner).Account().PublicKey().Bytes() + v := stackitem.NewBigInteger(big.NewInt(int64(candidatesCount-i+1) * 1000000)) + expected[i] = stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(pub), + v, + }) + neoCommitteeInvoker.Invoke(t, v, "getCandidateVote", pub) + } + + slices.SortFunc(expected, func(a, b stackitem.Item) int { + return bytes.Compare(a.Value().([]stackitem.Item)[0].Value().([]byte), b.Value().([]stackitem.Item)[0].Value().([]byte)) + }) + + neoCommitteeInvoker.Invoke(t, stackitem.NewArray(expected), "getCandidates") + + // Invalid cases. + var newCand = voters[0] + + // Missing data. + gasValidatorsInvoker.WithSigners(newCand).InvokeFail(t, "invalid conversion", "transfer", newCand.(neotest.SingleSigner).Account().ScriptHash(), neoHash, registrationPrice, nil) + // Invalid data. + gasValidatorsInvoker.WithSigners(newCand).InvokeFail(t, "unexpected EOF", "transfer", newCand.(neotest.SingleSigner).Account().ScriptHash(), neoHash, registrationPrice, []byte{2, 2, 2}) + // NEO transfer. + neoValidatorsInvoker.WithSigners(newCand).InvokeFail(t, "only GAS is accepted", "transfer", newCand.(neotest.SingleSigner).Account().ScriptHash(), neoHash, 1, newCand.(neotest.SingleSigner).Account().PublicKey().Bytes()) + // Incorrect amount. + gasValidatorsInvoker.WithSigners(newCand).InvokeFail(t, "incorrect GAS amount", "transfer", newCand.(neotest.SingleSigner).Account().ScriptHash(), neoHash, 1, newCand.(neotest.SingleSigner).Account().PublicKey().Bytes()) + // Incorrect witness. + var anotherAcc = e.NewAccount(t, 2000_0000_0000) + gasValidatorsInvoker.WithSigners(newCand).InvokeFail(t, "not witnessed by the key owner", "transfer", newCand.(neotest.SingleSigner).Account().ScriptHash(), neoHash, registrationPrice, anotherAcc.(neotest.SingleSigner).Account().PublicKey().Bytes()) +}