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 Nov 24, 2024
1 parent df5946d commit d4d1ff2
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 0 deletions.
35 changes: 35 additions & 0 deletions pkg/core/native/native_neo.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,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.AllowCall|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 @@ -828,6 +835,34 @@ func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stac
return stackitem.NewBool(err == nil)
}

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])
)

if caller != n.GAS.Hash {
panic("only GAS is accepted")
}
if !amount.IsInt64() || amount.Int64() != n.getRegisterPriceInternal(ic.DAO) {
panic(fmt.Errorf("incorrect GAS amount for registration (expected %d)", n.getRegisterPriceInternal(ic.DAO)))
}
ok, err := runtime.CheckKeyedWitness(ic, pub)
if err != nil {
panic(err)

Check warning on line 854 in pkg/core/native/native_neo.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/native/native_neo.go#L854

Added line #L854 was not covered by tests
} else if !ok {
panic("not witnessed by the key owner")
}
n.GAS.burn(ic, n.Hash, amount)
err = n.RegisterCandidateInternal(ic, pub)
if err != nil {
panic(err)

Check warning on line 861 in pkg/core/native/native_neo.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/native/native_neo.go#L861

Added line #L861 was not covered by tests
}
return stackitem.Null{}
}

// RegisterCandidateInternal registers pub as a new candidate.
func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error {
var emitEvent = true
Expand Down
78 changes: 78 additions & 0 deletions pkg/core/native/native_test/neo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -889,3 +889,81 @@ func TestNEO_GetCandidates(t *testing.T) {
neoCommitteeInvoker.Invoke(t, expected, "getCandidates")
checkGetAllCandidates(t, expected)
}

func TestNEO_RegisterViaNEP27(t *testing.T) {
neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000)
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)

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 d4d1ff2

Please sign in to comment.