Skip to content

Commit

Permalink
native: add candidate registration via onNEP17Payment
Browse files Browse the repository at this point in the history
Solves two problems:
 * inability to estimate GAS needed for registerCandidate in a regular way
   because of its very high fee (more than what normal RPC servers allow)
 * inability to have MaxBlockSystemFee lower than the registration price
   which is very high on its own (more than practically possible to execute)

See neo-project/neo#3552.

Signed-off-by: Roman Khimov <[email protected]>
  • Loading branch information
roman-khimov committed Dec 22, 2024
1 parent 9928907 commit 5c28954
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <br> https://github.com/neo-project/neo/pull/2881 <br> https://github.com/nspcc-dev/neo-go/pull/3080 <br> https://github.com/neo-project/neo/pull/2883 <br> https://github.com/nspcc-dev/neo-go/pull/3085 <br> 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 <br> https://github.com/neo-project/neo/pull/2942 <br> https://github.com/nspcc-dev/neo-go/pull/3301 <br> https://github.com/neo-project/neo/pull/2925 <br> https://github.com/nspcc-dev/neo-go/pull/3362 <br> 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 <br> https://github.com/neo-project/neo/pull/3290 <br> https://github.com/nspcc-dev/neo-go/pull/3473 <br> https://github.com/neo-project/neo/pull/3290 <br> https://github.com/neo-project/neo/pull/3301 <br> 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 <br> https://github.com/neo-project/neo/pull/3597 <br> https://github.com/nspcc-dev/neo-go/pull/3700 |


## DB compatibility
Expand Down
54 changes: 49 additions & 5 deletions pkg/core/native/native_neo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -815,17 +824,52 @@ 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])
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")
}
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 stackitem.NewBool(false)
return errRegistrationNotWitnessed
}
if !ic.VM.AddGas(n.getRegisterPriceInternal(ic.DAO)) {
panic("insufficient gas")
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")
}
err = n.RegisterCandidateInternal(ic, pub)
return stackitem.NewBool(err == nil)
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.
Expand Down
112 changes: 99 additions & 13 deletions pkg/core/native/native_test/neo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
}

0 comments on commit 5c28954

Please sign in to comment.