cli: fix wallet convert and add tests

Close #1589.
This commit is contained in:
Evgenii Stratonikov 2020-12-03 17:33:52 +03:00
parent 4cd5747ab7
commit 239a8c3de7
6 changed files with 383 additions and 20 deletions

View file

@ -0,0 +1 @@
{"name":"wallet1","version":"1.0","scrypt":{"n":16384,"r":8,"p":8},"accounts":[{"address":"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"2102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2ac","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"extra":null},{"address":"AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae","parameters":[{"name":"parameter0","type":"Signature"},{"name":"parameter1","type":"Signature"},{"name":"parameter2","type":"Signature"}],"deployed":false},"extra":null}],"extra":null}

View file

@ -0,0 +1,55 @@
{
"version": "3.0",
"accounts": [
{
"address": "NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc",
"key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux",
"label": "",
"contract": {
"script": "DCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcILQZVEDXg=",
"parameters": [
{
"name": "parameter0",
"type": "Signature"
}
],
"deployed": false
},
"lock": false,
"isdefault": false
},
{
"address": "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY",
"key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux",
"label": "",
"contract": {
"script": "EwwhAhA6f33QFlWFl/eWDSfFFqQ5T9loueZRVetLAT5AQEBuDCECp7xV/oaE4BGXaNEEujB5W9zIZhnoZK3SYVZyPtGFzWIMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwgwhA9kMB99j5pDOd5EuEKtRrMlEtmhgI3tgjE+PgwnnHuaZFAtBE43vrw==",
"parameters": [
{
"name": "parameter0",
"type": "Signature"
},
{
"name": "parameter1",
"type": "Signature"
},
{
"name": "parameter2",
"type": "Signature"
}
],
"deployed": false
},
"lock": false,
"isdefault": false
}
],
"scrypt": {
"n": 16384,
"r": 8,
"p": 8
},
"extra": {
"Tokens": null
}
}

165
cli/wallet/legacy.go Normal file
View file

