neo-go/pkg/rpc/txBuilder.go
Roman Khimov 8d4dd2d2e1 vm: move opcodes into their own package
This allows easier reuse of opcodes and in some cases allows to eliminate
dependencies on the whole vm package, like in compiler that only needs opcodes
and doesn't care about VM for any other purpose.

And yes, they're opcodes because an instruction is a whole thing with
operands, that's what context.Next() returns.
2019-12-03 18:22:14 +03:00

312 lines
8.6 KiB
Go

package rpc
import (
"bytes"
"errors"
"fmt"
"strconv"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
errs "github.com/pkg/errors"
)
// CreateRawContractTransaction returns contract-type Transaction built from specified parameters.
func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transaction, error) {
var (
err error
tx = transaction.NewContractTX()
toAddressHash, fromAddressHash util.Uint160
fromAddress string
receiverOutput *transaction.Output
wif, assetID, address, amount, balancer = params.wif, params.assetID, params.address, params.value, params.balancer
)
fromAddress = wif.PrivateKey.Address()
if fromAddressHash, err = crypto.Uint160DecodeAddress(fromAddress); err != nil {
return nil, errs.Wrapf(err, "Failed to take script hash from address: %v", fromAddress)
}
if toAddressHash, err = crypto.Uint160DecodeAddress(address); err != nil {
return nil, errs.Wrapf(err, "Failed to take script hash from address: %v", address)
}
tx.Attributes = append(tx.Attributes,
&transaction.Attribute{
Usage: transaction.Script,
Data: fromAddressHash.Bytes(),
})
if err = AddInputsAndUnspentsToTx(tx, fromAddress, assetID, amount, balancer); err != nil {
return nil, errs.Wrap(err, "failed to add inputs and unspents to transaction")
}
receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash)
tx.AddOutput(receiverOutput)
if err = SignTx(tx, &wif); err != nil {
return nil, errs.Wrap(err, "failed to sign tx")
}
return tx, nil
}
// AddInputsAndUnspentsToTx adds inputs needed to transaction and one output
// with change.
func AddInputsAndUnspentsToTx(tx *transaction.Transaction, address string, assetID util.Uint256, amount util.Fixed8, balancer BalanceGetter) error {
scriptHash, err := crypto.Uint160DecodeAddress(address)
if err != nil {
return errs.Wrapf(err, "failed to take script hash from address: %v", address)
}
inputs, spent, err := balancer.CalculateInputs(address, assetID, amount)
if err != nil {
return errs.Wrap(err, "failed to get inputs")
}
for _, input := range inputs {
tx.AddInput(&input)
}
if senderUnspent := spent - amount; senderUnspent > 0 {
senderOutput := transaction.NewOutput(assetID, senderUnspent, scriptHash)
tx.AddOutput(senderOutput)
}
return nil
}
// SignTx signs given transaction in-place using given key.
func SignTx(tx *transaction.Transaction, wif *keys.WIF) error {
var witness transaction.Witness
var err error
if witness.InvocationScript, err = GetInvocationScript(tx, wif); err != nil {
return errs.Wrap(err, "failed to create invocation script")
}
witness.VerificationScript = wif.GetVerificationScript()
tx.Scripts = append(tx.Scripts, &witness)
tx.Hash()
return nil
}
// GetInvocationScript returns NEO VM script containing transaction signature.
func GetInvocationScript(tx *transaction.Transaction, wif *keys.WIF) ([]byte, error) {
const (
pushbytes64 = 0x40
)
var (
err error
buf = io.NewBufBinWriter()
signature []byte
)
tx.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return nil, errs.Wrap(buf.Err, "Failed to encode transaction to binary")
}
data := buf.Bytes()
signature, err = wif.PrivateKey.Sign(data[:(len(data) - 1)])
if err != nil {
return nil, errs.Wrap(err, "Failed ti sign transaction with private key")
}
return append([]byte{pushbytes64}, signature...), nil
}
// CreateDeploymentScript returns a script that deploys given smart contract
// with its metadata.
func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) {
var props smartcontract.PropertyState
script := new(bytes.Buffer)
if err := vm.EmitBytes(script, []byte(contract.Description)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.Email)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.Author)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.Version)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.ProjectName)); err != nil {
return nil, err
}
if contract.HasStorage {
props |= smartcontract.HasStorage
}
if contract.HasDynamicInvocation {
props |= smartcontract.HasDynamicInvoke
}
if contract.IsPayable {
props |= smartcontract.IsPayable
}
if err := vm.EmitInt(script, int64(props)); err != nil {
return nil, err
}
if err := vm.EmitInt(script, int64(contract.ReturnType)); err != nil {
return nil, err
}
params := make([]byte, len(contract.Parameters))
for k := range contract.Parameters {
params[k] = byte(contract.Parameters[k])
}
if err := vm.EmitBytes(script, params); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, avm); err != nil {
return nil, err
}
if err := vm.EmitSyscall(script, "Neo.Contract.Create"); err != nil {
return nil, err
}
return script.Bytes(), nil
}
// expandArrayIntoScript pushes all FuncParam parameters from the given array
// into the given buffer in reverse order.
func expandArrayIntoScript(script *bytes.Buffer, slice []Param) error {
for j := len(slice) - 1; j >= 0; j-- {
fp, err := slice[j].GetFuncParam()
if err != nil {
return err
}
switch fp.Type {
case ByteArray, Signature:
str, err := fp.Value.GetBytesHex()
if err != nil {
return err
}
if err := vm.EmitBytes(script, str); err != nil {
return err
}
case String:
str, err := fp.Value.GetString()
if err != nil {
return err
}
if err := vm.EmitString(script, str); err != nil {
return err
}
case Hash160:
hash, err := fp.Value.GetUint160FromHex()
if err != nil {
return err
}
if err := vm.EmitBytes(script, hash.Bytes()); err != nil {
return err
}
case Hash256:
hash, err := fp.Value.GetUint256()
if err != nil {
return err
}
if err := vm.EmitBytes(script, hash.Bytes()); err != nil {
return err
}
case PublicKey:
str, err := fp.Value.GetString()
if err != nil {
return err
}
key, err := keys.NewPublicKeyFromString(string(str))
if err != nil {
return err
}
if err := vm.EmitBytes(script, key.Bytes()); err != nil {
return err
}
case Integer:
val, err := fp.Value.GetInt()
if err != nil {
return err
}
if err := vm.EmitInt(script, int64(val)); err != nil {
return err
}
case Boolean:
str, err := fp.Value.GetString()
if err != nil {
return err
}
switch str {
case "true":
err = vm.EmitInt(script, 1)
case "false":
err = vm.EmitInt(script, 0)
default:
err = errors.New("wrong boolean value")
}
if err != nil {
return err
}
default:
return fmt.Errorf("parameter type %v is not supported", fp.Type)
}
}
return nil
}
// CreateFunctionInvocationScript creates a script to invoke given contract with
// given parameters.
func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byte, error) {
script := new(bytes.Buffer)
for i := len(params) - 1; i >= 0; i-- {
switch params[i].Type {
case stringT:
if err := vm.EmitString(script, params[i].String()); err != nil {
return nil, err
}
case numberT:
num, err := params[i].GetInt()
if err != nil {
return nil, err
}
if err := vm.EmitString(script, strconv.Itoa(num)); err != nil {
return nil, err
}
case arrayT:
slice, err := params[i].GetArray()
if err != nil {
return nil, err
}
err = expandArrayIntoScript(script, slice)
if err != nil {
return nil, err
}
err = vm.EmitInt(script, int64(len(slice)))
if err != nil {
return nil, err
}
err = vm.EmitOpcode(script, opcode.PACK)
if err != nil {
return nil, err
}
}
}
if err := vm.EmitAppCall(script, contract, false); err != nil {
return nil, err
}
return script.Bytes(), nil
}
// CreateInvocationScript creates a script to invoke given contract with
// given parameters. It differs from CreateFunctionInvocationScript in that it
// expects one array of FuncParams and expands it onto the stack as independent
// elements.
func CreateInvocationScript(contract util.Uint160, funcParams []Param) ([]byte, error) {
script := new(bytes.Buffer)
err := expandArrayIntoScript(script, funcParams)
if err != nil {
return nil, err
}
if err = vm.EmitAppCall(script, contract, false); err != nil {
return nil, err
}
return script.Bytes(), nil
}