From 1b6f4051d8ec7378a5e192b8a63ecdb6f3175d49 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 25 Jul 2022 15:46:20 +0300 Subject: [PATCH 1/4] smartcontract: move CreateCallAndUnwrapIteratorScript there RPC client shouldn't build scripts and this function can be useful as a reusable building block. --- pkg/rpcclient/helper.go | 66 +------------------------------- pkg/smartcontract/entry.go | 78 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 65 deletions(-) create mode 100644 pkg/smartcontract/entry.go diff --git a/pkg/rpcclient/helper.go b/pkg/rpcclient/helper.go index 5f1614585..3db0052d3 100644 --- a/pkg/rpcclient/helper.go +++ b/pkg/rpcclient/helper.go @@ -6,17 +6,12 @@ import ( "fmt" "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/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/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" - "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" ) @@ -121,72 +116,13 @@ func (c *Client) InvokeAndPackIteratorResults(contract util.Uint160, operation s if len(maxIteratorResultItems) != 0 { max = maxIteratorResultItems[0] } - bytes, err := createIteratorUnwrapperScript(contract, operation, params, max) + bytes, err := smartcontract.CreateCallAndUnwrapIteratorScript(contract, operation, params, max) if err != nil { return nil, fmt.Errorf("failed to create iterator unwrapper script: %w", err) } return c.InvokeScript(bytes, signers) } -func createIteratorUnwrapperScript(contract util.Uint160, operation string, params []smartcontract.Parameter, maxIteratorResultItems int) ([]byte, error) { - script := io.NewBufBinWriter() - emit.Int(script.BinWriter, int64(maxIteratorResultItems)) - // Pack arguments for System.Contract.Call. - arr, err := smartcontract.ExpandParameterToEmitable(smartcontract.Parameter{ - Type: smartcontract.ArrayType, - Value: params, - }) - if err != nil { - return nil, fmt.Errorf("failed to expand parameters array to emitable: %w", err) - } - emit.Array(script.BinWriter, arr.([]interface{})...) - emit.AppCallNoArgs(script.BinWriter, contract, operation, callflag.All) // The System.Contract.Call itself, it will push Iterator on estack. - emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) // Push new empty array to estack. This array will store iterator's elements. - - // Start the iterator traversal cycle. - iteratorTraverseCycleStartOffset := script.Len() - emit.Opcodes(script.BinWriter, opcode.OVER) // Load iterator from 1-st cell of estack. - emit.Syscall(script.BinWriter, interopnames.SystemIteratorNext) // Call System.Iterator.Next, it will pop the iterator from estack and push `true` or `false` to estack. - jmpIfNotOffset := script.Len() - emit.Instruction(script.BinWriter, opcode.JMPIFNOT, // Pop boolean value (from the previous step) from estack, if `false`, then iterator has no more items => jump to the end of program. - []byte{ - 0x00, // jump to loadResultOffset, but we'll fill this byte after script creation. - }) - emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack. - opcode.PUSH2, opcode.PICK) // Pick iterator from the 2-nd cell of estack. - emit.Syscall(script.BinWriter, interopnames.SystemIteratorValue) // Call System.Iterator.Value, it will pop the iterator from estack and push its current value to estack. - emit.Opcodes(script.BinWriter, opcode.APPEND) // Pop iterator value and the resulting array from estack. Append value to the resulting array. Array is a reference type, thus, value stored at the 1-th cell of local slot will also be updated. - emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack. - opcode.SIZE, // Pop array from estack and push its size to estack. - opcode.PUSH3, opcode.PICK, // Pick maxIteratorResultItems from the 3-d cell of estack. - opcode.GE) // Compare len(arr) and maxIteratorResultItems - jmpIfMaxReachedOffset := script.Len() - emit.Instruction(script.BinWriter, opcode.JMPIF, // Pop boolean value (from the previous step) from estack, if `false`, then max array elements is reached => jump to the end of program. - []byte{ - 0x00, // jump to loadResultOffset, but we'll fill this byte after script creation. - }) - jmpOffset := script.Len() - emit.Instruction(script.BinWriter, opcode.JMP, // Jump to the start of iterator traverse cycle. - []byte{ - uint8(iteratorTraverseCycleStartOffset - jmpOffset), // jump to iteratorTraverseCycleStartOffset; offset is relative to JMP position. - }) - - // End of the program: push the result on stack and return. - loadResultOffset := script.Len() - emit.Opcodes(script.BinWriter, opcode.NIP, // Remove iterator from the 1-st cell of estack - opcode.NIP) // Remove maxIteratorResultItems from the 1-st cell of estack, so that only resulting array is left on estack. - if err := script.Err; err != nil { - return nil, fmt.Errorf("failed to build iterator unwrapper script: %w", err) - } - - // Fill in JMPIFNOT instruction parameter. - bytes := script.Bytes() - bytes[jmpIfNotOffset+1] = uint8(loadResultOffset - jmpIfNotOffset) // +1 is for JMPIFNOT itself; offset is relative to JMPIFNOT position. - // Fill in jmpIfMaxReachedOffset instruction parameter. - bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position. - return bytes, nil -} - // topIterableFromStack returns the list of elements of `resultItemType` type from the top element // of the provided stack. The top element is expected to be an Array, otherwise an error is returned. func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]interface{}, error) { diff --git a/pkg/smartcontract/entry.go b/pkg/smartcontract/entry.go new file mode 100644 index 000000000..fda82c0b1 --- /dev/null +++ b/pkg/smartcontract/entry.go @@ -0,0 +1,78 @@ +package smartcontract + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" +) + +// CreateCallAndUnwrapIteratorScript creates a script that calls 'operation' method +// of the 'contract' with the specified arguments. This method is expected to return +// an iterator that then is traversed (using iterator.Next) with values (iterator.Value) +// extracted and added into array. At most maxIteratorResultItems number of items is +// processed this way (and this number can't exceed VM limits), the result of the +// script is an array containing extracted value elements. This script can be useful +// for interactions with RPC server that have iterator sessions disabled. +func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string, params []Parameter, maxIteratorResultItems int) ([]byte, error) { + script := io.NewBufBinWriter() + emit.Int(script.BinWriter, int64(maxIteratorResultItems)) + // Pack arguments for System.Contract.Call. + arr, err := ExpandParameterToEmitable(Parameter{ + Type: ArrayType, + Value: params, + }) + if err != nil { + return nil, fmt.Errorf("expanding params to emitable: %w", err) + } + emit.Array(script.BinWriter, arr.([]interface{})...) + emit.AppCallNoArgs(script.BinWriter, contract, operation, callflag.All) // The System.Contract.Call itself, it will push Iterator on estack. + emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) // Push new empty array to estack. This array will store iterator's elements. + + // Start the iterator traversal cycle. + iteratorTraverseCycleStartOffset := script.Len() + emit.Opcodes(script.BinWriter, opcode.OVER) // Load iterator from 1-st cell of estack. + emit.Syscall(script.BinWriter, interopnames.SystemIteratorNext) // Call System.Iterator.Next, it will pop the iterator from estack and push `true` or `false` to estack. + jmpIfNotOffset := script.Len() + emit.Instruction(script.BinWriter, opcode.JMPIFNOT, // Pop boolean value (from the previous step) from estack, if `false`, then iterator has no more items => jump to the end of program. + []byte{ + 0x00, // jump to loadResultOffset, but we'll fill this byte after script creation. + }) + emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack. + opcode.PUSH2, opcode.PICK) // Pick iterator from the 2-nd cell of estack. + emit.Syscall(script.BinWriter, interopnames.SystemIteratorValue) // Call System.Iterator.Value, it will pop the iterator from estack and push its current value to estack. + emit.Opcodes(script.BinWriter, opcode.APPEND) // Pop iterator value and the resulting array from estack. Append value to the resulting array. Array is a reference type, thus, value stored at the 1-th cell of local slot will also be updated. + emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack. + opcode.SIZE, // Pop array from estack and push its size to estack. + opcode.PUSH3, opcode.PICK, // Pick maxIteratorResultItems from the 3-d cell of estack. + opcode.GE) // Compare len(arr) and maxIteratorResultItems + jmpIfMaxReachedOffset := script.Len() + emit.Instruction(script.BinWriter, opcode.JMPIF, // Pop boolean value (from the previous step) from estack, if `false`, then max array elements is reached => jump to the end of program. + []byte{ + 0x00, // jump to loadResultOffset, but we'll fill this byte after script creation. + }) + jmpOffset := script.Len() + emit.Instruction(script.BinWriter, opcode.JMP, // Jump to the start of iterator traverse cycle. + []byte{ + uint8(iteratorTraverseCycleStartOffset - jmpOffset), // jump to iteratorTraverseCycleStartOffset; offset is relative to JMP position. + }) + + // End of the program: push the result on stack and return. + loadResultOffset := script.Len() + emit.Opcodes(script.BinWriter, opcode.NIP, // Remove iterator from the 1-st cell of estack + opcode.NIP) // Remove maxIteratorResultItems from the 1-st cell of estack, so that only resulting array is left on estack. + if err := script.Err; err != nil { + return nil, fmt.Errorf("emitting iterator unwrapper script: %w", err) + } + + // Fill in JMPIFNOT instruction parameter. + bytes := script.Bytes() + bytes[jmpIfNotOffset+1] = uint8(loadResultOffset - jmpIfNotOffset) // +1 is for JMPIFNOT itself; offset is relative to JMPIFNOT position. + // Fill in jmpIfMaxReachedOffset instruction parameter. + bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position. + return bytes, nil +} From 32ebb4a90dd95ef7e49dbf35e6ef3aff23c3696c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 25 Jul 2022 20:04:43 +0300 Subject: [PATCH 2/4] smartcontract: add Builder, method invocation helpers and doc Move the last remaining script-related things out of the rpcclient. --- pkg/rpcclient/nep11.go | 14 +++----- pkg/rpcclient/nep17.go | 17 ++++----- pkg/smartcontract/builder.go | 68 +++++++++++++++++++++++++++++++++++ pkg/smartcontract/doc.go | 8 +++++ pkg/smartcontract/doc_test.go | 49 +++++++++++++++++++++++++ pkg/smartcontract/entry.go | 9 +++++ 6 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 pkg/smartcontract/builder.go create mode 100644 pkg/smartcontract/doc.go create mode 100644 pkg/smartcontract/doc_test.go diff --git a/pkg/rpcclient/nep11.go b/pkg/rpcclient/nep11.go index 4ddc66855..0877a54ef 100644 --- a/pkg/rpcclient/nep11.go +++ b/pkg/rpcclient/nep11.go @@ -6,14 +6,10 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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/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" ) @@ -65,17 +61,15 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160, // `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string, data interface{}. func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160, gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - if w.Err != nil { - return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", w.Err) + script, err := smartcontract.CreateCallWithAssertScript(tokenHash, "transfer", args...) + if err != nil { + return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", err) } from, err := address.StringToUint160(acc.Address) if err != nil { return nil, fmt.Errorf("bad account address: %w", err) } - return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{ + return c.CreateTxFromScript(script, acc, -1, gas, append([]SignerAccount{{ Signer: transaction.Signer{ Account: from, Scopes: transaction.CalledByEntry, diff --git a/pkg/rpcclient/nep17.go b/pkg/rpcclient/nep17.go index acdff51eb..06d0a4209 100644 --- a/pkg/rpcclient/nep17.go +++ b/pkg/rpcclient/nep17.go @@ -5,12 +5,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "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/emit" - "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -79,16 +76,16 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, if err != nil { return nil, fmt.Errorf("bad account address: %w", err) } - w := io.NewBufBinWriter() + b := smartcontract.NewBuilder() for i := range recipients { - emit.AppCall(w.BinWriter, recipients[i].Token, "transfer", callflag.All, + b.InvokeWithAssert(recipients[i].Token, "transfer", from, recipients[i].Address, recipients[i].Amount, recipients[i].Data) - emit.Opcodes(w.BinWriter, opcode.ASSERT) } - if w.Err != nil { - return nil, fmt.Errorf("failed to create transfer script: %w", w.Err) + script, err := b.Script() + if err != nil { + return nil, fmt.Errorf("failed to create transfer script: %w", err) } - return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{ + return c.CreateTxFromScript(script, acc, -1, gas, append([]SignerAccount{{ Signer: transaction.Signer{ Account: from, Scopes: transaction.CalledByEntry, diff --git a/pkg/smartcontract/builder.go b/pkg/smartcontract/builder.go new file mode 100644 index 000000000..9ea69f225 --- /dev/null +++ b/pkg/smartcontract/builder.go @@ -0,0 +1,68 @@ +package smartcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" +) + +// Builder is used to create arbitrary scripts from the set of methods it provides. +// Each method emits some set of opcodes performing an action and (in most cases) +// returning a result. These chunks of code can be composed together to perform +// several actions in the same script (and therefore in the same transaction), but +// the end result (in terms of state changes and/or resulting items) of the script +// totally depends on what it contains and that's the responsibility of the Builder +// user. Builder is mostly used to create transaction scripts (also known as +// "entry scripts"), so the set of methods it exposes is tailored to this model +// of use and any calls emitted don't limit flags in any way (always use +// callflag.All). +type Builder struct { + bw *io.BufBinWriter +} + +// NewBuilder creates a new Builder instance. +func NewBuilder() *Builder { + return &Builder{bw: io.NewBufBinWriter()} +} + +// InvokeMethod is the most generic contract method invoker, the code it produces +// packs all of the arguments given into an array and calls some method of the +// contract. The correctness of this invocation (number and type of parameters) is +// out of scope of this method, as well as return value, if contract's method returns +// something this value just remains on the execution stack. +func (b *Builder) InvokeMethod(contract util.Uint160, method string, params ...interface{}) { + emit.AppCall(b.bw.BinWriter, contract, method, callflag.All, params...) +} + +// Assert emits an ASSERT opcode that expects a Boolean value to be on the stack, +// checks if it's true and aborts the transaction if it's not. +func (b *Builder) Assert() { + emit.Opcodes(b.bw.BinWriter, opcode.ASSERT) +} + +// InvokeWithAssert emits an invocation of the method (see InvokeMethod) with +// an ASSERT after the invocation. The presumption is that the method called +// returns a Boolean value signalling the success or failure of the operation. +// This pattern is pretty common, NEP-11 or NEP-17 'transfer' methods do exactly +// that as well as NEO's 'vote'. The ASSERT then allow to simplify transaction +// status checking, if action is successful then transaction is successful as +// well, if it went wrong than whole transaction fails (ends with vmstate.FAULT). +func (b *Builder) InvokeWithAssert(contract util.Uint160, method string, params ...interface{}) { + b.InvokeMethod(contract, method, params...) + b.Assert() +} + +// Script return current script, you can't use Builder after invoking this method +// unless you Reset it. +func (b *Builder) Script() ([]byte, error) { + err := b.bw.Err + return b.bw.Bytes(), err +} + +// Reset resets the Builder, allowing to reuse the same script buffer (but +// previous script will be overwritten there). +func (b *Builder) Reset() { + b.bw.Reset() +} diff --git a/pkg/smartcontract/doc.go b/pkg/smartcontract/doc.go new file mode 100644 index 000000000..d9292ae85 --- /dev/null +++ b/pkg/smartcontract/doc.go @@ -0,0 +1,8 @@ +/* +Package smartcontract contains functions to deal with widely used scripts. +Neo is all about various executed code, verifications and executions of +transactions need some NeoVM code and this package simplifies creating it +for common tasks like multisignature verification scripts or transaction +entry scripts that call previously deployed contracts. +*/ +package smartcontract diff --git a/pkg/smartcontract/doc_test.go b/pkg/smartcontract/doc_test.go new file mode 100644 index 000000000..d1b7fe8cc --- /dev/null +++ b/pkg/smartcontract/doc_test.go @@ -0,0 +1,49 @@ +package smartcontract_test + +import ( + "context" + "encoding/hex" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +func ExampleBuilder() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + neoHash, _ := c.GetNativeContractHash("NeoToken") + + pKey, _ := hex.DecodeString("03d9e8b16bd9b22d3345d6d4cde31be1c3e1d161532e3d0ccecb95ece2eb58336e") // Public key. + + b := smartcontract.NewBuilder() + // Single NEO "vote" call with a check + b.InvokeWithAssert(neoHash, "vote", pKey) + script, _ := b.Script() + + // The script can then be used to create transaction or to invoke via RPC. + res, _ := c.InvokeScript(script, []transaction.Signer{{Account: util.Uint160{0x01, 0x02, 0x03}, Scopes: transaction.CalledByEntry}}) + if res.State != "HALT" { + // The script failed + } + + b.Reset() // Copy the old script above if you need it! + + w, _ := wallet.NewWalletFromFile("somewhere") + // Assuming there is one Account inside + acc := w.Accounts[0] + from, _ := address.StringToUint160(acc.Address) + + // Multiple transfers in a single script. If any of them fail whole script fails. + b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x70}, 1, nil) + b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x71}, 10, []byte("data")) + b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x72}, 1, nil) + script, _ = b.Script() + + // The script can then be used to create transaction or to invoke via RPC. + txid, _ := c.SignAndPushInvocationTx(script, acc, -1, 0, nil) + _ = txid +} diff --git a/pkg/smartcontract/entry.go b/pkg/smartcontract/entry.go index fda82c0b1..5423495ef 100644 --- a/pkg/smartcontract/entry.go +++ b/pkg/smartcontract/entry.go @@ -76,3 +76,12 @@ func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string, bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position. return bytes, nil } + +// CreateCallWithAssertScript returns a script that calls contract's method with +// the specified parameters expecting a Boolean value to be return that then is +// used for ASSERT. See also (*Builder).InvokeWithAssert. +func CreateCallWithAssertScript(contract util.Uint160, method string, params ...interface{}) ([]byte, error) { + b := NewBuilder() + b.InvokeWithAssert(contract, method, params...) + return b.Script() +} From f749aaff3cd01211a1f0d392be0f78c7040acd0b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 25 Jul 2022 22:07:13 +0300 Subject: [PATCH 3/4] *: reuse smartcontract package to create standard entry scripts --- cli/wallet/validator.go | 24 +++++++-------- internal/testchain/transaction.go | 26 ++++++---------- pkg/consensus/consensus_test.go | 36 +++++++++++----------- pkg/core/bench_test.go | 14 +++------ pkg/core/interop/runtime/ext_test.go | 7 +++-- pkg/core/native/native_test/oracle_test.go | 10 ++---- pkg/core/native/oracle.go | 11 +++---- pkg/neotest/basic.go | 15 ++++----- pkg/smartcontract/entry.go | 9 ++++++ scripts/gendump/main.go | 9 ++---- 10 files changed, 73 insertions(+), 88 deletions(-) diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index dd95c57b9..9426bd14c 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -10,12 +10,9 @@ import ( "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/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/rpcclient" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" - "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/wallet" "github.com/urfave/cli" ) @@ -122,10 +119,11 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error { if err != nil { return err } - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes()) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - res, err := c.SignAndPushInvocationTx(w.Bytes(), acc, sysGas, gas, []rpcclient.SignerAccount{{ + script, err := smartcontract.CreateCallWithAssertScript(neoContractHash, method, acc.PrivateKey().PublicKey().Bytes()) + if err != nil { + return cli.NewExitError(err, 1) + } + res, err := c.SignAndPushInvocationTx(script, acc, sysGas, gas, []rpcclient.SignerAccount{{ Signer: transaction.Signer{ Account: acc.Contract.ScriptHash(), Scopes: transaction.CalledByEntry, @@ -182,11 +180,11 @@ func handleVote(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, neoContractHash, "vote", callflag.States, addr.BytesBE(), pubArg) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - - res, err := c.SignAndPushInvocationTx(w.Bytes(), acc, -1, gas, []rpcclient.SignerAccount{{ + script, err := smartcontract.CreateCallWithAssertScript(neoContractHash, "vote", addr.BytesBE(), pubArg) + if err != nil { + return cli.NewExitError(err, 1) + } + res, err := c.SignAndPushInvocationTx(script, acc, -1, gas, []rpcclient.SignerAccount{{ Signer: transaction.Signer{ Account: acc.Contract.ScriptHash(), Scopes: transaction.CalledByEntry, diff --git a/internal/testchain/transaction.go b/internal/testchain/transaction.go index 3c5aac449..2e05a804c 100644 --- a/internal/testchain/transaction.go +++ b/internal/testchain/transaction.go @@ -6,7 +6,7 @@ import ( gio "io" "strings" - "github.com/nspcc-dev/neo-go/cli/smartcontract" + clisc "github.com/nspcc-dev/neo-go/cli/smartcontract" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" @@ -16,11 +16,9 @@ import ( "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/callflag" + "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/emit" - "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) // Ledger is an interface that abstracts the implementation of the blockchain. @@ -42,14 +40,11 @@ var ( // NewTransferFromOwner returns a transaction transferring funds from NEO and GAS owner. func NewTransferFromOwner(bc Ledger, contractHash, to util.Uint160, amount int64, nonce, validUntil uint32) (*transaction.Transaction, error) { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, contractHash, "transfer", callflag.All, ownerHash, to, amount, nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - if w.Err != nil { - return nil, w.Err + script, err := smartcontract.CreateCallWithAssertScript(contractHash, "transfer", ownerHash, to, amount, nil) + if err != nil { + return nil, err } - script := w.Bytes() tx := transaction.New(script, 11000000) tx.ValidUntilBlock = validUntil tx.Nonce = nonce @@ -74,7 +69,7 @@ func NewDeployTx(bc Ledger, name string, sender util.Uint160, r gio.Reader, conf NoEventsCheck: true, } if confFile != nil { - conf, err := smartcontract.ParseContractConfig(*confFile) + conf, err := clisc.ParseContractConfig(*confFile) if err != nil { return nil, util.Uint160{}, nil, fmt.Errorf("failed to parse configuration: %w", err) } @@ -108,13 +103,12 @@ func NewDeployTx(bc Ledger, name string, sender util.Uint160, r gio.Reader, conf if err != nil { return nil, util.Uint160{}, nil, err } - buf := io.NewBufBinWriter() - emit.AppCall(buf.BinWriter, bc.ManagementContractHash(), "deploy", callflag.All, neb, rawManifest) - if buf.Err != nil { - return nil, util.Uint160{}, nil, buf.Err + script, err := smartcontract.CreateCallScript(bc.ManagementContractHash(), "deploy", neb, rawManifest) + if err != nil { + return nil, util.Uint160{}, nil, err } - tx := transaction.New(buf.Bytes(), 100*native.GASFactor) + tx := transaction.New(script, 100*native.GASFactor) tx.Signers = []transaction.Signer{{Account: sender}} h := state.CreateContractHash(tx.Sender(), ne.Checksum, m.Name) diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go index a49af9759..754bb74b0 100644 --- a/pkg/consensus/consensus_test.go +++ b/pkg/consensus/consensus_test.go @@ -22,7 +22,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/io" npayload "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -55,16 +54,16 @@ func initServiceNextConsensus(t *testing.T, newAcc *wallet.Account, offset uint3 newPriv := newAcc.PrivateKey() // Transfer funds to new validator. - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, bc.GoverningTokenHash(), "transfer", callflag.All, + b := smartcontract.NewBuilder() + b.InvokeWithAssert(bc.GoverningTokenHash(), "transfer", acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(native.NEOTotalSupply), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - emit.AppCall(w.BinWriter, bc.UtilityTokenHash(), "transfer", callflag.All, - acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(10000_000_000_000), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) - tx := transaction.New(w.Bytes(), 21_000_000) + b.InvokeWithAssert(bc.UtilityTokenHash(), "transfer", + acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(10000_000_000_000), nil) + script, err := b.Script() + require.NoError(t, err) + + tx := transaction.New(script, 21_000_000) tx.ValidUntilBlock = bc.BlockHeight() + 1 tx.NetworkFee = 10_000_000 tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: acc.Contract.ScriptHash()}} @@ -75,11 +74,12 @@ func initServiceNextConsensus(t *testing.T, newAcc *wallet.Account, offset uint3 srv.dbft.Start() // Register new candidate. - w.Reset() - emit.AppCall(w.BinWriter, bc.GoverningTokenHash(), "registerCandidate", callflag.All, newPriv.PublicKey().Bytes()) - require.NoError(t, w.Err) + b.Reset() + b.InvokeWithAssert(bc.GoverningTokenHash(), "registerCandidate", newPriv.PublicKey().Bytes()) + script, err = b.Script() + require.NoError(t, err) - tx = transaction.New(w.Bytes(), 1001_00000000) + tx = transaction.New(script, 1001_00000000) tx.ValidUntilBlock = bc.BlockHeight() + 1 tx.NetworkFee = 20_000_000 tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: newPriv.GetScriptHash()}} @@ -94,13 +94,13 @@ func initServiceNextConsensus(t *testing.T, newAcc *wallet.Account, offset uint3 } // Vote for new candidate. - w.Reset() - emit.AppCall(w.BinWriter, bc.GoverningTokenHash(), "vote", callflag.All, + b.Reset() + b.InvokeWithAssert(bc.GoverningTokenHash(), "vote", newPriv.GetScriptHash(), newPriv.PublicKey().Bytes()) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) + script, err = b.Script() + require.NoError(t, err) - tx = transaction.New(w.Bytes(), 20_000_000) + tx = transaction.New(script, 20_000_000) tx.ValidUntilBlock = bc.BlockHeight() + 1 tx.NetworkFee = 20_000_000 tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: newPriv.GetScriptHash()}} diff --git a/pkg/core/bench_test.go b/pkg/core/bench_test.go index 2e6415a10..2885487b7 100644 --- a/pkg/core/bench_test.go +++ b/pkg/core/bench_test.go @@ -14,12 +14,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "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/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" - "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/smartcontract" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -96,13 +93,12 @@ func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBloc from := e.Validator.ScriptHash() for j := 0; j < chainHeight; j++ { - w := io.NewBufBinWriter() + b := smartcontract.NewBuilder() for i := 0; i < transfersPerBlock; i++ { - emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, from, acc, 1, nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) + b.InvokeWithAssert(gasHash, "transfer", from, acc, 1, nil) } - require.NoError(t, w.Err) - script := w.Bytes() + script, err := b.Script() + require.NoError(t, err) tx := transaction.New(script, int64(1100_0000*transfersPerBlock)) tx.NetworkFee = 1_0000_000 tx.ValidUntilBlock = bc.BlockHeight() + 1 diff --git a/pkg/core/interop/runtime/ext_test.go b/pkg/core/interop/runtime/ext_test.go index bc262fe4a..43e5fff56 100644 --- a/pkg/core/interop/runtime/ext_test.go +++ b/pkg/core/interop/runtime/ext_test.go @@ -26,6 +26,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -424,9 +425,9 @@ func TestGetInvocationCounter(t *testing.T) { require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) }) t.Run("Contract", func(t *testing.T) { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, cs.Hash, "invocCounter", callflag.All) - v.LoadWithFlags(w.Bytes(), callflag.All) + script, err := smartcontract.CreateCallScript(cs.Hash, "invocCounter") + require.NoError(t, err) + v.LoadWithFlags(script, callflag.All) require.NoError(t, v.Run()) require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) }) diff --git a/pkg/core/native/native_test/oracle_test.go b/pkg/core/native/native_test/oracle_test.go index 4aafc7175..754add6d1 100644 --- a/pkg/core/native/native_test/oracle_test.go +++ b/pkg/core/native/native_test/oracle_test.go @@ -14,10 +14,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/neotest" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -81,11 +79,9 @@ func TestOracle_Request(t *testing.T) { // Finish. prepareResponseTx := func(t *testing.T, requestID uint64) *transaction.Transaction { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, oracleCommitteeInvoker.Hash, "finish", callflag.All) - require.NoError(t, w.Err) + script, err := smartcontract.CreateCallScript(oracleCommitteeInvoker.Hash, "finish") + require.NoError(t, err) - script := w.Bytes() tx := transaction.New(script, 1000_0000) tx.Nonce = neotest.Nonce() tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 8f99fd936..a31eb929d 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -20,13 +20,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util/slice" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -556,10 +554,9 @@ func (o *Oracle) updateCache(d *dao.Simple) error { // CreateOracleResponseScript returns a script that is used to create the native Oracle // response. func CreateOracleResponseScript(nativeOracleHash util.Uint160) []byte { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, nativeOracleHash, "finish", callflag.All) - if w.Err != nil { - panic(fmt.Errorf("failed to create Oracle response script: %w", w.Err)) + script, err := smartcontract.CreateCallScript(nativeOracleHash, "finish") + if err != nil { + panic(fmt.Errorf("failed to create Oracle response script: %w", err)) } - return w.Bytes() + return script } diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index 7d85882f4..a115aa7a4 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -16,11 +16,11 @@ import ( "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/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -81,11 +81,9 @@ func (e *Executor) NativeID(t testing.TB, name string) int32 { // NewUnsignedTx creates a new unsigned transaction which invokes the method of the contract with the hash. func (e *Executor) NewUnsignedTx(t testing.TB, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, hash, method, callflag.All, args...) - require.NoError(t, w.Err) + script, err := smartcontract.CreateCallScript(hash, method, args...) + require.NoError(t, err) - script := w.Bytes() tx := transaction.New(script, 0) tx.Nonce = Nonce() tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 @@ -266,11 +264,10 @@ func NewDeployTxBy(t testing.TB, bc *core.Blockchain, signer Signer, c *Contract neb, err := c.NEF.Bytes() require.NoError(t, err) - buf := io.NewBufBinWriter() - emit.AppCall(buf.BinWriter, bc.ManagementContractHash(), "deploy", callflag.All, neb, rawManifest, data) - require.NoError(t, buf.Err) + script, err := smartcontract.CreateCallScript(bc.ManagementContractHash(), "deploy", neb, rawManifest, data) + require.NoError(t, err) - tx := transaction.New(buf.Bytes(), 100*native.GASFactor) + tx := transaction.New(script, 100*native.GASFactor) tx.Nonce = Nonce() tx.ValidUntilBlock = bc.BlockHeight() + 1 tx.Signers = []transaction.Signer{{ diff --git a/pkg/smartcontract/entry.go b/pkg/smartcontract/entry.go index 5423495ef..a3fb44262 100644 --- a/pkg/smartcontract/entry.go +++ b/pkg/smartcontract/entry.go @@ -77,6 +77,15 @@ func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string, return bytes, nil } +// CreateCallScript returns a script that calls contract's method with +// the specified parameters. Whatever this method returns remains on the stack. +// See also (*Builder).InvokeMethod. +func CreateCallScript(contract util.Uint160, method string, params ...interface{}) ([]byte, error) { + b := NewBuilder() + b.InvokeMethod(contract, method, params...) + return b.Script() +} + // CreateCallWithAssertScript returns a script that calls contract's method with // the specified parameters expecting a Boolean value to be return that then is // used for ASSERT. See also (*Builder).InvokeWithAssert. diff --git a/scripts/gendump/main.go b/scripts/gendump/main.go index ef7079413..6f79ff917 100644 --- a/scripts/gendump/main.go +++ b/scripts/gendump/main.go @@ -20,8 +20,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/wallet" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -99,11 +97,10 @@ func main() { _, err = rand.Read(value) handleError("can't get random data for value", err) - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, contractHash, "put", callflag.All, key, value) - handleError("can't create transaction", w.Err) + script, err := smartcontract.CreateCallScript(contractHash, "put", key, value) + handleError("can't create transaction", err) - tx := transaction.New(w.Bytes(), 4_040_000) + tx := transaction.New(script, 4_040_000) tx.ValidUntilBlock = i + 1 tx.NetworkFee = 4_000_000 tx.Nonce = nonce From c8ff48928752044a9c01eb25000193115c282c93 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 26 Jul 2022 09:15:40 +0300 Subject: [PATCH 4/4] native: use CreateOracleResponseScript directly It wasn't possible way back when this test was written (CreateOracleResponseScript was a method), now we can simplify things. --- pkg/core/native/native_test/oracle_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/core/native/native_test/oracle_test.go b/pkg/core/native/native_test/oracle_test.go index 754add6d1..bc51d6de9 100644 --- a/pkg/core/native/native_test/oracle_test.go +++ b/pkg/core/native/native_test/oracle_test.go @@ -15,7 +15,6 @@ import ( "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/neotest" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -79,8 +78,7 @@ func TestOracle_Request(t *testing.T) { // Finish. prepareResponseTx := func(t *testing.T, requestID uint64) *transaction.Transaction { - script, err := smartcontract.CreateCallScript(oracleCommitteeInvoker.Hash, "finish") - require.NoError(t, err) + script := native.CreateOracleResponseScript(oracleCommitteeInvoker.Hash) tx := transaction.New(script, 1000_0000) tx.Nonce = neotest.Nonce()