@ -0,0 +1,165 @@
package wallet
import (
"crypto/elliptic"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"os"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
type (
walletV2 struct {
Version string `json:"version"`
Accounts []accountV2 `json:"accounts"`
Scrypt keys.ScryptParams `json:"scrypt"`
Extra wallet.Extra `json:"extra"`
}
accountV2 struct {
Address string `json:"address"`
EncryptedWIF string `json:"key"`
Label string `json:"label"`
Contract *struct {
Script string `json:"script"`
Parameters []wallet.ContractParam `json:"parameters"`
Deployed bool `json:"deployed"`
} `json:"contract"`
Locked bool `json:"lock"`
Default bool `json:"isdefault"`
}
)
// newWalletV2FromFile reads NEO2 wallet from file.
// This should be used read-only, no operations are supported on returned wallet.
func newWalletV2FromFile(path string) (*walletV2, error) {
file, err := os.OpenFile(path, os.O_RDWR, os.ModeAppend)
if err != nil {
return nil, err
}
defer file.Close()
wall := new(walletV2)
return wall, json.NewDecoder(file).Decode(wall)
}
const simpleSigLen = 35
func (a *accountV2) convert(pass string) (*wallet.Account, error) {
address.Prefix = address.NEO2Prefix
priv, err := keys.NEP2Decrypt(a.EncryptedWIF, pass)
if err != nil {
return nil, err
}
address.Prefix = address.NEO3Prefix
newAcc, err := wallet.NewAccountFromWIF(priv.WIF())
if err != nil {
return nil, err
}
if a.Contract != nil {
script, err := hex.DecodeString(a.Contract.Script)
if err != nil {
return nil, err
}
// If it is simple signature script, newAcc does already have it.
if len(script) != simpleSigLen {
nsigs, pubs, ok := parseMultisigContract(script)
if !ok {
return nil, errors.New("invalid multisig contract")
}
script, err := smartcontract.CreateMultiSigRedeemScript(nsigs, pubs)
if err != nil {
return nil, errors.New("can't create new multisig contract")
}
newAcc.Contract.Script = script
newAcc.Contract.Parameters = a.Contract.Parameters
newAcc.Contract.Deployed = a.Contract.Deployed
}
}
newAcc.Address = address.Uint160ToString(newAcc.Contract.ScriptHash())
newAcc.Default = a.Default
newAcc.Label = a.Label
newAcc.Locked = a.Locked
return newAcc, newAcc.Encrypt(pass)
}
const (
opPush1 = 0x51
opPush16 = 0x60
opPushBytes1 = 0x01
opPushBytes2 = 0x02
opPushBytes33 = 0x21
opCheckMultisig = 0xAE
opRet = 0x66
)
func getNumOfThingsFromInstr(script []byte) (int, int, bool) {
var op = script[0]
switch {
case opPush1 <= op && op <= opPush16:
return int(op-opPush1) + 1, 1, true
case op == opPushBytes1 && len(script) >= 2:
return int(script[1]), 2, true
case op == opPushBytes2 && len(script) >= 3:
return int(binary.LittleEndian.Uint16(script[1:])), 3, true
default:
return 0, 0, false
}
}
const minMultisigLen = 37
// parseMultisigContract accepts multisig verification script from NEO2
// and returns list of public keys in the same order as in script..
func parseMultisigContract(script []byte) (int, keys.PublicKeys, bool) {
// It should contain at least 1 public key.
if len(script) < minMultisigLen {
return 0, nil, false
}
nsigs, offset, ok := getNumOfThingsFromInstr(script)
if !ok {
return 0, nil, false
}
var pubs [][]byte
var nkeys int
for offset < len(script) && script[offset] == opPushBytes33 {
if len(script[offset:]) < 34 {
return 0, nil, false
}
pubs = append(pubs, script[offset+1:offset+34])
nkeys++
offset += 34
}
if nkeys < nsigs || offset >= len(script) {
return 0, nil, false
}
nkeys2, off, ok := getNumOfThingsFromInstr(script[offset:])
if !ok || nkeys2 != nkeys {
return 0, nil, false
}
end := script[offset+off:]
switch {
case len(end) == 1 && end[0] == opCheckMultisig:
case len(end) == 2 && end[0] == opCheckMultisig && end[1] == opRet:
default:
return 0, nil, false
}
ret := make(keys.PublicKeys, len(pubs))
for i := range pubs {
pub, err := keys.NewPublicKeyFromBytes(pubs[i], elliptic.P256())
if err != nil {
return 0, nil, false
}
ret[i] = pub
}
return nsigs, ret, true
}

111
cli/wallet/legacy_test.go Normal file
View file

@ -0,0 +1,111 @@
package wallet
import (
"crypto/elliptic"
"encoding/hex"
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
)
func testParseMultisigContract(t *testing.T, s []byte, nsigs int, keys ...*keys.PublicKey) {
ns, ks, ok := parseMultisigContract(s)
if len(keys) == 0 {
require.False(t, ok)
return
}
require.True(t, ok)
require.Equal(t, nsigs, ns)
require.Equal(t, len(keys), len(ks))
for i := range keys {
require.Equal(t, keys[i], ks[i])
}
}
func TestParseMultisigContract(t *testing.T) {
t.Run("single multisig", func(t *testing.T) {
s := fromHex(t, "512102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc251ae")
pub := pubFromHex(t, "02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2")
t.Run("good, no ret", func(t *testing.T) {
testParseMultisigContract(t, s, 1, pub)
})
t.Run("good, with ret", func(t *testing.T) {
s := append(s, opRet)
testParseMultisigContract(t, s, 1, pub)
})
t.Run("bad, no check multisig", func(t *testing.T) {
sBad := make([]byte, len(s))
copy(sBad, s)
sBad[len(sBad)-1] ^= 0xFF
testParseMultisigContract(t, sBad, 0)
})
t.Run("bad, invalid number of keys", func(t *testing.T) {
sBad := make([]byte, len(s))
copy(sBad, s)
sBad[len(sBad)-2] = opPush1 + 1
testParseMultisigContract(t, sBad, 0)
})
t.Run("bad, invalid first instruction", func(t *testing.T) {
sBad := make([]byte, len(s))
copy(sBad, s)
sBad[0] = 0xFF
testParseMultisigContract(t, sBad, 0)
})
t.Run("bad, invalid public key", func(t *testing.T) {
sBad := make([]byte, len(s))
copy(sBad, s)
sBad[2] = 0xFF
testParseMultisigContract(t, sBad, 0)
})
t.Run("bad, many sigs", func(t *testing.T) {
sBad := make([]byte, len(s))
copy(sBad, s)
sBad[0] = opPush1 + 1
testParseMultisigContract(t, sBad, 0)
})
t.Run("empty, no panic", func(t *testing.T) {
testParseMultisigContract(t, []byte{}, 0)
})
})
t.Run("3/4 multisig", func(t *testing.T) {
// From privnet consensus wallet.
s := fromHex(t, "532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae")
ks := keys.PublicKeys{
pubFromHex(t, "02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e"),
pubFromHex(t, "02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62"),
pubFromHex(t, "02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2"),
pubFromHex(t, "03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699"),
}
t.Run("good", func(t *testing.T) {
testParseMultisigContract(t, s, 3, ks...)
})
t.Run("good, with pushbytes1", func(t *testing.T) {
s := append([]byte{opPushBytes1, 3}, s[1:]...)
testParseMultisigContract(t, s, 3, ks...)
})
t.Run("good, with pushbytes2", func(t *testing.T) {
s := append([]byte{opPushBytes2, 3, 0}, s[1:]...)
testParseMultisigContract(t, s, 3, ks...)
})
t.Run("bad, no panic on prefix", func(t *testing.T) {
for i := minMultisigLen; i < len(s)-1; i++ {
testParseMultisigContract(t, s[:i], 0)
}
})
})
}
func fromHex(t *testing.T, s string) []byte {
bs, err := hex.DecodeString(s)
require.NoError(t, err)
return bs
}
func pubFromHex(t *testing.T, s string) *keys.PublicKey {
bs := fromHex(t, s)
pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
require.NoError(t, err)
return pub
}

View file

@ -247,40 +247,25 @@ func claimGas(ctx *cli.Context) error {
}
func convertWallet(ctx *cli.Context) error {
wall, err := openWallet(ctx.String("wallet"))
wall, err := newWalletV2FromFile(ctx.String("wallet"))
if err != nil {
return cli.NewExitError(err, 1)
}
defer wall.Close()
newWallet, err := wallet.NewWallet(ctx.String("out"))
if err != nil {
return cli.NewExitError(err, -1)
return cli.NewExitError(err, 1)
}
defer newWallet.Close()
for _, acc := range wall.Accounts {
address.Prefix = address.NEO2Prefix
pass, err := input.ReadPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label))
if err != nil {
return cli.NewExitError(err, -1)
} else if err := acc.Decrypt(pass); err != nil {
return cli.NewExitError("invalid passphrase", -1)
return cli.NewExitError(err, 1)
}
address.Prefix = address.NEO3Prefix
newAcc, err := wallet.NewAccountFromWIF(acc.PrivateKey().WIF())
newAcc, err := acc.convert(pass)
if err != nil {
return cli.NewExitError(fmt.Errorf("can't convert account: %w", err), -1)
}
newAcc.Address = address.Uint160ToString(acc.Contract.ScriptHash())
newAcc.Contract = acc.Contract
newAcc.Default = acc.Default
newAcc.Label = acc.Label
newAcc.Locked = acc.Locked
if err := newAcc.Encrypt(pass); err != nil {
return cli.NewExitError(fmt.Errorf("can't encrypt converted account: %w", err), -1)
return cli.NewExitError(err, 1)
}
newWallet.AddAccount(newAcc)
}

