From 17842dabd6f0271d20217006e3651e28900063df Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 19 Nov 2020 13:00:46 +0300 Subject: [PATCH] core: implement native Notary contract --- pkg/core/blockchain.go | 3 + pkg/core/blockchain_test.go | 182 +++++++++++++++- pkg/core/helper_test.go | 61 +++++- pkg/core/native/contract.go | 18 +- pkg/core/native/native_nep17.go | 13 ++ pkg/core/native/notary.go | 357 ++++++++++++++++++++++++++++++++ pkg/core/native/util.go | 12 ++ pkg/core/native_notary_test.go | 298 ++++++++++++++++++++++++++ pkg/core/native_policy_test.go | 94 +++------ pkg/core/state/deposit.go | 26 +++ 10 files changed, 981 insertions(+), 83 deletions(-) create mode 100644 pkg/core/native/notary.go create mode 100644 pkg/core/native_notary_test.go create mode 100644 pkg/core/state/deposit.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 6a3aeef5d..9939cdf6f 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1388,6 +1388,9 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if !bc.config.P2PSigExtensions { return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) } + if !tx.HasSigner(bc.contracts.Notary.Hash) { + return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute) + } default: if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound { return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index e771b0135..c32467401 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -286,7 +286,7 @@ func TestVerifyTx(t *testing.T) { require.Equal(t, 1, len(aer)) require.Equal(t, aer[0].VMState, vm.HaltState) - res, err := invokeNativePolicyMethod(bc, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) + res, err := invokeContractMethod(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) @@ -720,6 +720,23 @@ func TestVerifyTx(t *testing.T) { }) }) t.Run("NotaryAssisted", func(t *testing.T) { + notary, err := wallet.NewAccount() + require.NoError(t, err) + txSetNotary := transaction.New(netmode.UnitTestNet, []byte{}, 0) + setSigner(txSetNotary, testchain.CommitteeScriptHash()) + txSetNotary.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(txSetNotary.GetSignedPart()), + VerificationScript: testchain.CommitteeVerificationScript(), + }} + bl := block.New(netmode.UnitTestNet, false) + bl.Index = bc.BlockHeight() + 1 + ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetNotary) + ic.SpawnVM() + ic.VM.LoadScript([]byte{byte(opcode.RET)}) + require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, native.RoleP2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()})) + require.NoError(t, bc.contracts.Designate.OnPersistEnd(ic.DAO)) + _, err = ic.DAO.Persist() + require.NoError(t, err) getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction { tx := bc.newTestTx(h, testScript) tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ @@ -730,34 +747,177 @@ func TestVerifyTx(t *testing.T) { tx.Signers = []transaction.Signer{{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.None, - }} + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) size := io.GetVarSize(tx) netFee, sizeDelta := fee.Calculate(rawScript) tx.NetworkFee += netFee tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() data := tx.GetSignedPart() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(data), - VerificationScript: rawScript, - }} + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: rawScript, + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } return tx } t.Run("Disabled", func(t *testing.T) { bc.config.P2PSigExtensions = false tx := getNotaryAssistedTx(0, 0) - require.Error(t, bc.VerifyTx(tx)) + require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) }) t.Run("Enabled, insufficient network fee", func(t *testing.T) { bc.config.P2PSigExtensions = true tx := getNotaryAssistedTx(1, 0) require.Error(t, bc.VerifyTx(tx)) }) - t.Run("Enabled, positive", func(t *testing.T) { + t.Run("Test verify", func(t *testing.T) { bc.config.P2PSigExtensions = true - tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) - require.NoError(t, bc.VerifyTx(tx)) + t.Run("no NotaryAssisted attribute", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Attributes = []transaction.Attribute{} + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("no deposit", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary signer scope", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.CalledByEntry, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("not signed by Notary", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary node witness", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + acc, err := keys.NewPrivateKey() + require.NoError(t, err) + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("missing payer", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + require.NoError(t, bc.VerifyTx(tx)) + }) }) }) }) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 50323e3e7..d9c854164 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -18,14 +18,18 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "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/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -364,9 +368,9 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, bc.AddBlock(b)) } -func newNEP17Transfer(sc, from, to util.Uint160, amount int64) *transaction.Transaction { +func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", from, to, amount, nil) + emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", from, to, amount, additionalArgs) emit.Opcodes(w.BinWriter, opcode.ASSERT) script := w.Bytes() @@ -411,3 +415,56 @@ func addNetworkFee(bc *Blockchain, tx *transaction.Transaction, sender *wallet.A tx.NetworkFee += int64(size) * bc.FeePerByte() return nil } + +func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, hash, method, args...) + if w.Err != nil { + return nil, w.Err + } + script := w.Bytes() + tx := transaction.New(chain.GetConfig().Magic, script, sysfee) + tx.ValidUntilBlock = chain.blockHeight + 1 + addSigners(tx) + err := testchain.SignTx(chain, tx) + if err != nil { + return nil, err + } + b := chain.newBlock(tx) + err = chain.AddBlock(b) + if err != nil { + return nil, err + } + + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) + if err != nil { + return nil, err + } + return &res[0], nil +} + +func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { + transferTx := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...) + transferTx.SystemFee = 100000000 + transferTx.ValidUntilBlock = chain.BlockHeight() + 1 + addSigners(transferTx) + require.NoError(t, testchain.SignTx(chain, transferTx)) + b := chain.newBlock(transferTx) + require.NoError(t, chain.AddBlock(b)) + return transferTx +} + +func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { + require.Equal(t, vm.HaltState, result.VMState) + require.Equal(t, 1, len(result.Stack)) + require.Equal(t, expected, result.Stack[0]) +} + +func checkFAULTState(t *testing.T, result *state.AppExecResult) { + require.Equal(t, vm.FaultState, result.VMState) +} + +func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) { + balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ContractID] + require.Equal(t, int64(expected), balance.Balance.Int64()) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 007ff8a1e..29f7e58b7 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -12,6 +12,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) +// reservedContractID represents the upper bound of the reserved IDs for native contracts. +const reservedContractID = -100 + // Contracts is a set of registered native contracts. type Contracts struct { NEO *NEO @@ -19,6 +22,7 @@ type Contracts struct { Policy *Policy Oracle *Oracle Designate *Designate + Notary *Notary Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -47,8 +51,8 @@ func (cs *Contracts) ByName(name string) interop.Contract { return nil } -// NewContracts returns new set of native contracts with new GAS, NEO and Policy -// contracts. +// NewContracts returns new set of native contracts with new GAS, NEO, Policy, Oracle, +// Designate and (optional) Notary contracts. func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { cs := new(Contracts) @@ -78,6 +82,14 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { cs.Oracle.Desig = desig cs.Contracts = append(cs.Contracts, desig) + if p2pSigExtensionsEnabled { + notary := newNotary() + notary.GAS = gas + notary.Desig = desig + cs.Notary = notary + cs.Contracts = append(cs.Contracts, notary) + } + return cs } @@ -114,7 +126,7 @@ func (cs *Contracts) GetPostPersistScript() []byte { md := cs.Contracts[i].Metadata() // Not every contract is persisted: // https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103 - if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID { + if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID || md.ContractID == notaryContractID { continue } emit.Int(w.BinWriter, 0) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index ddb24479a..c8ddc6a72 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -3,6 +3,7 @@ package native import ( "errors" "fmt" + "math" "math/big" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -320,6 +321,18 @@ func toUint160(s stackitem.Item) util.Uint160 { return u } +func toUint32(s stackitem.Item) uint32 { + bigInt := toBigInt(s) + if !bigInt.IsInt64() { + panic("bigint is not an int64") + } + int64Value := bigInt.Int64() + if int64Value < 0 || int64Value > math.MaxUint32 { + panic("bigint does not fit into uint32") + } + return uint32(int64Value) +} + func getOnPersistWrapper(f func(ic *interop.Context) error) interop.Method { return func(ic *interop.Context, _ []stackitem.Item) stackitem.Item { err := f(ic) diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go new file mode 100644 index 000000000..9282c5211 --- /dev/null +++ b/pkg/core/native/notary.go @@ -0,0 +1,357 @@ +package native + +import ( + "errors" + "fmt" + "math" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Notary represents Notary native contract. +type Notary struct { + interop.ContractMD + GAS *GAS + Desig *Designate +} + +const ( + notaryName = "Notary" + notaryContractID = reservedContractID - 1 + + // prefixDeposit is a prefix for storing Notary deposits. + prefixDeposit = 1 +) + +// newNotary returns Notary native contract. +func newNotary() *Notary { + n := &Notary{ContractMD: *interop.NewContractMD(notaryName)} + n.ContractID = notaryContractID + + desc := newDescriptor("onPayment", smartcontract.VoidType, + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("data", smartcontract.AnyType)) + md := newMethodAndPrice(n.onPayment, 100_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("lockDepositUntil", smartcontract.BoolType, + manifest.NewParameter("address", smartcontract.Hash160Type), + manifest.NewParameter("till", smartcontract.IntegerType)) + md = newMethodAndPrice(n.lockDepositUntil, 100_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("withdraw", smartcontract.BoolType, + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("to", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.withdraw, 100_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("balanceOf", smartcontract.IntegerType, + manifest.NewParameter("addr", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.balanceOf, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("expirationOf", smartcontract.IntegerType, + manifest.NewParameter("addr", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.expirationOf, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("verify", smartcontract.BoolType, + manifest.NewParameter("signature", smartcontract.SignatureType)) + md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, false) + + desc = newDescriptor("onPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, false) + + desc = newDescriptor("postPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, false) + + return n +} + +// Metadata implements Contract interface. +func (n *Notary) Metadata() *interop.ContractMD { + return &n.ContractMD +} + +// Initialize initializes Notary native contract and implements Contract interface. +func (n *Notary) Initialize(ic *interop.Context) error { + return nil +} + +// OnPersist implements Contract interface. +func (n *Notary) OnPersist(ic *interop.Context) error { + var ( + nFees int64 + notaries keys.PublicKeys + err error + ) + for _, tx := range ic.Block.Transactions { + if tx.HasAttribute(transaction.NotaryAssistedT) { + if notaries == nil { + notaries, err = n.GetNotaryNodes(ic.DAO) + if err != nil { + return fmt.Errorf("failed to get notary nodes: %w", err) + } + } + nKeys := tx.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys + nFees += int64(nKeys) + 1 + if tx.Sender() == n.Hash { + payer := tx.Signers[1] + balance := n.getDepositFor(ic.DAO, payer.Account) + balance.Amount.Sub(balance.Amount, big.NewInt(tx.SystemFee+tx.NetworkFee)) + if balance.Amount.Sign() == 0 { + err := n.removeDepositFor(ic.DAO, payer.Account) + if err != nil { + return fmt.Errorf("failed to remove an empty deposit for %s from storage: %w", payer.Account.StringBE(), err) + } + } else { + err := n.putDepositFor(ic.DAO, balance, payer.Account) + if err != nil { + return fmt.Errorf("failed to update deposit for %s: %w", payer.Account.StringBE(), err) + } + } + } + } + } + if nFees == 0 { + return nil + } + singleReward := calculateNotaryReward(nFees, len(notaries)) + for _, notary := range notaries { + n.GAS.mint(ic, notary.GetScriptHash(), singleReward) + } + return nil +} + +// onPayment records deposited amount as belonging to "from" address with a lock +// till the specified chain's height. +func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + if h := ic.VM.GetCallingScriptHash(); h != n.GAS.Hash { + panic(fmt.Errorf("only GAS can be accepted for deposit, got %s", h.StringBE())) + } + from := toUint160(args[0]) + to := from + amount := toBigInt(args[1]) + data, ok := args[2].(*stackitem.Array) + if !ok || len(data.Value().([]stackitem.Item)) != 2 { + panic(errors.New("`data` parameter should be an array of 2 elements")) + } + additionalParams := data.Value().([]stackitem.Item) + if !additionalParams[0].Equals(stackitem.Null{}) { + to = toUint160(additionalParams[0]) + } + till := toUint32(additionalParams[1]) + currentHeight := ic.Chain.BlockHeight() + if till < currentHeight { + panic(fmt.Errorf("`till` shouldn't be less then the chain's height %d", currentHeight)) + } + + deposit := n.getDepositFor(ic.DAO, to) + if deposit == nil { + if amount.Cmp(big.NewInt(2*transaction.NotaryServiceFeePerKey)) < 0 { + panic(fmt.Errorf("first deposit can not be less then %d, got %d", 2*transaction.NotaryServiceFeePerKey, amount.Int64())) + } + deposit = &state.Deposit{ + Amount: new(big.Int), + } + } else { + if till < deposit.Till { + panic(fmt.Errorf("`till` shouldn't be less then the previous value %d", deposit.Till)) + } + } + deposit.Amount.Add(deposit.Amount, amount) + deposit.Till = till + + if err := n.putDepositFor(ic.DAO, deposit, to); err != nil { + panic(fmt.Errorf("failed to put deposit for %s into the storage: %w", from.StringBE(), err)) + } + return stackitem.Null{} +} + +// lockDepositUntil updates the chain's height until which deposit is locked. +func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) stackitem.Item { + addr := toUint160(args[0]) + ok, err := runtime.CheckHashedWitness(ic, addr) + if err != nil { + panic(fmt.Errorf("failed to check witness for %s: %w", addr.StringBE(), err)) + } + if !ok { + return stackitem.NewBool(false) + } + till := toUint32(args[1]) + if till < ic.Chain.BlockHeight() { + return stackitem.NewBool(false) + } + deposit := n.getDepositFor(ic.DAO, addr) + if deposit == nil { + return stackitem.NewBool(false) + } + if till < deposit.Till { + return stackitem.NewBool(false) + } + deposit.Till = till + err = n.putDepositFor(ic.DAO, deposit, addr) + if err != nil { + panic(fmt.Errorf("failed to put deposit for %s into the storage: %w", addr.StringBE(), err)) + } + return stackitem.NewBool(true) +} + +// withdraw sends all deposited GAS for "from" address to "to" address. +func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item { + from := toUint160(args[0]) + ok, err := runtime.CheckHashedWitness(ic, from) + if err != nil { + panic(fmt.Errorf("failed to check witness for %s: %w", from.StringBE(), err)) + } + if !ok { + return stackitem.NewBool(false) + } + to := from + if !args[1].Equals(stackitem.Null{}) { + to = toUint160(args[1]) + } + deposit := n.getDepositFor(ic.DAO, from) + if deposit == nil { + return stackitem.NewBool(false) + } + if ic.Chain.BlockHeight() < deposit.Till { + return stackitem.NewBool(false) + } + cs, err := ic.DAO.GetContractState(n.GAS.Hash) + if err != nil { + panic(fmt.Errorf("failed to get GAS contract state: %w", err)) + } + transferArgs := []stackitem.Item{stackitem.NewByteArray(n.Hash.BytesBE()), stackitem.NewByteArray(to.BytesBE()), stackitem.NewBigInteger(deposit.Amount), stackitem.Null{}} + err = contract.CallExInternal(ic, cs, "transfer", transferArgs, smartcontract.All, vm.EnsureIsEmpty, func(ctx *vm.Context) { // we need EnsureIsEmpty because there's a callback popping result from the stack + isTransferOk := ic.VM.Estack().Pop().Bool() + if !isTransferOk { + panic("failed to transfer GAS from Notary account") + } + }) + if err != nil { + panic(fmt.Errorf("failed to transfer GAS from Notary account: %w", err)) + } + if err := n.removeDepositFor(ic.DAO, from); err != nil { + panic(fmt.Errorf("failed to remove withdrawn deposit for %s from the storage: %w", from.StringBE(), err)) + } + return stackitem.NewBool(true) +} + +// balanceOf returns deposited GAS amount for specified address. +func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { + acc := toUint160(args[0]) + deposit := n.getDepositFor(ic.DAO, acc) + if deposit == nil { + return stackitem.NewBigInteger(big.NewInt(0)) + } + return stackitem.NewBigInteger(deposit.Amount) +} + +// expirationOf Returns deposit lock height for specified address. +func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { + acc := toUint160(args[0]) + deposit := n.getDepositFor(ic.DAO, acc) + if deposit == nil { + return stackitem.Make(0) + } + return stackitem.Make(deposit.Till) +} + +// verify checks whether the transaction was signed by one of the notaries. +func (n *Notary) verify(ic *interop.Context, args []stackitem.Item) stackitem.Item { + sig, err := args[0].TryBytes() + if err != nil { + panic(fmt.Errorf("failed to get signature bytes: %w", err)) + } + tx := ic.Tx + if len(tx.GetAttributes(transaction.NotaryAssistedT)) == 0 { + return stackitem.NewBool(false) + } + for _, signer := range tx.Signers { + if signer.Account == n.Hash { + if signer.Scopes != transaction.None { + return stackitem.NewBool(false) + } + } + } + if tx.Sender() == n.Hash { + if len(tx.Signers) != 2 { + return stackitem.NewBool(false) + } + payer := tx.Signers[1].Account + balance := n.getDepositFor(ic.DAO, payer) + if balance == nil || balance.Amount.Cmp(big.NewInt(tx.NetworkFee+tx.SystemFee)) < 0 { + return stackitem.NewBool(false) + } + } + notaries, err := n.GetNotaryNodes(ic.DAO) + if err != nil { + panic(fmt.Errorf("failed to get notary nodes: %w", err)) + } + hash := tx.GetSignedHash().BytesBE() + var verified bool + for _, n := range notaries { + if n.Verify(sig, hash) { + verified = true + break + } + } + return stackitem.NewBool(verified) +} + +// GetNotaryNodes returns public keys of notary nodes. +func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) { + nodes, _, err := n.Desig.GetDesignatedByRole(d, RoleP2PNotary, math.MaxUint32) + return nodes, err +} + +// getDepositFor returns state.Deposit for the account specified. It returns nil in case if +// deposit is not found in storage and panics in case of any other error. +func (n *Notary) getDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit { + key := append([]byte{prefixDeposit}, acc.BytesBE()...) + deposit := new(state.Deposit) + err := getSerializableFromDAO(n.ContractID, dao, key, deposit) + if err == nil { + return deposit + } + if err == storage.ErrKeyNotFound { + return nil + } + panic(fmt.Errorf("failed to get deposit for %s from storage: %w", acc.StringBE(), err)) +} + +// putDepositFor puts deposit on the balance of the specified account in the storage. +func (n *Notary) putDepositFor(dao dao.DAO, deposit *state.Deposit, acc util.Uint160) error { + key := append([]byte{prefixDeposit}, acc.BytesBE()...) + return putSerializableToDAO(n.ContractID, dao, key, deposit) +} + +// removeDepositFor removes deposit from the storage. +func (n *Notary) removeDepositFor(dao dao.DAO, acc util.Uint160) error { + key := append([]byte{prefixDeposit}, acc.BytesBE()...) + return dao.DeleteStorageItem(n.ContractID, key) +} + +// calculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count. +func calculateNotaryReward(nFees int64, notariesCount int) *big.Int { + return big.NewInt(nFees * transaction.NotaryServiceFeePerKey / int64(notariesCount)) +} diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go index 293581c34..2ca752105 100644 --- a/pkg/core/native/util.go +++ b/pkg/core/native/util.go @@ -2,6 +2,7 @@ package native import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/io" ) @@ -15,3 +16,14 @@ func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializabl item.DecodeBinary(r) return r.Err } + +func putSerializableToDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error { + w := io.NewBufBinWriter() + item.EncodeBinary(w.BinWriter) + if w.Err != nil { + return w.Err + } + return d.PutStorageItem(id, key, &state.StorageItem{ + Value: w.Bytes(), + }) +} diff --git a/pkg/core/native_notary_test.go b/pkg/core/native_notary_test.go new file mode 100644 index 000000000..33d9703a6 --- /dev/null +++ b/pkg/core/native_notary_test.go @@ -0,0 +1,298 @@ +package core + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "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/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func TestNotaryContractPipeline(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + notaryHash := chain.contracts.Notary.Hash + gasHash := chain.contracts.GAS.Hash + depositLock := 30 + + // check Notary contract has no GAS on the account + checkBalanceOf(t, chain, notaryHash, 0) + + // `balanceOf`: check multisig account has no GAS on deposit + balance, err := invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(0)) + + // `expirationOf`: should fail to get deposit which does not exist + till, err := invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(0)) + + // `lockDepositUntil`: should fail because there's no deposit + lockDepositUntilRes, err := invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+1)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + + // `onPayment`: bad token + transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, chain.contracts.NEO.Hash, 1, nil, int64(depositLock)) + res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + + // `onPayment`: insufficient first deposit + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey-1, nil, int64(depositLock)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + + // `onPayment`: invalid `data` (missing `till` parameter) + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey-1, nil) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + + // `onPayment`: good + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey, nil, int64(depositLock)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + checkBalanceOf(t, chain, notaryHash, 2*transaction.NotaryServiceFeePerKey) + + // `expirationOf`: check `till` was set + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock)) + + // `balanceOf`: check deposited amount for the multisig account + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(2*transaction.NotaryServiceFeePerKey)) + + // `onPayment`: good second deposit and explicit `to` paramenter + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, transaction.NotaryServiceFeePerKey, testchain.MultisigScriptHash(), int64(depositLock+1)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + + // `balanceOf`: check deposited amount for the multisig account + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey)) + + // `expirationOf`: check `till` is updated. + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+1)) + + // `onPayment`: empty payment, should fail because `till` less then the previous one + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(depositLock)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+1)) + + // `onPayment`: empty payment, should fail because `till` less then the chain height + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(1)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+1)) + + // `onPayment`: empty payment, should successfully update `till` + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(depositLock+2)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: bad witness + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", util.Uint160{1, 2, 3}, int64(depositLock+5)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: bad `till` (less then the previous one) + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+1)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: bad `till` (less then the chain's height) + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(1)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: good `till` + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+3)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(true)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+3)) + + // transfer 1 GAS to the new account for the next test + acc, _ := wallet.NewAccount() + transferTx = transferTokenFromMultisigAccount(t, chain, acc.PrivateKey().PublicKey().GetScriptHash(), gasHash, 100000000) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + + // `withdraw`: bad witness + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, notaryHash, "withdraw", testchain.MultisigScriptHash(), acc.PrivateKey().PublicKey().GetScriptHash()) + require.NoError(t, w.Err) + script := w.Bytes() + withdrawTx := transaction.New(chain.GetConfig().Magic, script, 10000000) + withdrawTx.ValidUntilBlock = chain.blockHeight + 1 + withdrawTx.NetworkFee = 10000000 + withdrawTx.Signers = []transaction.Signer{ + { + Account: acc.PrivateKey().PublicKey().GetScriptHash(), + Scopes: transaction.None, + }, + } + err = acc.SignTx(withdrawTx) + require.NoError(t, err) + b := chain.newBlock(withdrawTx) + err = chain.AddBlock(b) + require.NoError(t, err) + appExecRes, err := chain.GetAppExecResults(withdrawTx.Hash(), trigger.Application) + require.NoError(t, err) + checkResult(t, &appExecRes[0], stackitem.NewBool(false)) + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey)) + + // `withdraw`: locked deposit + withdrawRes, err := invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), acc.PrivateKey().PublicKey().GetScriptHash()) + require.NoError(t, err) + checkResult(t, withdrawRes, stackitem.NewBool(false)) + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey)) + + // `withdraw`: unlock deposit and transfer GAS back to owner + chain.genBlocks(depositLock) + withdrawRes, err = invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, withdrawRes, stackitem.NewBool(true)) + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(0)) + checkBalanceOf(t, chain, notaryHash, 0) + + // `withdraw`: the second time it should fail, because there's no deposit left + withdrawRes, err = invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, withdrawRes, stackitem.NewBool(false)) +} + +func TestNotaryNodesReward(t *testing.T) { + checkReward := func(nKeys int, nNotaryNodes int, spendFullDeposit bool) { + chain := newTestChain(t) + defer chain.Close() + notaryHash := chain.contracts.Notary.Hash + gasHash := chain.contracts.GAS.Hash + signer := testchain.MultisigScriptHash() + var err error + + // set Notary nodes and check their balance + notaryNodes := make([]*keys.PrivateKey, nNotaryNodes) + notaryNodesPublicKeys := make(keys.PublicKeys, nNotaryNodes) + for i := range notaryNodes { + notaryNodes[i], err = keys.NewPrivateKey() + require.NoError(t, err) + notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey() + } + chain.setNodesByRole(t, true, native.RoleP2PNotary, notaryNodesPublicKeys) + for _, notaryNode := range notaryNodesPublicKeys { + checkBalanceOf(t, chain, notaryNode.GetScriptHash(), 0) + } + + // deposit GAS for `signer` with lock until the next block + depositAmount := 100_0000 + (2+int64(nKeys))*transaction.NotaryServiceFeePerKey // sysfee + netfee of the next transaction + if !spendFullDeposit { + depositAmount += 1_0000 + } + transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, depositAmount, signer, int64(chain.BlockHeight()+1)) + res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + + // send transaction with Notary contract as a sender + tx := chain.newTestTx(util.Uint160{}, []byte{byte(opcode.PUSH1)}) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}}) + tx.NetworkFee = (2 + int64(nKeys)) * transaction.NotaryServiceFeePerKey + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: signer, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notaryNodes[0].Sign(data)...), + }, + { + InvocationScript: testchain.Sign(data), + VerificationScript: testchain.MultisigVerificationScript(), + }, + } + b := chain.newBlock(tx) + require.NoError(t, chain.AddBlock(b)) + checkBalanceOf(t, chain, notaryHash, int(depositAmount-tx.SystemFee-tx.NetworkFee)) + for _, notaryNode := range notaryNodesPublicKeys { + checkBalanceOf(t, chain, notaryNode.GetScriptHash(), transaction.NotaryServiceFeePerKey*(nKeys+1)/nNotaryNodes) + } + } + + for _, spendDeposit := range []bool{true, false} { + checkReward(0, 1, spendDeposit) + checkReward(0, 2, spendDeposit) + checkReward(1, 1, spendDeposit) + checkReward(1, 2, spendDeposit) + checkReward(1, 3, spendDeposit) + checkReward(5, 1, spendDeposit) + checkReward(5, 2, spendDeposit) + checkReward(5, 6, spendDeposit) + checkReward(5, 7, spendDeposit) + } +} diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index 496c6b1b5..b1fb38eca 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -5,18 +5,11 @@ import ( "testing" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/native" - "github.com/nspcc-dev/neo-go/pkg/core/state" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/network/payload" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -24,6 +17,7 @@ import ( func TestMaxTransactionsPerBlock(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao) @@ -31,14 +25,14 @@ func TestMaxTransactionsPerBlock(t *testing.T) { }) t.Run("get, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getMaxTransactionsPerBlock") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxTransactionsPerBlock") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(512))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) @@ -47,7 +41,7 @@ func TestMaxTransactionsPerBlock(t *testing.T) { }) t.Run("set, too big value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(block.MaxContentsPerBlock))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(block.MaxContentsPerBlock))) require.NoError(t, err) checkFAULTState(t, res) }) @@ -56,6 +50,7 @@ func TestMaxTransactionsPerBlock(t *testing.T) { func TestMaxBlockSize(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetMaxBlockSizeInternal(chain.dao) @@ -63,25 +58,25 @@ func TestMaxBlockSize(t *testing.T) { }) t.Run("get, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getMaxBlockSize") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSize") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1024*256))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) - res, err = invokeNativePolicyMethod(chain, "getMaxBlockSize") + res, err = invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSize") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(102400))) require.NoError(t, chain.persist()) }) t.Run("set, too big value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(payload.MaxSize+1))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(payload.MaxSize+1))) require.NoError(t, err) checkFAULTState(t, res) }) @@ -90,6 +85,7 @@ func TestMaxBlockSize(t *testing.T) { func TestFeePerByte(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) @@ -97,14 +93,14 @@ func TestFeePerByte(t *testing.T) { }) t.Run("get, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getFeePerByte") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getFeePerByte") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1000))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) @@ -113,13 +109,13 @@ func TestFeePerByte(t *testing.T) { }) t.Run("set, negative value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(-1))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(-1))) require.NoError(t, err) checkFAULTState(t, res) }) t.Run("set, too big value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(100_000_000+1))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(100_000_000+1))) require.NoError(t, err) checkFAULTState(t, res) }) @@ -128,6 +124,7 @@ func TestFeePerByte(t *testing.T) { func TestBlockSystemFee(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetMaxBlockSystemFeeInternal(chain.dao) @@ -135,24 +132,24 @@ func TestBlockSystemFee(t *testing.T) { }) t.Run("get", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getMaxBlockSystemFee") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSystemFee") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(9000*native.GASFactor))) require.NoError(t, chain.persist()) }) t.Run("set, too low fee", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(4007600))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(4007600))) require.NoError(t, err) checkFAULTState(t, res) }) t.Run("set, success", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) - res, err = invokeNativePolicyMethod(chain, "getMaxBlockSystemFee") + res, err = invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSystemFee") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000))) require.NoError(t, chain.persist()) @@ -163,6 +160,7 @@ func TestBlockedAccounts(t *testing.T) { chain := newTestChain(t) defer chain.Close() account := util.Uint160{1, 2, 3} + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("isBlocked, internal method", func(t *testing.T) { isBlocked := chain.contracts.Policy.IsBlockedInternal(chain.dao, random.Uint160()) @@ -170,14 +168,14 @@ func TestBlockedAccounts(t *testing.T) { }) t.Run("isBlocked, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "isBlocked", random.Uint160()) + res, err := invokeContractMethod(chain, 100000000, policyHash, "isBlocked", random.Uint160()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) }) t.Run("block-unblock account", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + res, err := invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) @@ -185,7 +183,7 @@ func TestBlockedAccounts(t *testing.T) { require.Equal(t, isBlocked, true) require.NoError(t, chain.persist()) - res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) @@ -196,65 +194,27 @@ func TestBlockedAccounts(t *testing.T) { t.Run("double-block", func(t *testing.T) { // block - res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + res, err := invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) // double-block should fail - res, err = invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) // unblock - res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) // unblock the same account should fail as we don't have it blocked - res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) }) } - -func invokeNativePolicyMethod(chain *Blockchain, method string, args ...interface{}) (*state.AppExecResult, error) { - w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, chain.contracts.Policy.Metadata().Hash, method, args...) - if w.Err != nil { - return nil, w.Err - } - script := w.Bytes() - tx := transaction.New(chain.GetConfig().Magic, script, 10000000) - validUntil := chain.blockHeight + 1 - tx.ValidUntilBlock = validUntil - addSigners(tx) - err := testchain.SignTx(chain, tx) - if err != nil { - return nil, err - } - b := chain.newBlock(tx) - err = chain.AddBlock(b) - if err != nil { - return nil, err - } - - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) - if err != nil { - return nil, err - } - return &res[0], nil -} - -func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { - require.Equal(t, vm.HaltState, result.VMState) - require.Equal(t, 1, len(result.Stack)) - require.Equal(t, expected, result.Stack[0]) -} - -func checkFAULTState(t *testing.T, result *state.AppExecResult) { - require.Equal(t, vm.FaultState, result.VMState) -} diff --git a/pkg/core/state/deposit.go b/pkg/core/state/deposit.go new file mode 100644 index 000000000..8e45e9595 --- /dev/null +++ b/pkg/core/state/deposit.go @@ -0,0 +1,26 @@ +package state + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" + "github.com/nspcc-dev/neo-go/pkg/io" +) + +// Deposit represents GAS deposit from Notary contract. +type Deposit struct { + Amount *big.Int + Till uint32 +} + +// EncodeBinary implements io.Serializable interface. +func (d *Deposit) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes(bigint.ToBytes(d.Amount)) + w.WriteU32LE(d.Till) +} + +// DecodeBinary implements io.Serializable interface. +func (d *Deposit) DecodeBinary(r *io.BinReader) { + d.Amount = bigint.FromBytes(r.ReadVarBytes()) + d.Till = r.ReadU32LE() +}