forked from TrueCloudLab/neoneo-go
parent
4cd5747ab7
commit
239a8c3de7
6 changed files with 383 additions and 20 deletions
1
cli/testdata/wallets/testwallet_NEO2.json
vendored
Normal file
1
cli/testdata/wallets/testwallet_NEO2.json
vendored
Normal 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}
|
55
cli/testdata/wallets/testwallet_NEO3.json
vendored
Normal file
55
cli/testdata/wallets/testwallet_NEO3.json
vendored
Normal 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
165
cli/wallet/legacy.go
Normal 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
111
cli/wallet/legacy_test.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue