diff --git a/cmd/wallet/address.go b/cmd/wallet/address.go index 170813350..1134e520a 100644 --- a/cmd/wallet/address.go +++ b/cmd/wallet/address.go @@ -86,6 +86,9 @@ func buildNewAddressCmd(parentCmd *cobra.Command) { if *addressType == wallet.AddressTypeBLSAccount { addressInfo, err = wlt.NewBLSAccountAddress(label) + } else if *addressType == wallet.AddressTypeEd25519Account { + password := cmd.PromptInput("Password") + addressInfo, err = wlt.NewEd25519AccountAddress(label, password) } else if *addressType == wallet.AddressTypeValidator { addressInfo, err = wlt.NewValidatorAddress(label) } else { diff --git a/crypto/address.go b/crypto/address.go index 536a93505..fd26b6e46 100644 --- a/crypto/address.go +++ b/crypto/address.go @@ -164,7 +164,8 @@ func (addr Address) IsTreasuryAddress() bool { func (addr Address) IsAccountAddress() bool { return addr.Type() == AddressTypeTreasury || - addr.Type() == AddressTypeBLSAccount + addr.Type() == AddressTypeBLSAccount || + addr.Type() == AddressTypeEd25519Account } func (addr Address) IsValidatorAddress() bool { diff --git a/crypto/address_test.go b/crypto/address_test.go index 6bc28b3af..21733f8a0 100644 --- a/crypto/address_test.go +++ b/crypto/address_test.go @@ -3,7 +3,6 @@ package crypto_test import ( "bytes" "encoding/hex" - "fmt" "io" "strings" "testing" @@ -15,109 +14,145 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAddressKeyType(t *testing.T) { - ts := testsuite.NewTestSuite(t) - - pub, _ := ts.RandBLSKeyPair() - accAddr := pub.AccountAddress() - valAddr := pub.ValidatorAddress() +func TestTreasuryAddressType(t *testing.T) { treasury := crypto.TreasuryAddress - assert.True(t, accAddr.IsAccountAddress()) - assert.False(t, accAddr.IsValidatorAddress()) - assert.False(t, accAddr.IsTreasuryAddress()) - assert.False(t, valAddr.IsAccountAddress()) - assert.True(t, valAddr.IsValidatorAddress()) assert.False(t, treasury.IsValidatorAddress()) assert.True(t, treasury.IsAccountAddress()) assert.True(t, treasury.IsTreasuryAddress()) - assert.NotEqual(t, accAddr, valAddr) } -func TestString(t *testing.T) { - ts := testsuite.NewTestSuite(t) +func TestAddressType(t *testing.T) { + tests := []struct { + address string + account bool + validator bool + }{ + {address: "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr", account: false, validator: true}, + {address: "pc1zzqkzzu4vyddss052as6c37qrdcfptegquw826x", account: true, validator: false}, + {address: "pc1rspm7ps49gar9ft5g0tkl6lhxs8ygeakq87quh3", account: true, validator: false}, + } + + for _, test := range tests { + addr, _ := crypto.AddressFromString(test.address) + + assert.Equal(t, test.account, addr.IsAccountAddress()) + assert.Equal(t, test.validator, addr.IsValidatorAddress()) + } +} - a, _ := crypto.AddressFromString("pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr") - fmt.Println(a.String()) +func TestShortString(t *testing.T) { + ts := testsuite.NewTestSuite(t) addr1 := ts.RandAccAddress() assert.Contains(t, addr1.String(), addr1.ShortString()) } -func TestToString(t *testing.T) { +func TestFromString(t *testing.T) { tests := []struct { - encoded string - err error - result *crypto.Address + encoded string + err error + bytes []byte + addrType crypto.AddressType }{ { "000000000000000000000000000000000000000000", nil, - &crypto.Address{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + crypto.AddressTypeTreasury, }, { "", bech32m.InvalidLengthError(0), nil, + 0, }, { "not_proper_encoded", bech32m.InvalidSeparatorIndexError(-1), nil, + 0, }, { "pc1ioiooi", bech32m.NonCharsetCharError(105), nil, + 0, }, { "pc19p72rf", bech32m.InvalidLengthError(0), nil, + 0, }, { "qc1z0hrct7eflrpw4ccrttxzs4qud2axex4dh8zz75", crypto.InvalidHRPError("qc"), nil, + 0, }, { "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dg8xaf5", bech32m.InvalidChecksumError{Expected: "cdzdfr", Actual: "g8xaf5"}, nil, + 0, }, { "pc1p0hrct7eflrpw4ccrttxzs4qud2axexs2dhdk8", crypto.InvalidLengthError(20), nil, + 0, }, { "pc1y0hrct7eflrpw4ccrttxzs4qud2axex4dksmred", crypto.InvalidAddressTypeError(4), nil, + 0, }, { "PC1P0HRCT7EFLRPW4CCRTTXZS4QUD2AXEX4DCDZDFR", // UPPERCASE nil, - &crypto.Address{ - 0x1, 0x7d, 0xc7, 0x85, 0xfb, 0x29, 0xf8, 0xc2, 0xea, 0xe3, - 0x3, 0x5a, 0xcc, 0x28, 0x54, 0x1c, 0x6a, 0xba, 0x6c, 0x9a, 0xad, + []byte{ + 0x01, 0x7d, 0xc7, 0x85, 0xfb, 0x29, 0xf8, 0xc2, 0xea, 0xe3, + 0x03, 0x5a, 0xcc, 0x28, 0x54, 0x1c, 0x6a, 0xba, 0x6c, 0x9a, 0xad, }, + crypto.AddressTypeValidator, }, { "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr", nil, - &crypto.Address{ - 0x1, 0x7d, 0xc7, 0x85, 0xfb, 0x29, 0xf8, 0xc2, 0xea, 0xe3, - 0x3, 0x5a, 0xcc, 0x28, 0x54, 0x1c, 0x6a, 0xba, 0x6c, 0x9a, 0xad, + []byte{ + 0x01, 0x7d, 0xc7, 0x85, 0xfb, 0x29, 0xf8, 0xc2, 0xea, 0xe3, + 0x03, 0x5a, 0xcc, 0x28, 0x54, 0x1c, 0x6a, 0xba, 0x6c, 0x9a, 0xad, + }, + crypto.AddressTypeValidator, + }, + { + "pc1zzqkzzu4vyddss052as6c37qrdcfptegquw826x", + nil, + []byte{ + 0x02, 0x10, 0x2c, 0x21, 0x72, 0xac, 0x23, 0x5b, 0x08, 0x3e, 0x8a, + 0xec, 0x35, 0x88, 0xf8, 0x03, 0x6e, 0x12, 0x15, 0xe5, 0x00, }, + crypto.AddressTypeBLSAccount, + }, + { + "pc1rspm7ps49gar9ft5g0tkl6lhxs8ygeakq87quh3", + nil, + []byte{ + 0x03, 0x80, 0x77, 0xe0, 0xc2, 0xa5, 0x47, 0x46, 0x54, 0xae, + 0x88, 0x7a, 0xed, 0xfd, 0x7e, 0xe6, 0x81, 0xc8, 0x8c, 0xf6, 0xc0, + }, + crypto.AddressTypeEd25519Account, }, } for no, test := range tests { addr, err := crypto.AddressFromString(test.encoded) if test.err == nil { assert.NoError(t, err, "test %v: unexpected error", no) - assert.Equal(t, *test.result, addr, "test %v: invalid result", no) + assert.Equal(t, test.bytes, addr.Bytes(), "test %v: invalid result", no) assert.Equal(t, strings.ToLower(test.encoded), addr.String(), "test %v: invalid encode", no) + assert.Equal(t, test.addrType, addr.Type(), "test %v: invalid type", no) } else { assert.ErrorIs(t, err, test.err, "test %v: invalid error", no) } @@ -165,6 +200,11 @@ func TestAddressEncoding(t *testing.T) { "02000102030405060708090a0b0c0d0e0f00010203", nil, }, + { + 21, + "03000102030405060708090a0b0c0d0e0f00010203", + nil, + }, } for no, test := range tests { data, _ := hex.DecodeString(test.hex) diff --git a/types/tx/errors.go b/types/tx/errors.go index c5e195efb..a05263e11 100644 --- a/types/tx/errors.go +++ b/types/tx/errors.go @@ -1,6 +1,13 @@ package tx -import "github.com/pactus-project/pactus/types/tx/payload" +import ( + "errors" + + "github.com/pactus-project/pactus/types/tx/payload" +) + +// ErrInvalidSigner is returned when the signer address is not valid. +var ErrInvalidSigner = errors.New("invalid signer address") // BasicCheckError is returned when the basic check on the transaction fails. type BasicCheckError struct { diff --git a/types/tx/tx.go b/types/tx/tx.go index 353087cd1..1e5f87456 100644 --- a/types/tx/tx.go +++ b/types/tx/tx.go @@ -8,6 +8,7 @@ import ( "github.com/fxamacker/cbor/v2" "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/ed25519" "github.com/pactus-project/pactus/crypto/hash" "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx/payload" @@ -250,18 +251,37 @@ func (tx *Tx) UnmarshalCBOR(bs []byte) error { // SerializeSize returns the number of bytes it would take to serialize the transaction. func (tx *Tx) SerializeSize() int { - n := 3 + // one byte version, flag, payload type - 4 + // for tx.LockTime + n := 7 + // flag (1) + version (1) + payload type (1) + lock_time (4) encoding.VarIntSerializeSize(uint64(tx.Fee())) + encoding.VarStringSerializeSize(tx.Memo()) if tx.Payload() != nil { n += tx.Payload().SerializeSize() } if tx.data.Signature != nil { - n += bls.SignatureSize + switch tx.data.Payload.Signer().Type() { + case crypto.AddressTypeValidator, + crypto.AddressTypeBLSAccount: + n += bls.SignatureSize + + case crypto.AddressTypeEd25519Account: + n += ed25519.SignatureSize + + case crypto.AddressTypeTreasury: + n += 0 + } } if tx.data.PublicKey != nil { - n += bls.PublicKeySize + switch tx.data.Payload.Signer().Type() { + case crypto.AddressTypeValidator, + crypto.AddressTypeBLSAccount: + n += bls.PublicKeySize + + case crypto.AddressTypeEd25519Account: + n += ed25519.PublicKeySize + + case crypto.AddressTypeTreasury: + n += 0 + } } return n @@ -360,27 +380,74 @@ func (tx *Tx) Decode(r io.Reader) error { return err } - if !util.IsFlagSet(tx.data.Flags, flagNotSigned) { - sig := new(bls.Signature) - err = sig.Decode(r) + if util.IsFlagSet(tx.data.Flags, flagNotSigned) { + return nil + } + + // It is a signed transaction, Decode signatory. + sig, err := tx.decodeSignature(r) + if err != nil { + return err + } + tx.data.Signature = sig + + if !tx.IsPublicKeyStriped() { + pub, err := tx.decodePublicKey(r) if err != nil { return err } - tx.data.Signature = sig - - if !tx.IsPublicKeyStriped() { - pub := new(bls.PublicKey) - err = pub.Decode(r) - if err != nil { - return err - } - tx.data.PublicKey = pub - } + tx.data.PublicKey = pub } return nil } +func (tx *Tx) decodeSignature(r io.Reader) (crypto.Signature, error) { + switch tx.data.Payload.Signer().Type() { + case crypto.AddressTypeValidator, + crypto.AddressTypeBLSAccount: + sig := new(bls.Signature) + err := sig.Decode(r) + + return sig, err + + case crypto.AddressTypeEd25519Account: + sig := new(ed25519.Signature) + err := sig.Decode(r) + + return sig, err + + case crypto.AddressTypeTreasury: + return nil, ErrInvalidSigner + + default: + return nil, ErrInvalidSigner + } +} + +func (tx *Tx) decodePublicKey(r io.Reader) (crypto.PublicKey, error) { + switch tx.data.Payload.Signer().Type() { + case crypto.AddressTypeValidator, + crypto.AddressTypeBLSAccount: + pub := new(bls.PublicKey) + err := pub.Decode(r) + + return pub, err + + case crypto.AddressTypeEd25519Account: + pub := new(ed25519.PublicKey) + err := pub.Decode(r) + + return pub, err + + case crypto.AddressTypeTreasury: + return nil, ErrInvalidSigner + + default: + return nil, ErrInvalidSigner + } +} + func (tx *Tx) String() string { return fmt.Sprintf("{⌘ %v - %v 🏵 %v}", tx.ID().ShortString(), diff --git a/types/tx/tx_test.go b/types/tx/tx_test.go index 6dd3b4a26..b3893b775 100644 --- a/types/tx/tx_test.go +++ b/types/tx/tx_test.go @@ -7,8 +7,11 @@ import ( "testing" "github.com/fxamacker/cbor/v2" + "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/ed25519" "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/tx/payload" "github.com/pactus-project/pactus/util" @@ -38,11 +41,6 @@ func TestEncodingTx(t *testing.T) { trx3 := ts.GenerateTestUnbondTx() trx4 := ts.GenerateTestWithdrawTx() trx5 := ts.GenerateTestSortitionTx() - assert.True(t, trx1.IsTransferTx()) - assert.True(t, trx2.IsBondTx()) - assert.True(t, trx3.IsUnbondTx()) - assert.True(t, trx4.IsWithdrawTx()) - assert.True(t, trx5.IsSortitionTx()) tests := []*tx.Tx{trx1, trx2, trx3, trx4, trx5} for _, trx := range tests { @@ -113,7 +111,7 @@ func TestBasicCheck(t *testing.T) { t.Run("Invalid payload, Should returns error", func(t *testing.T) { invAddr := ts.RandAccAddress() - invAddr[0] = 3 + invAddr[0] = 4 trx := tx.NewTransferTx(ts.RandHeight(), ts.RandAccAddress(), invAddr, 1e9, ts.RandAmount()) err := trx.BasicCheck() @@ -337,22 +335,22 @@ func TestInvalidSignature(t *testing.T) { }) } -func TestSignBytes(t *testing.T) { +func TestSignBytesBLS(t *testing.T) { d, _ := hex.DecodeString( "00" + // Flags "01" + // Version "01020304" + // LockTime "01" + // Fee - "00" + // Memo + "0474657374" + // Memo "01" + // PayloadType "013333333333333333333333333333333333333333" + // Sender "012222222222222222222222222222222222222222" + // Receiver - "01" + // Amount + "02" + // Amount "b53d79e156e9417e010fa21f2b2a96bee6be46fcd233295d2f697cdb9e782b6112ac01c80d0d9d64c2320664c77fa2a6" + // Signature "8d82fa4fcac04a3b565267685e90db1b01420285d2f8295683c138c092c209479983ba1591370778846681b7b558e061" + // PublicKey "1776208c0718006311c84b4a113335c70d1f5c7c5dd93a5625c4af51c48847abd0b590c055306162d2a03ca1cbf7bcc1") - h, _ := hash.FromString("1a8cedbb2ffce29df63210f112afb1c0295b27e2162323bfc774068f0573388e") + h, _ := hash.FromString("084f69979757cecb58d0a37bdd10eebee912ed29f923adb93f09d6bde2b94d5f") trx, err := tx.FromBytes(d) assert.NoError(t, err) assert.Equal(t, len(d), trx.SerializeSize()) @@ -362,6 +360,39 @@ func TestSignBytes(t *testing.T) { assert.Equal(t, h, trx.ID()) assert.Equal(t, hash.CalcHash(sb), trx.ID()) assert.Equal(t, uint32(0x04030201), trx.LockTime()) + assert.Equal(t, "test", trx.Memo()) + assert.Equal(t, amount.Amount(1), trx.Fee()) + assert.Equal(t, amount.Amount(2), trx.Payload().Value()) +} + +func TestSignBytesEd25519(t *testing.T) { + d, _ := hex.DecodeString( + "00" + // Flags + "01" + // Version + "01020304" + // LockTime + "01" + // Fee + "0474657374" + // Memo + "01" + // PayloadType + "033333333333333333333333333333333333333333" + // Sender + "032222222222222222222222222222222222222222" + // Receiver + "02" + // Amount + "4ed287f380291202f36a6a7516d602f1a6eaf789d092dd4050c0907ce79f49db" + // Signature + "6e70c21c82411803815db09713eab426297210a6793658d6bd9ed116ef2c0aac" + // PublicKey + "0aacf0da469a4a47dfb968a321ad7d6b919fdc37d2d2834c69cef90692730902") + + h, _ := hash.FromString("e5a0e1fb4ee6f26a867dd3c091fc9fdfcbd25a5caff8cf13a4485a716501150d") + trx, err := tx.FromBytes(d) + assert.NoError(t, err) + assert.Equal(t, len(d), trx.SerializeSize()) + + sb := d[1 : len(d)-ed25519.PublicKeySize-ed25519.SignatureSize] + assert.Equal(t, sb, trx.SignBytes()) + assert.Equal(t, h, trx.ID()) + assert.Equal(t, hash.CalcHash(sb), trx.ID()) + assert.Equal(t, uint32(0x04030201), trx.LockTime()) + assert.Equal(t, "test", trx.Memo()) + assert.Equal(t, amount.Amount(1), trx.Fee()) + assert.Equal(t, amount.Amount(2), trx.Payload().Value()) } func TestStripPublicKey(t *testing.T) { @@ -401,3 +432,51 @@ func TestFlagNotSigned(t *testing.T) { trx.SetSignature(nil) assert.False(t, trx.IsSigned(), "FlagNotSigned should not be set when the signature is set to nil") } + +func TestInvalidSignerSignature(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + trx := tx.NewTransferTx(ts.RandHeight(), crypto.TreasuryAddress, ts.RandAccAddress(), + ts.RandAmount(), ts.RandAmount()) + trx.SetSignature(ts.RandBLSSignature()) + + bytes, _ := trx.Bytes() + _, err := tx.FromBytes(bytes) + assert.ErrorIs(t, err, tx.ErrInvalidSigner) +} + +func TestInvalidSignerPublicKey(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + trx := tx.NewTransferTx(ts.RandHeight(), crypto.TreasuryAddress, ts.RandAccAddress(), + ts.RandAmount(), ts.RandAmount()) + pub, _ := ts.RandBLSKeyPair() + trx.SetSignature(ts.RandBLSSignature()) + trx.SetPublicKey(pub) + + bytes, _ := trx.Bytes() + _, err := tx.FromBytes(bytes) + assert.ErrorIs(t, err, tx.ErrInvalidSigner) +} + +func TestIsFreeTx(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + trx1 := ts.GenerateTestTransferTx() + trx2 := ts.GenerateTestBondTx() + trx3 := ts.GenerateTestUnbondTx() + trx4 := ts.GenerateTestWithdrawTx() + trx5 := ts.GenerateTestSortitionTx() + + assert.True(t, trx1.IsTransferTx()) + assert.True(t, trx2.IsBondTx()) + assert.True(t, trx3.IsUnbondTx()) + assert.True(t, trx4.IsWithdrawTx()) + assert.True(t, trx5.IsSortitionTx()) + + assert.False(t, trx1.IsFreeTx()) + assert.False(t, trx2.IsFreeTx()) + assert.True(t, trx3.IsFreeTx()) + assert.False(t, trx4.IsFreeTx()) + assert.True(t, trx5.IsFreeTx()) +} diff --git a/util/testsuite/testsuite.go b/util/testsuite/testsuite.go index 3ab7d7bfc..1b8237161 100644 --- a/util/testsuite/testsuite.go +++ b/util/testsuite/testsuite.go @@ -280,9 +280,12 @@ func (ts *TestSuite) RandHash() hash.Hash { // RandAccAddress generates a random account address for testing purposes. func (ts *TestSuite) RandAccAddress() crypto.Address { - addr := crypto.NewAddress(crypto.AddressTypeBLSAccount, ts.RandBytes(20)) + isBLSAddress := ts.RandBool() + if isBLSAddress { + return crypto.NewAddress(crypto.AddressTypeBLSAccount, ts.RandBytes(20)) + } - return addr + return crypto.NewAddress(crypto.AddressTypeEd25519Account, ts.RandBytes(20)) } // RandValAddress generates a random validator address for testing purposes. @@ -320,11 +323,11 @@ func (ts *TestSuite) RandPeerID() peer.ID { // GenerateTestAccount generates an account for testing purposes. func (ts *TestSuite) GenerateTestAccount(number int32) (*account.Account, crypto.Address) { - _, prv := ts.RandBLSKeyPair() + addr := ts.RandAccAddress() acc := account.NewAccount(number) acc.AddToBalance(ts.RandAmount()) - return acc, prv.PublicKeyNative().AccountAddress() + return acc, addr } // GenerateTestValidator generates a validator for testing purposes. diff --git a/wallet/vault/vault.go b/wallet/vault/vault.go index 6b928bc51..7dfe205d0 100644 --- a/wallet/vault/vault.go +++ b/wallet/vault/vault.go @@ -7,7 +7,9 @@ import ( "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/crypto/bls" - "github.com/pactus-project/pactus/crypto/bls/hdkeychain" + blshdkeychain "github.com/pactus-project/pactus/crypto/bls/hdkeychain" + "github.com/pactus-project/pactus/crypto/ed25519" + ed25519hdkeychain "github.com/pactus-project/pactus/crypto/ed25519/hdkeychain" "github.com/pactus-project/pactus/wallet/addresspath" "github.com/pactus-project/pactus/wallet/encrypter" "github.com/tyler-smith/go-bip39" @@ -15,25 +17,41 @@ import ( ) // -// Deterministic Hierarchy derivation path +// Deterministic Hierarchical Derivation Path // -// Specification +// Overview: // -// We define the following 4 levels in BIP32 path: +// This specification defines a hierarchical derivation path for generating addresses, based on BIP32. +// The path is structured into four distinct levels: // // m / purpose' / coin_type' / address_type' / address_index // -// Where: -// `'` Apostrophe in the path indicates that BIP32 hardened derivation is used. +// Explanation: +// // `m` Denotes the master node (or root) of the tree +// `'` Apostrophe in the path indicates that BIP32 hardened derivation is used. // `/` Separates the tree into depths, thus i / j signifies that j is a child of i -// `purpose` is set to 12381 which is the name of the new curve (BLS12-381). -// `coin_type` is set 21888 for Mainnet, 21777 for Testnet -// `address_type` determine the type of address -// `address_index` is a sequential number and increase when a new address is derived. +// +// Path Components: +// +// * `purpose`: Indicates the specific use case for the derived addresses: +// - 12381: Used for the BLS12-381 curve, based on PIP-8. +// - 65535: Used for imported private keys, based on PIP-13. +// - 44: A comprehensive purpose for standard curves, based on BIP-44. +// +// * `coin_type`: Identifies the coin type: +// - 21888: Pactus Mainnet +// - 21777: Pactus Testnet +// +// * `address_type`: Specifies the type of address. +// +// * `address_index`: A sequential number and increase when a new address is derived. // // References: -// PIP-8: https://pips.pactus.org/PIPs/pip-8 +// - https://pips.pactus.org/PIPs/pip-8 +// - https://pips.pactus.org/PIPs/pip-13 +// - https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki +// const ( TypeFull = int(1) @@ -49,6 +67,7 @@ type AddressInfo struct { const ( PurposeBLS12381 = uint32(12381) + PurposeBIP44 = uint32(44) PurposeImportPrivateKey = uint32(65535) ) @@ -71,7 +90,8 @@ type masterNode struct { } type purposes struct { - PurposeBLS purposeBLS `json:"purpose_bls"` // BLS Purpose: m/12381'/21888/0'/0' + PurposeBLS purposeBLS `json:"purpose_bls"` // BLS Purpose: m/12381'/21888'/1' or 2'/0 + PurposeEd25519 purposeEd25519 `json:"purpose_ed25519"` // ED25519 Purpose: m/44'/21888'/3'/0 } type purposeBLS struct { @@ -81,12 +101,16 @@ type purposeBLS struct { NextValidatorIndex uint32 `json:"next_validator_index"` // Index of next derived validator } +type purposeEd25519 struct { + NextAccountIndex uint32 `json:"next_account_index"` // Index of next derived account +} + func CreateVaultFromMnemonic(mnemonic string, coinType uint32) (*Vault, error) { seed, err := bip39.NewSeedWithErrorChecking(mnemonic, "") if err != nil { return nil, err } - masterKey, err := hdkeychain.NewMaster(seed, false) + masterKey, err := blshdkeychain.NewMaster(seed, false) if err != nil { return nil, err } @@ -396,31 +420,19 @@ func (v *Vault) PrivateKeys(password string, addrs []string) ([]crypto.PrivateKe switch path.Purpose() { case H(PurposeBLS12381): - seed, err := bip39.NewSeedWithErrorChecking(keyStore.MasterNode.Mnemonic, "") + prvKey, err := bls12381PrivateKey(keyStore, path) if err != nil { return nil, err } - masterKey, err := hdkeychain.NewMaster(seed, false) - if err != nil { - return nil, err - } - ext, err := masterKey.DerivePath(path) - if err != nil { - return nil, err - } - prvBytes, err := ext.RawPrivateKey() - if err != nil { - return nil, err - } - - prvKey, err := bls.PrivateKeyFromBytes(prvBytes) + keys[i] = prvKey + case H(PurposeBIP44): + prvKey, err := bip44PrivateKey(keyStore, path, password) if err != nil { return nil, err } - keys[i] = prvKey case H(PurposeImportPrivateKey): - index := path.AddressIndex() - hdkeychain.HardenedKeyStart + index := path.AddressIndex() - blshdkeychain.HardenedKeyStart // TODO: index out of range check str := keyStore.ImportedKeys[index] prv, err := bls.PrivateKeyFromString(str) @@ -437,7 +449,7 @@ func (v *Vault) PrivateKeys(password string, addrs []string) ([]crypto.PrivateKe } func (v *Vault) NewBLSAccountAddress(label string) (*AddressInfo, error) { - ext, err := hdkeychain.NewKeyFromString(v.Purposes.PurposeBLS.XPubAccount) + ext, err := blshdkeychain.NewKeyFromString(v.Purposes.PurposeBLS.XPubAccount) if err != nil { return nil, err } @@ -465,8 +477,53 @@ func (v *Vault) NewBLSAccountAddress(label string) (*AddressInfo, error) { return &data, nil } +func (v *Vault) NewEd25519AccountAddress(label, password string) (*AddressInfo, error) { + mnemonic, err := v.Mnemonic(password) + if err != nil { + return nil, err + } + + seed, err := bip39.NewSeedWithErrorChecking(mnemonic, password) + if err != nil { + return nil, err + } + + masterKey, err := ed25519hdkeychain.NewMaster(seed) + if err != nil { + return nil, err + } + + index := v.Purposes.PurposeEd25519.NextAccountIndex + ext, err := masterKey.DerivePath([]uint32{ + H(PurposeBIP44), + H(v.CoinType), + H(crypto.AddressTypeEd25519Account), + H(index), + }) + if err != nil { + return nil, err + } + + ed25519PubKey, err := ed25519.PublicKeyFromBytes(ext.RawPublicKey()) + if err != nil { + return nil, err + } + + addr := ed25519PubKey.AccountAddress().String() + data := AddressInfo{ + Address: addr, + Label: label, + PublicKey: ed25519PubKey.String(), + Path: addresspath.NewPath(ext.Path()...).String(), + } + v.Addresses[addr] = data + v.Purposes.PurposeEd25519.NextAccountIndex++ + + return &data, nil +} + func (v *Vault) NewValidatorAddress(label string) (*AddressInfo, error) { - ext, err := hdkeychain.NewKeyFromString(v.Purposes.PurposeBLS.XPubValidator) + ext, err := blshdkeychain.NewKeyFromString(v.Purposes.PurposeBLS.XPubValidator) if err != nil { return nil, err } @@ -527,7 +584,7 @@ func (v *Vault) AddressInfo(addr string) *AddressInfo { xPub = v.Purposes.PurposeBLS.XPubValidator } - ext, err := hdkeychain.NewKeyFromString(xPub) + ext, err := blshdkeychain.NewKeyFromString(xPub) if err != nil { return nil } @@ -548,6 +605,8 @@ func (v *Vault) AddressInfo(addr string) *AddressInfo { } info.PublicKey = blsPubKey.String() + case H(PurposeBIP44): + return &info case H(PurposeImportPrivateKey): default: return nil @@ -612,3 +671,42 @@ func (v *Vault) encryptKeyStore(keyStore *keyStore, password string) error { return nil } + +func bls12381PrivateKey(ks *keyStore, path []uint32) (*bls.PrivateKey, error) { + seed, err := bip39.NewSeedWithErrorChecking(ks.MasterNode.Mnemonic, "") + if err != nil { + return nil, err + } + masterKey, err := blshdkeychain.NewMaster(seed, false) + if err != nil { + return nil, err + } + ext, err := masterKey.DerivePath(path) + if err != nil { + return nil, err + } + prvBytes, err := ext.RawPrivateKey() + if err != nil { + return nil, err + } + + return bls.PrivateKeyFromBytes(prvBytes) +} + +func bip44PrivateKey(ks *keyStore, path []uint32, password string) (*ed25519.PrivateKey, error) { + seed, err := bip39.NewSeedWithErrorChecking(ks.MasterNode.Mnemonic, password) + if err != nil { + return nil, err + } + masterKey, err := ed25519hdkeychain.NewMaster(seed) + if err != nil { + return nil, err + } + ext, err := masterKey.DerivePath(path) + if err != nil { + return nil, err + } + prvBytes := ext.RawPrivateKey() + + return ed25519.PrivateKeyFromBytes(prvBytes) +} diff --git a/wallet/vault/vault_test.go b/wallet/vault/vault_test.go index 627b19428..7fc662bc5 100644 --- a/wallet/vault/vault_test.go +++ b/wallet/vault/vault_test.go @@ -231,6 +231,28 @@ func TestNewBLSAccountAddress(t *testing.T) { assert.Equal(t, label, addressInfo.Label) } +func TestNewED25519AccountAddress(t *testing.T) { + td := setup(t) + + addressInfo, err := td.vault.NewEd25519AccountAddress("addr-1", tPassword) + assert.NoError(t, err) + assert.NotEmpty(t, addressInfo.Address) + assert.NotEmpty(t, addressInfo.PublicKey) + assert.Equal(t, "m/44'/21888'/3'/0'", addressInfo.Path) + + addressInfo, err = td.vault.NewEd25519AccountAddress("addr-2", tPassword) + assert.NoError(t, err) + assert.NotEmpty(t, addressInfo.Address) + assert.NotEmpty(t, addressInfo.PublicKey) + assert.Equal(t, "m/44'/21888'/3'/1'", addressInfo.Path) + + addressInfo, err = td.vault.NewEd25519AccountAddress("addr-3", tPassword) + assert.NoError(t, err) + assert.NotEmpty(t, addressInfo.Address) + assert.NotEmpty(t, addressInfo.PublicKey) + assert.Equal(t, "m/44'/21888'/3'/2'", addressInfo.Path) +} + func TestNewValidatorAddress(t *testing.T) { td := setup(t) diff --git a/wallet/wallet.go b/wallet/wallet.go index ed54e7c71..ba6e3257f 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -19,8 +19,14 @@ import ( ) const ( - AddressTypeBLSAccount string = "bls_account" - AddressTypeValidator string = "validator" + AddressTypeBLSAccount string = "bls_account" + AddressTypeEd25519Account string = "ed25519_account" + AddressTypeValidator string = "validator" +) + +const ( + Version1 = 1 // initial version + Version2 = 2 // supporting Ed25519 = ) type Wallet struct { @@ -95,7 +101,7 @@ func Create(walletPath, mnemonic, password string, chain genesis.ChainType, opti } store := &store{ - Version: 1, + Version: Version2, UUID: uuid.New(), CreatedAt: time.Now().Round(time.Second).UTC(), Network: chain, @@ -402,6 +408,13 @@ func (w *Wallet) NewBLSAccountAddress(label string) (*vault.AddressInfo, error) return w.store.Vault.NewBLSAccountAddress(label) } +// NewEd25519AccountAddress create a new Ed25519-based account address and +// associates it with the given label. +// The password is required to access the master private key needed for address generation. +func (w *Wallet) NewEd25519AccountAddress(label, password string) (*vault.AddressInfo, error) { + return w.store.Vault.NewEd25519AccountAddress(label, password) +} + // NewValidatorAddress creates a new BLS validator address and // associates it with the given label. func (w *Wallet) NewValidatorAddress(label string) (*vault.AddressInfo, error) { diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 9704b91fc..53ebb25b1 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -257,7 +257,7 @@ func TestStake(t *testing.T) { }) } -func TestSigningTx(t *testing.T) { +func TestSigningTxWithBLS(t *testing.T) { td := setup(t) defer td.Close() @@ -286,6 +286,35 @@ func TestSigningTx(t *testing.T) { assert.Equal(t, fee, trx.Fee()) } +func TestSigningTxWithEd25519(t *testing.T) { + td := setup(t) + defer td.Close() + + senderInfo, _ := td.wallet.NewEd25519AccountAddress("testing addr", td.password) + receiver := td.RandAccAddress() + amt := td.RandAmount() + fee := td.RandFee() + lockTime := td.RandHeight() + + opts := []wallet.TxOption{ + wallet.OptionFee(fee), + wallet.OptionLockTime(lockTime), + wallet.OptionMemo("test"), + } + + trx, err := td.wallet.MakeTransferTx(senderInfo.Address, receiver.String(), amt, opts...) + assert.NoError(t, err) + err = td.wallet.SignTransaction(td.password, trx) + assert.NoError(t, err) + assert.NotNil(t, trx.Signature()) + assert.NoError(t, trx.BasicCheck()) + + id, err := td.wallet.BroadcastTransaction(trx) + assert.NoError(t, err) + assert.Equal(t, trx.ID().String(), id) + assert.Equal(t, fee, trx.Fee()) +} + func TestMakeTransferTx(t *testing.T) { td := setup(t) defer td.Close()