Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[account] Improve account experience #125

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/e
- [`Fix`] Make NodeClient match AptosRpcClient interface
- [`Dependency`] Update `golang.org/x/crypto` to `v0.32.0`
- [`Dependency`] Update `github.com/hasura/go-graphql-client` to `v0.13.1`
- [`Fix`] Make NodeClient satisfy AptosRpcClient interface
- [`Breaking`] Change Account signer input to use Address instead of AuthKey, authkey comes from the private key

# v1.4.1 (01/02/2024)

Expand Down
4 changes: 2 additions & 2 deletions account.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ var AccountThree = types.AccountThree
var AccountFour = types.AccountFour

// NewAccountFromSigner creates an account from a Signer, which is most commonly a private key
func NewAccountFromSigner(signer crypto.Signer, authKey ...crypto.AuthenticationKey) (*Account, error) {
return types.NewAccountFromSigner(signer, authKey...)
func NewAccountFromSigner(signer crypto.Signer, accountAddress ...AccountAddress) (*Account, error) {
return types.NewAccountFromSigner(signer, accountAddress...)
}

// NewEd25519Account creates a legacy Ed25519 account, this is most commonly used in wallets
Expand Down
44 changes: 40 additions & 4 deletions internal/types/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ type Account struct {
}

// NewAccountFromSigner creates an account from a [crypto.Signer] with an optional [crypto.AuthenticationKey]
func NewAccountFromSigner(signer crypto.Signer, authKey ...crypto.AuthenticationKey) (*Account, error) {
func NewAccountFromSigner(signer crypto.Signer, address ...AccountAddress) (*Account, error) {
out := &Account{}
if len(authKey) == 1 {
copy(out.Address[:], authKey[0][:])
} else if len(authKey) > 1 {
if len(address) == 1 {
copy(out.Address[:], address[0][:])
} else if len(address) > 1 {
// Throw error
return nil, errors.New("must only provide one auth key")
} else {
Expand Down Expand Up @@ -60,6 +60,42 @@ func NewSecp256k1Account() (*Account, error) {
return NewAccountFromSigner(signer)
}

// ExtractMessageSigner extracts the message signer from the account for

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is for needed?

func (account *Account) ExtractMessageSigner() (crypto.MessageSigner, bool) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth returning error here as opposed to bool for consistency?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the same consistency topic, do we need the Extract prefix in these new funcs? Other places in the sdk we use noun phrases for these derived values/attributes

ed25519PrivateKey, ok := account.Signer.(*crypto.Ed25519PrivateKey)
if ok {
return ed25519PrivateKey, ok
}
singleSigner, ok := account.Signer.(*crypto.SingleSigner)
if ok {
return singleSigner.Signer, ok
}
return nil, false
}

// ExtractPrivateKeyString extracts the private key string
func (account *Account) ExtractPrivateKeyString() (string, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth exposing the PrivateKey as a byte slice of the raw bytes instead of string? Alternatively, update the doc to indicate that the func returns a AIP-80 compliant string

// Handle the key by itself
ed25519PrivateKey, ok := account.Signer.(*crypto.Ed25519PrivateKey)
if ok {
return ed25519PrivateKey.ToAIP80()
}

// Handle key in single signer
singleSigner, ok := account.Signer.(*crypto.SingleSigner)
if ok {
innerSigner := singleSigner.Signer
switch innerSigner.(type) {
case *crypto.Ed25519PrivateKey:
return innerSigner.(*crypto.Ed25519PrivateKey).ToAIP80()
case *crypto.Secp256k1PrivateKey:
return innerSigner.(*crypto.Secp256k1PrivateKey).ToAIP80()
}
}

return "", errors.New("signer is not a private key")
}

// Sign signs a message, returning an appropriate authenticator for the signer
func (account *Account) Sign(message []byte) (authenticator *crypto.AccountAuthenticator, err error) {
return account.Signer.Sign(message)
Expand Down
8 changes: 8 additions & 0 deletions internal/types/accountAddress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package types
import (
"encoding/json"
"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/crypto"
"github.com/stretchr/testify/assert"
"testing"
)
Expand All @@ -19,6 +20,13 @@ func TestAccountSpecialString(t *testing.T) {
assert.Equal(t, aa, aa2)
}

func TestAccountAddress_AuthKey(t *testing.T) {
authKey := &crypto.AuthenticationKey{}
var aa AccountAddress
aa.FromAuthKey(authKey)
assert.Equal(t, AccountZero, aa)
}

func TestSpecialAddresses(t *testing.T) {
var addr AccountAddress
err := addr.ParseStringRelaxed("0x0")
Expand Down
127 changes: 122 additions & 5 deletions internal/types/account_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"errors"
"github.com/aptos-labs/aptos-go-sdk/crypto"
"github.com/stretchr/testify/assert"
"testing"
Expand All @@ -22,6 +23,26 @@ func TestGenerateEd25519Account(t *testing.T) {
assert.True(t, output.Auth.Verify(message))
}

func TestGenerateSingleSignerEd25519Account(t *testing.T) {
message := []byte{0x12, 0x34}
account, err := NewEd25519SingleSignerAccount()
assert.NoError(t, err)
output, err := account.Sign(message)
assert.NoError(t, err)
assert.Equal(t, crypto.AccountAuthenticatorSingleSender, output.Variant)
assert.True(t, output.Auth.Verify(message))
}

func TestGenerateSecp256k1Account(t *testing.T) {
message := []byte{0x12, 0x34}
account, err := NewSecp256k1Account()
assert.NoError(t, err)
output, err := account.Sign(message)
assert.NoError(t, err)
assert.Equal(t, crypto.AccountAuthenticatorSingleSender, output.Variant)
assert.True(t, output.Auth.Verify(message))
}

func TestNewAccountFromSigner(t *testing.T) {
message := []byte{0x12, 0x34}
key, err := crypto.GenerateEd25519PrivateKey()
Expand All @@ -43,24 +64,120 @@ func TestNewAccountFromSignerWithAddress(t *testing.T) {
key, err := crypto.GenerateEd25519PrivateKey()
assert.NoError(t, err)

authenticationKey := crypto.AuthenticationKey{}

account, err := NewAccountFromSigner(key, authenticationKey)
account, err := NewAccountFromSigner(key, AccountZero)
assert.NoError(t, err)
output, err := account.Sign(message)
assert.NoError(t, err)
assert.Equal(t, crypto.AccountAuthenticatorEd25519, output.Variant)
assert.True(t, output.Auth.Verify(message))

outputSig, err := account.SignMessage(message)
assert.NoError(t, err)
assert.True(t, account.Signer.PubKey().Verify(message, outputSig))

assert.Equal(t, AccountZero, account.Address)
assert.Equal(t, AccountZero, account.AccountAddress())
assert.Equal(t, key.AuthKey(), account.AuthKey())
assert.Equal(t, key.PubKey(), account.PubKey())
}

func TestNewAccountFromSignerWithAddressMulti(t *testing.T) {
key, err := crypto.GenerateEd25519PrivateKey()
assert.NoError(t, err)

authenticationKey := crypto.AuthenticationKey{}
_, err = NewAccountFromSigner(key, AccountZero, AccountOne)
assert.Error(t, err)
}

type WrapperSigner struct {
signer crypto.Signer
}

func (w *WrapperSigner) Sign(_ []byte) (*crypto.AccountAuthenticator, error) {
return nil, errors.New("not implemented")
}
func (w *WrapperSigner) SignMessage(_ []byte) (crypto.Signature, error) {
return nil, errors.New("not implemented")
}
func (w *WrapperSigner) SimulationAuthenticator() *crypto.AccountAuthenticator {
return nil
}
func (w *WrapperSigner) AuthKey() *crypto.AuthenticationKey {
return &crypto.AuthenticationKey{}
}
func (w *WrapperSigner) PubKey() crypto.PublicKey {
// Note this is just for testing
return &crypto.Ed25519PublicKey{}
}

func TestAccount_ExtractMessageSigner(t *testing.T) {
ed25519PrivateKey, err := crypto.GenerateEd25519PrivateKey()
assert.NoError(t, err)
ed25519Account, err := NewAccountFromSigner(ed25519PrivateKey)
assert.NoError(t, err)

ed25519Out, ok := ed25519Account.ExtractMessageSigner()
assert.True(t, ok)
assert.Equal(t, ed25519PrivateKey, ed25519Out)

ed25519SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(ed25519PrivateKey))
assert.NoError(t, err)

ed25519Out, ok = ed25519SingleSignerAccount.ExtractMessageSigner()
assert.True(t, ok)
assert.Equal(t, ed25519PrivateKey, ed25519Out)

secp256k1PrivateKey, err := crypto.GenerateSecp256k1Key()
assert.NoError(t, err)
secp256k1SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(secp256k1PrivateKey))
assert.NoError(t, err)

secp256k1Out, ok := secp256k1SingleSignerAccount.ExtractMessageSigner()
assert.True(t, ok)
assert.Equal(t, secp256k1PrivateKey, secp256k1Out)

wrapperSigner := &WrapperSigner{signer: secp256k1SingleSignerAccount}
customAccount, err := NewAccountFromSigner(wrapperSigner)
assert.NoError(t, err)
out, ok := customAccount.ExtractMessageSigner()
assert.False(t, ok)
assert.Nil(t, out)
}

func TestAccount_ExtractPrivateKeyString(t *testing.T) {
ed25519PrivateKey, err := crypto.GenerateEd25519PrivateKey()
assert.NoError(t, err)
ed25519Account, err := NewAccountFromSigner(ed25519PrivateKey)
assert.NoError(t, err)

_, err = NewAccountFromSigner(key, authenticationKey, authenticationKey)
ed25519KeyString, err := ed25519Account.ExtractPrivateKeyString()
assert.NoError(t, err)
expectedEd25519String, err := ed25519PrivateKey.ToAIP80()
assert.NoError(t, err)
assert.Equal(t, expectedEd25519String, ed25519KeyString)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add another test case to verify AIP 80

assert.True(t, strings.HasPrefix(ed25519KeyString, "ed25519-priv-"))


ed25519SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(ed25519PrivateKey))
assert.NoError(t, err)

ed25519SingleSignerKeyString, err := ed25519SingleSignerAccount.ExtractPrivateKeyString()
assert.NoError(t, err)
assert.Equal(t, expectedEd25519String, ed25519SingleSignerKeyString)

secp256k1PrivateKey, err := crypto.GenerateSecp256k1Key()
assert.NoError(t, err)
secp256k1SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(secp256k1PrivateKey))
assert.NoError(t, err)

expectedSecp256k1String, err := secp256k1PrivateKey.ToAIP80()
assert.NoError(t, err)
secp256k1SingleSignerKeyString, err := secp256k1SingleSignerAccount.ExtractPrivateKeyString()
assert.NoError(t, err)
assert.Equal(t, expectedSecp256k1String, secp256k1SingleSignerKeyString)

wrapperSigner := &WrapperSigner{signer: secp256k1SingleSignerAccount}
customAccount, err := NewAccountFromSigner(wrapperSigner)
assert.NoError(t, err)
out, err := customAccount.ExtractPrivateKeyString()
assert.Error(t, err)
assert.Empty(t, out)
}
15 changes: 1 addition & 14 deletions internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ package util

import (
"encoding/hex"
"encoding/json"
"fmt"
"golang.org/x/crypto/sha3"
"math/big"
"strconv"
"strings"
)

// SHA3256Hash hashes the input bytes using SHA3-256
// Sha3256Hash hashes the input bytes using SHA3-256
func Sha3256Hash(bytes [][]byte) (output []byte) {
hasher := sha3.New256()
for _, b := range bytes {
Expand Down Expand Up @@ -50,15 +49,3 @@ func StrToBigInt(val string) (num *big.Int, err error) {
}
return num, nil
}

// PrettyJson a simple pretty print for JSON examples
func PrettyJson(x any) string {
out := strings.Builder{}
enc := json.NewEncoder(&out)
enc.SetIndent("", " ")
err := enc.Encode(x)
if err != nil {
return ""
}
return out.String()
}