From bbd802681e2b32d78b34ddbcffc5bf0e11ce8c66 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 11 Mar 2020 20:32:06 +0300 Subject: [PATCH 1/2] cli: make gas parameter to deployment add gas to the base price That's how it was intended to behave originally. One thing questionable here is contract price (policy thing, basically) being moved to smartcontract package, but it's probably fine for NEO 2.0 (as it won't change) and we'll make something better for NEO 3.0. --- cli/smartcontract/smart_contract.go | 14 +++++++++++--- pkg/core/gas_price.go | 13 +------------ pkg/rpc/request/txBuilder.go | 27 ++++++++++++++++----------- pkg/smartcontract/deployment_price.go | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 pkg/smartcontract/deployment_price.go diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 33278122f..643001866 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -44,7 +44,7 @@ var ( } gasFlag = cli.Float64Flag{ Name: "gas, g", - Usage: "gas to pay for transaction", + Usage: "gas to add to the transaction", } ) @@ -86,8 +86,14 @@ func NewCommands() []cli.Command { }, }, { - Name: "deploy", - Usage: "deploy a smart contract (.avm with description)", + Name: "deploy", + Usage: "deploy a smart contract (.avm with description)", + Description: `Deploys given contract into the chain. The gas parameter is for additional + gas to be added as a network fee to prioritize the transaction. It may also + be required to add that to satisfy chain's policy regarding transaction size + and the minimum size fee (so if transaction send fails, try adding 0.001 GAS + to it). +`, Action: contractDeploy, Flags: []cli.Flag{ cli.StringFlag{ @@ -583,6 +589,8 @@ func contractDeploy(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("failed to create deployment script: %v", err), 1) } + gas += smartcontract.GetDeploymentPrice(request.DetailsToSCProperties(&conf.Contract)) + txHash, err := c.SignAndPushInvocationTx(txScript, wif, gas) if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1) diff --git a/pkg/core/gas_price.go b/pkg/core/gas_price.go index 036dc0cbb..021297eb9 100644 --- a/pkg/core/gas_price.go +++ b/pkg/core/gas_price.go @@ -93,18 +93,7 @@ func getSyscallPrice(v *vm.VM, id uint32) util.Fixed8 { arg := estack.Peek(1).BigInt().Int64() return util.Fixed8FromInt64(arg * 5000) case neoContractCreate, neoContractMigrate, antSharesContractCreate, antSharesContractMigrate: - fee := int64(100) - props := smartcontract.PropertyState(estack.Peek(3).BigInt().Int64()) - - if props&smartcontract.HasStorage != 0 { - fee += 400 - } - - if props&smartcontract.HasDynamicInvoke != 0 { - fee += 500 - } - - return util.Fixed8FromInt64(fee) + return smartcontract.GetDeploymentPrice(smartcontract.PropertyState(estack.Peek(3).BigInt().Int64())) case systemStoragePut, systemStoragePutEx, neoStoragePut, antSharesStoragePut: // price for storage PUT is 1 GAS per 1 KiB keySize := len(estack.Peek(1).Bytes()) diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index 8fe3df81c..f841a71b3 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -80,17 +80,10 @@ func AddInputsAndUnspentsToTx(tx *transaction.Transaction, addr string, assetID return nil } -// CreateDeploymentScript returns a script that deploys given smart contract -// with its metadata. -func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) { +// DetailsToSCProperties extract the fields needed from ContractDetails +// and converts them to smartcontract.PropertyState. +func DetailsToSCProperties(contract *ContractDetails) smartcontract.PropertyState { var props smartcontract.PropertyState - - script := io.NewBufBinWriter() - emit.Bytes(script.BinWriter, []byte(contract.Description)) - emit.Bytes(script.BinWriter, []byte(contract.Email)) - emit.Bytes(script.BinWriter, []byte(contract.Author)) - emit.Bytes(script.BinWriter, []byte(contract.Version)) - emit.Bytes(script.BinWriter, []byte(contract.ProjectName)) if contract.HasStorage { props |= smartcontract.HasStorage } @@ -100,7 +93,19 @@ func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, erro if contract.IsPayable { props |= smartcontract.IsPayable } - emit.Int(script.BinWriter, int64(props)) + return props +} + +// CreateDeploymentScript returns a script that deploys given smart contract +// with its metadata. +func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) { + script := io.NewBufBinWriter() + emit.Bytes(script.BinWriter, []byte(contract.Description)) + emit.Bytes(script.BinWriter, []byte(contract.Email)) + emit.Bytes(script.BinWriter, []byte(contract.Author)) + emit.Bytes(script.BinWriter, []byte(contract.Version)) + emit.Bytes(script.BinWriter, []byte(contract.ProjectName)) + emit.Int(script.BinWriter, int64(DetailsToSCProperties(contract))) emit.Int(script.BinWriter, int64(contract.ReturnType)) params := make([]byte, len(contract.Parameters)) for k := range contract.Parameters { diff --git a/pkg/smartcontract/deployment_price.go b/pkg/smartcontract/deployment_price.go new file mode 100644 index 000000000..5ba9b8929 --- /dev/null +++ b/pkg/smartcontract/deployment_price.go @@ -0,0 +1,18 @@ +package smartcontract + +import "github.com/nspcc-dev/neo-go/pkg/util" + +// GetDeploymentPrice returns contract deployment price based on its properties. +func GetDeploymentPrice(props PropertyState) util.Fixed8 { + fee := int64(100) + + if props&HasStorage != 0 { + fee += 400 + } + + if props&HasDynamicInvoke != 0 { + fee += 500 + } + + return util.Fixed8FromInt64(fee) +} From 25da5a30d8fb0f6fb2bca1bf4931578adb559daf Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 11 Mar 2020 21:11:51 +0300 Subject: [PATCH 2/2] cli: change deploy and invoke commands to use wallet Passing WIF directly in the command line is not something we should be doing. Also split netfee and sysfee in the RPC as they're different (and add a script attribute for free transactions). --- cli/smartcontract/smart_contract.go | 83 ++++++++++++++++++++--------- pkg/rpc/client/rpc.go | 26 +++++---- 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 643001866..54efe463b 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -10,26 +10,31 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" + "syscall" "github.com/go-yaml/yaml" + "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/pkg/compiler" "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/address" "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/pkg/errors" "github.com/urfave/cli" + "golang.org/x/crypto/ssh/terminal" ) var ( errNoEndpoint = errors.New("no RPC endpoint specified, use option '--endpoint' or '-e'") errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag") - errNoWIF = errors.New("no WIF parameter found, specify it with the '--wif or -w' flag") + errNoWallet = errors.New("no wallet parameter found, specify it with the '--wallet or -w' flag") errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument") errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") errFileExist = errors.New("A file with given smart-contract name already exists") @@ -38,9 +43,13 @@ var ( Name: "endpoint, e", Usage: "trusted RPC endpoint address (like 'http://localhost:20331')", } - wifFlag = cli.StringFlag{ - Name: "wif, w", - Usage: "key to sign deployed transaction (in wif format)", + walletFlag = cli.StringFlag{ + Name: "wallet, w", + Usage: "wallet to use to get the key for transaction signing", + } + addressFlag = flags.AddressFlag{ + Name: "address, a", + Usage: "address to use as transaction signee (and gas source)", } gasFlag = cli.Float64Flag{ Name: "gas, g", @@ -105,14 +114,15 @@ func NewCommands() []cli.Command { Usage: "configuration input file (*.yml)", }, endpointFlag, - wifFlag, + walletFlag, + addressFlag, gasFlag, }, }, { Name: "invoke", Usage: "invoke deployed contract on the blockchain", - UsageText: "neo-go contract invoke -e endpoint -w wif [-g gas] scripthash [arguments...]", + UsageText: "neo-go contract invoke -e endpoint -w wallet [-a address] [-g gas] scripthash [arguments...]", Description: `Executes given (as a script hash) deployed script with the given arguments. See testinvoke documentation for the details about parameters. It differs from testinvoke in that this command sends an invocation transaction to @@ -121,14 +131,15 @@ func NewCommands() []cli.Command { Action: invoke, Flags: []cli.Flag{ endpointFlag, - wifFlag, + walletFlag, + addressFlag, gasFlag, }, }, { Name: "invokefunction", Usage: "invoke deployed contract on the blockchain", - UsageText: "neo-go contract invokefunction -e endpoint -w wif [-g gas] scripthash [method] [arguments...]", + UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...]", Description: `Executes given (as a script hash) deployed script with the given method and and arguments. See testinvokefunction documentation for the details about parameters. It differs from testinvokefunction in that this command sends an @@ -137,7 +148,8 @@ func NewCommands() []cli.Command { Action: invokeFunction, Flags: []cli.Flag{ endpointFlag, - wifFlag, + walletFlag, + addressFlag, gasFlag, }, }, @@ -371,7 +383,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error { params = make([]smartcontract.Parameter, 0) paramsStart = 1 resp *result.Invoke - wif *keys.WIF + acc *wallet.Account ) endpoint := ctx.String("endpoint") @@ -400,8 +412,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error { if signAndPush { gas = util.Fixed8FromFloat(ctx.Float64("gas")) - - wif, err = getWifFromContext(ctx) + acc, err = getAccFromContext(ctx) if err != nil { return err } @@ -427,7 +438,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error { if err != nil { return cli.NewExitError(fmt.Errorf("bad script returned from the RPC node: %v", err), 1) } - txHash, err := c.SignAndPushInvocationTx(script, wif, gas) + txHash, err := c.SignAndPushInvocationTx(script, acc, 0, gas) if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1) } @@ -531,17 +542,41 @@ func inspect(ctx *cli.Context) error { return nil } -func getWifFromContext(ctx *cli.Context) (*keys.WIF, error) { - wifStr := ctx.String("wif") - if len(wifStr) == 0 { - return nil, cli.NewExitError(errNoWIF, 1) +func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { + var addr util.Uint160 + + wPath := ctx.String("wallet") + if len(wPath) == 0 { + return nil, cli.NewExitError(errNoWallet, 1) } - wif, err := keys.WIFDecode(wifStr, 0) + wall, err := wallet.NewWalletFromFile(wPath) if err != nil { - return nil, cli.NewExitError(fmt.Errorf("bad wif: %v", err), 1) + return nil, cli.NewExitError(err, 1) } - return wif, nil + addrFlag := ctx.Generic("address").(*flags.Address) + if addrFlag.IsSet { + addr = addrFlag.Uint160() + } else { + addr = wall.GetChangeAddress() + } + acc := wall.GetAccount(addr) + if acc == nil { + return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) + } + + fmt.Printf("Enter account %s password > ", address.Uint160ToString(addr)) + rawPass, err := terminal.ReadPassword(syscall.Stdin) + fmt.Println() + if err != nil { + return nil, cli.NewExitError(err, 1) + } + pass := strings.TrimRight(string(rawPass), "\n") + err = acc.Decrypt(pass) + if err != nil { + return nil, cli.NewExitError(err, 1) + } + return acc, nil } // contractDeploy deploys contract. @@ -560,7 +595,7 @@ func contractDeploy(ctx *cli.Context) error { } gas := util.Fixed8FromFloat(ctx.Float64("gas")) - wif, err := getWifFromContext(ctx) + acc, err := getAccFromContext(ctx) if err != nil { return err } @@ -589,9 +624,9 @@ func contractDeploy(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("failed to create deployment script: %v", err), 1) } - gas += smartcontract.GetDeploymentPrice(request.DetailsToSCProperties(&conf.Contract)) + sysfee := smartcontract.GetDeploymentPrice(request.DetailsToSCProperties(&conf.Contract)) - txHash, err := c.SignAndPushInvocationTx(txScript, wif, gas) + txHash, err := c.SignAndPushInvocationTx(txScript, acc, sysfee, gas) if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1) } diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d30148ee4..27d93f664 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -6,7 +6,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "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/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" @@ -249,24 +249,30 @@ func (c *Client) TransferAsset(asset util.Uint256, address string, amount util.F // SignAndPushInvocationTx signs and pushes given script as an invocation // transaction using given wif to sign it and spending the amount of gas // specified. It returns a hash of the invocation transaction and an error. -func (c *Client) SignAndPushInvocationTx(script []byte, wif *keys.WIF, gas util.Fixed8) (util.Uint256, error) { +func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee util.Fixed8, netfee util.Fixed8) (util.Uint256, error) { var txHash util.Uint256 var err error - tx := transaction.NewInvocationTX(script, gas) - - fromAddress := wif.PrivateKey.Address() + tx := transaction.NewInvocationTX(script, sysfee) + gas := sysfee + netfee if gas > 0 { - if err = request.AddInputsAndUnspentsToTx(tx, fromAddress, core.UtilityTokenID(), gas, c); err != nil { + if err = request.AddInputsAndUnspentsToTx(tx, acc.Address, core.UtilityTokenID(), gas, c); err != nil { return txHash, errors.Wrap(err, "failed to add inputs and unspents to transaction") } + } else { + addr, err := address.StringToUint160(acc.Address) + if err != nil { + return txHash, errors.Wrap(err, "failed to get address") + } + tx.Attributes = append(tx.Attributes, + transaction.Attribute{ + Usage: transaction.Script, + Data: addr.BytesBE(), + }) } - acc, err := wallet.NewAccountFromWIF(wif.S) - if err != nil { - return txHash, err - } else if err = acc.SignTx(tx); err != nil { + if err = acc.SignTx(tx); err != nil { return txHash, errors.Wrap(err, "failed to sign tx") } txHash = tx.Hash()