Merge pull request #2610 from nspcc-dev/sc-code-emit

Move code generation from rpcclient to smartcontract
This commit is contained in:
Roman Khimov 2022-07-26 12:35:18 +03:00 committed by GitHub
commit 1ff588a11b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 295 additions and 173 deletions

View file

@ -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,

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -14,10 +14,7 @@ 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/vm/stackitem"
"github.com/stretchr/testify/require"
)
@ -81,11 +78,8 @@ 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 := native.CreateOracleResponseScript(oracleCommitteeInvoker.Hash)
script := w.Bytes()
tx := transaction.New(script, 1000_0000)
tx.Nonce = neotest.Nonce()
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1

View file

@ -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
}

View file

@ -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{{

View file

@ -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) {

View file

@ -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,

View file

@ -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,

View file

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

8
pkg/smartcontract/doc.go Normal file
View file

@ -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

View file

@ -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
}

View file

@ -0,0 +1,96 @@
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
}
// 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.
func CreateCallWithAssertScript(contract util.Uint160, method string, params ...interface{}) ([]byte, error) {
b := NewBuilder()
b.InvokeWithAssert(contract, method, params...)
return b.Script()
}

View file

@ -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