View file

@ -310,3 +310,49 @@ func TestWalletDump(t *testing.T) {
require.Equal(t, "NNuJqXDnRqvwgzhSzhH4jnVFWB1DyZ34EM", w.Accounts[0].Address)
})
}
// Testcase is the wallet of privnet validator.
func TestWalletConvert(t *testing.T) {
tmpDir := path.Join(os.TempDir(), "neogo.test.convert")
require.NoError(t, os.Mkdir(tmpDir, os.ModePerm))
defer os.RemoveAll(tmpDir)
e := newExecutor(t, false)
defer e.Close(t)
outPath := path.Join(tmpDir, "wallet.json")
cmd := []string{"neo-go", "wallet", "convert"}
t.Run("missing wallet", func(t *testing.T) {
e.RunWithError(t, cmd...)
})
cmd = append(cmd, "--wallet", "testdata/wallets/testwallet_NEO2.json", "--out", outPath)
t.Run("invalid password", func(t *testing.T) {
// missing password
e.RunWithError(t, cmd...)
// invalid password
e.In.WriteString("two\r")
e.RunWithError(t, cmd...)
})
// 2 accounts.
e.In.WriteString("one\r")
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "convert",
"--wallet", "testdata/wallets/testwallet_NEO2.json",
"--out", outPath)
actual, err := wallet.NewWalletFromFile(outPath)
require.NoError(t, err)
expected, err := wallet.NewWalletFromFile("testdata/wallets/testwallet_NEO3.json")
require.NoError(t, err)
require.Equal(t, len(actual.Accounts), len(expected.Accounts))
for _, exp := range expected.Accounts {
addr, err := address.StringToUint160(exp.Address)
require.NoError(t, err)
act := actual.GetAccount(addr)
require.NotNil(t, act)
require.Equal(t, exp, act)
}
}