Skip to content

Commit

Permalink
Merge branch 'main' into add-secp256k1-recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
gregnazario authored Nov 25, 2024
2 parents ed18c1e + 5606ce9 commit ab67fa9
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 45 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/e
- [`Breaking`] Add checks for malleability to prevent duplicate secp256k1 signatures in verification and to ensure
correct on-chain behavior
- Adds functionality to recover public keys from secp256k1 signatures
- Add AIP-80 support for Ed25519 and Secp256k1 private keys
- Add support for optional ledger version in FA functions and APT balance

# v1.2.0 (11/15/2024)

Expand Down
6 changes: 3 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ type AptosRpcClient interface {
EstimateGasPrice() (info EstimateGasInfo, err error)

// AccountAPTBalance retrieves the APT balance in the account
AccountAPTBalance(address AccountAddress) (uint64, error)
AccountAPTBalance(address AccountAddress, ledgerVersion ...uint64) (uint64, error)

// NodeAPIHealthCheck checks if the node is within durationSecs of the current time, if not provided the node default is used
NodeAPIHealthCheck(durationSecs ...uint64) (api.HealthCheckResponse, error)
Expand Down Expand Up @@ -825,8 +825,8 @@ func (client *Client) EstimateGasPrice() (info EstimateGasInfo, err error) {
}

// AccountAPTBalance retrieves the APT balance in the account
func (client *Client) AccountAPTBalance(address AccountAddress) (uint64, error) {
return client.nodeClient.AccountAPTBalance(address)
func (client *Client) AccountAPTBalance(address AccountAddress, ledgerVersion ...uint64) (uint64, error) {
return client.nodeClient.AccountAPTBalance(address, ledgerVersion...)
}

// QueryIndexer queries the indexer using GraphQL to fill the `query` struct with data. See examples in the indexer client on how to make queries
Expand Down
14 changes: 12 additions & 2 deletions crypto/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"crypto/ed25519"
"errors"
"fmt"
"io"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/hdevalence/ed25519consensus"
"io"
)

//region Ed25519PrivateKey
Expand Down Expand Up @@ -151,6 +152,10 @@ func (key *Ed25519PrivateKey) Bytes() []byte {
// Implements:
// - [CryptoMaterial]
func (key *Ed25519PrivateKey) FromBytes(bytes []byte) (err error) {
bytes, err = ParsePrivateKey(bytes, PrivateKeyVariantEd25519, false)
if err != nil {
return err
}
if len(bytes) != ed25519.SeedSize {
return fmt.Errorf("invalid ed25519 private key size %d", len(bytes))
}
Expand All @@ -166,14 +171,19 @@ func (key *Ed25519PrivateKey) ToHex() string {
return util.BytesToHex(key.Bytes())
}

// ToAIP80 formats the private key to AIP-80 compliant string
func (key *Ed25519PrivateKey) ToAIP80() (formattedString string, err error) {
return FormatPrivateKey(key.ToHex(), PrivateKeyVariantEd25519)
}

// FromHex sets the [Ed25519PrivateKey] to the bytes represented by the hex string, with or without a leading 0x
//
// Errors if the hex string is not valid, or if the bytes length is not [ed25519.SeedSize].
//
// Implements:
// - [CryptoMaterial]
func (key *Ed25519PrivateKey) FromHex(hexStr string) (err error) {
bytes, err := util.ParseHex(hexStr)
bytes, err := ParsePrivateKey(hexStr, PrivateKeyVariantEd25519)
if err != nil {
return err
}
Expand Down
13 changes: 9 additions & 4 deletions crypto/ed25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package crypto

import (
"crypto/ed25519"
"testing"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/stretchr/testify/assert"
"testing"
)

const testEd25519PrivateKey = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PrivateKey = "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PrivateKeyHex = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PublicKey = "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c"
const testEd25519Address = "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa"
const testEd25519Message = "0x68656c6c6f20776f726c64"
Expand All @@ -18,7 +20,7 @@ func TestEd25519Keys(t *testing.T) {
testEd25519PrivateKeyBytes := []byte{0xc5, 0x33, 0x8c, 0xd2, 0x51, 0xc2, 0x2d, 0xaa, 0x8c, 0x9c, 0x9c, 0xc9, 0x4f, 0x49, 0x8c, 0xc8, 0xa5, 0xc7, 0xe1, 0xd2, 0xe7, 0x52, 0x87, 0xa5, 0xdd, 0xa9, 0x10, 0x96, 0xfe, 0x64, 0xef, 0xa5}

// First ensure bytes and hex are the same
readBytes, err := util.ParseHex(testEd25519PrivateKey)
readBytes, err := util.ParseHex(testEd25519PrivateKeyHex)
assert.NoError(t, err)
assert.Equal(t, testEd25519PrivateKeyBytes, readBytes)

Expand All @@ -33,7 +35,10 @@ func TestEd25519Keys(t *testing.T) {

// The outputs should match as well
assert.Equal(t, privateKey.Bytes(), testEd25519PrivateKeyBytes)
assert.Equal(t, privateKey.ToHex(), testEd25519PrivateKey)
assert.Equal(t, privateKey.ToHex(), testEd25519PrivateKeyHex)
formattedString, err := privateKey.ToAIP80()
assert.NoError(t, err)
assert.Equal(t, formattedString, testEd25519PrivateKey)

// Auth key should match
assert.Equal(t, testEd25519Address, privateKey.AuthKey().ToHex())
Expand Down
93 changes: 93 additions & 0 deletions crypto/privateKey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package crypto

import (
"fmt"
"strings"

"github.com/aptos-labs/aptos-go-sdk/internal/util"
)

// PrivateKeyVariant represents the type of private key
type PrivateKeyVariant string

const (
PrivateKeyVariantEd25519 PrivateKeyVariant = "ed25519"
PrivateKeyVariantSecp256k1 PrivateKeyVariant = "secp256k1"
)

// AIP80Prefixes contains the AIP-80 compliant prefixes for each private key type
var AIP80Prefixes = map[PrivateKeyVariant]string{
PrivateKeyVariantEd25519: "ed25519-priv-",
PrivateKeyVariantSecp256k1: "secp256k1-priv-",
}

// FormatPrivateKey formats a hex input to an AIP-80 compliant string
func FormatPrivateKey(privateKey any, keyType PrivateKeyVariant) (formattedString string, err error) {
aip80Prefix := AIP80Prefixes[keyType]

var hexStr string
switch v := privateKey.(type) {
case string:
// Remove the prefix if it exists
if strings.HasPrefix(v, aip80Prefix) {
parts := strings.Split(v, "-")
v = parts[2]
}

// If it's already a string, just ensure it's properly formatted
var strBytes, err = util.ParseHex(v)
if err != nil {
return "", err
}

// Reformat to have 0x prefix
hexStr = util.BytesToHex(strBytes)
case []byte:
hexStr = util.BytesToHex(v)
default:
return "", fmt.Errorf("unsupported private key type: must be string or []byte")
}

return fmt.Sprintf("%s%s", aip80Prefix, hexStr), nil
}

// ParseHexInput parses a hex input that may be bytes, hex string, or an AIP-80 compliant string to bytes.
//
// You may optionally pass in a boolean to strictly enforce AIP-80 compliance.
func ParsePrivateKey(value any, keyType PrivateKeyVariant, strict ...bool) (bytes []byte, err error) {
aip80Prefix := AIP80Prefixes[keyType]

// Get the first boolean if it exists, otherwise nil
var strictness *bool = nil
if len(strict) > 1 {
return nil, fmt.Errorf("strictness must be a single boolean")
} else if len(strict) == 1 {
strictness = &strict[0]
}

switch v := value.(type) {
case string:
if (strictness == nil || !*strictness) && !strings.HasPrefix(v, aip80Prefix) {
bytes, err := util.ParseHex(v)
if err != nil {
return nil, err
}

// If strictness is not explicitly false, warn about non-AIP-80 compliance
if strictness == nil {
fmt.Printf("[Aptos SDK] It is recommended that private keys are AIP-80 compliant (https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md). You can fix the private key by formatting it with crypto.FormatPrivateKey")
}

return bytes, nil
} else if strings.HasPrefix(v, aip80Prefix) {
// Parse for AIP-80 compliant String input
parts := strings.Split(v, "-")
return util.ParseHex(parts[2])
}
return nil, fmt.Errorf("invalid hex string input while parsing private key. Must be AIP-80 compliant string")
case []byte:
return v, nil
default:
return nil, fmt.Errorf("unsupported private key type: must be string or []byte")
}
}
12 changes: 11 additions & 1 deletion crypto/secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package crypto

import (
"fmt"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
Expand Down Expand Up @@ -90,6 +91,10 @@ func (key *Secp256k1PrivateKey) Bytes() []byte {
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PrivateKey) FromBytes(bytes []byte) (err error) {
bytes, err = ParsePrivateKey(bytes, PrivateKeyVariantSecp256k1, false)
if err != nil {
return err
}
if len(bytes) != Secp256k1PrivateKeyLength {
return fmt.Errorf("invalid secp256k1 private key size %d", len(bytes))
}
Expand All @@ -105,6 +110,11 @@ func (key *Secp256k1PrivateKey) ToHex() string {
return util.BytesToHex(key.Bytes())
}

// ToAIP80 formats the private key to AIP-80 compliant string
func (key *Secp256k1PrivateKey) ToAIP80() (formattedString string, err error) {
return FormatPrivateKey(key.ToHex(), PrivateKeyVariantSecp256k1)
}

//endregion

// FromHex populates the [Secp256k1PrivateKey] from a hex string
Expand All @@ -114,7 +124,7 @@ func (key *Secp256k1PrivateKey) ToHex() string {
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PrivateKey) FromHex(hexStr string) (err error) {
bytes, err := util.ParseHex(hexStr)
bytes, err := ParsePrivateKey(hexStr, PrivateKeyVariantSecp256k1)
if err != nil {
return err
}
Expand Down
13 changes: 9 additions & 4 deletions crypto/secp256k1_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package crypto

import (
"testing"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/stretchr/testify/assert"
"testing"
)

const (
testSecp256k1PrivateKey = "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e"
testSecp256k1PrivateKey = "secp256k1-priv-0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e"
testSecp256k1PrivateKeyHex = "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e"
testSecp256k1PublicKey = "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea"
testSecp256k1Address = "0x5792c985bc96f436270bd2a3c692210b09c7febb8889345ceefdbae4bacfe498"
testSecp256k1MessageEncoded = "0x68656c6c6f20776f726c64"
testSecp256k1Signature = "0xd0d634e843b61339473b028105930ace022980708b2855954b977da09df84a770c0b68c29c8ca1b5409a5085b0ec263be80e433c83fcf6debb82f3447e71edca"
)

func TestSecp256k1Keys(t *testing.T) {
testSecp256k1PrivateKeyBytes, err := util.ParseHex(testSecp256k1PrivateKey)
testSecp256k1PrivateKeyBytes, err := util.ParseHex(testSecp256k1PrivateKeyHex)
assert.NoError(t, err)

// Either bytes or hex should work
Expand All @@ -30,7 +32,10 @@ func TestSecp256k1Keys(t *testing.T) {

// The outputs should match as well
assert.Equal(t, privateKey.Bytes(), testSecp256k1PrivateKeyBytes)
assert.Equal(t, privateKey.ToHex(), testSecp256k1PrivateKey)
assert.Equal(t, privateKey.ToHex(), testSecp256k1PrivateKeyHex)
formattedString, err := privateKey.ToAIP80()
assert.NoError(t, err)
assert.Equal(t, formattedString, testSecp256k1PrivateKey)

// Auth key should match
singleSender := SingleSigner{privateKey}
Expand Down
2 changes: 1 addition & 1 deletion examples/fungible_asset/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/aptos-labs/aptos-go-sdk/crypto"
)

const testEd25519PrivateKey = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PrivateKey = "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const rupeePublisherAddress = "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa"

// These come from fungible_asset.json
Expand Down
Loading

0 comments on commit ab67fa9

Please sign in to comment.