Skip to content

Commit

Permalink
feat: add support for ED25519 seeds that don't use the sEd prefix (#…
Browse files Browse the repository at this point in the history
…415)

* initial implementation

* add test

* add keypairs test

* add doco

* add comments

* fix changelog

* add wrong prefix test
  • Loading branch information
mvadari committed Jul 26, 2022
1 parent 8657f45 commit 56c1270
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [[Unreleased]]
### Added:
- Function to parse the final account balances from a transaction's metadata
- Support for Ed25519 seeds that don't use the `sEd` prefix

### Fixed:
- Typing for factory classmethods on models
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/core/addresscodec/test_codec.py
Expand Up @@ -91,6 +91,27 @@ def test_seed_encode_decode_ed25519_high(self):
self.assertEqual(decode_result, hex_string_bytes)
self.assertEqual(encoding_type, CryptoAlgorithm.ED25519)

def test_seed_decode_ed25519_different_prefix(self):
hex_string = "2275BCC966EF1FED4AD08B11189A4157"
encoded_string = "ssB9S5Mca2hGZ73xNs4gruS1GY7fB"
hex_string_bytes = bytes.fromhex(hex_string)

decode_result, encoding_type = addresscodec.decode_seed(
encoded_string, CryptoAlgorithm.ED25519
)
self.assertEqual(decode_result, hex_string_bytes)
self.assertEqual(encoding_type, CryptoAlgorithm.ED25519)

def test_seed_decode_secp256k1_wrong_prefix(self):
encoded_string = "sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG"

self.assertRaises(
addresscodec.XRPLAddressCodecException,
addresscodec.decode_seed,
encoded_string,
CryptoAlgorithm.SECP256K1,
)

def test_seed_encode_decode_too_small(self):
hex_string = "CF2DE378FBDD7E2EE87D486DFB5A7B"
hex_string_bytes = bytes.fromhex(hex_string)
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/core/keypairs/test_main.py
Expand Up @@ -49,6 +49,19 @@ def test_derive_keypair_ed25519_validator(self):
with self.assertRaises(XRPLKeypairsException):
keypairs.derive_keypair("sEdSKaCy2JT7JaM7v95H9SxkhP9wS2r", validator=True)

def test_derive_keypair_ed25519_different_prefix(self):
public, private = keypairs.derive_keypair(
"ssB9S5Mca2hGZ73xNs4gruS1GY7fB", algorithm=CryptoAlgorithm.ED25519
)
self.assertEqual(
public,
"ED6BBFC23A490D021B87D25563C15DA953A7F0F1A493DAA3767FB27F82E2F80C3D",
)
self.assertEqual(
private,
"ED644E705250E4D736875E85DD3E5FBABA4E12E004549202010228E17D3D574576",
)

def test_derive_keypair_secp256k1(self):
public, private = keypairs.derive_keypair("sp5fghtJtpUorTwvof1NpDXAzNwf5")
self.assertEqual(
Expand Down
42 changes: 29 additions & 13 deletions xrpl/core/addresscodec/codec.py
@@ -1,6 +1,6 @@
"""This module encodes and decodes various types of base58 encodings."""

from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple

import base58
from typing_extensions import Final
Expand All @@ -27,10 +27,10 @@
_NODE_PUBLIC_KEY_LENGTH: Final[int] = 33
_ACCOUNT_PUBLIC_KEY_LENGTH: Final[int] = 33

_ALGORITHM_TO_PREFIX_MAP: Final[Dict[CryptoAlgorithm, List[int]]] = {
CryptoAlgorithm.ED25519: _ED25519_SEED_PREFIX,
CryptoAlgorithm.SECP256K1: _FAMILY_SEED_PREFIX,
}
_ALGORITHM_TO_PREFIX_MAP: Final[Dict[CryptoAlgorithm, List[List[int]]]] = {
CryptoAlgorithm.ED25519: [_ED25519_SEED_PREFIX, _FAMILY_SEED_PREFIX],
CryptoAlgorithm.SECP256K1: [_FAMILY_SEED_PREFIX],
} # first is default, rest are other options


def _encode(bytestring: bytes, prefix: List[int], expected_length: int) -> str:
Expand All @@ -50,10 +50,12 @@ def _encode(bytestring: bytes, prefix: List[int], expected_length: int) -> str:

def _decode(b58_string: str, prefix: bytes) -> bytes:
"""
b58_string: A base58 value
prefix: The prefix prepended to the bytestring
Args:
b58_string: A base58 value.
prefix: The prefix prepended to the bytestring.
Returns the byte decoding of the base58-encoded string.
Returns:
The byte decoding of the base58-encoded string.
"""
prefix_length = len(prefix)
decoded = base58.b58decode_check(b58_string, alphabet=XRPL_ALPHABET)
Expand Down Expand Up @@ -84,25 +86,39 @@ def encode_seed(entropy: bytes, encoding_type: CryptoAlgorithm) -> str:
f"Encoding type must be one of {CryptoAlgorithm}"
)

prefix = _ALGORITHM_TO_PREFIX_MAP[encoding_type]
prefix = _ALGORITHM_TO_PREFIX_MAP[encoding_type][0]
return _encode(entropy, prefix, SEED_LENGTH)


def decode_seed(seed: str) -> Tuple[bytes, CryptoAlgorithm]:
def decode_seed(
seed: str, algorithm: Optional[CryptoAlgorithm] = None
) -> Tuple[bytes, CryptoAlgorithm]:
"""
Returns (decoded seed, its algorithm).
Args:
seed: b58 encoding of a seed.
seed: The b58 encoding of a seed.
algorithm: The encoding algorithm. Inferred from the seed if not included.
Returns:
(decoded seed, its algorithm).
Raises:
XRPLAddressCodecException: If the seed is invalid.
"""
for algorithm in CryptoAlgorithm:
prefix = _ALGORITHM_TO_PREFIX_MAP[algorithm]
if algorithm is not None:
# check all algorithm prefixes
for prefix in _ALGORITHM_TO_PREFIX_MAP[algorithm]:
try:
decoded_result = _decode(seed, bytes(prefix))
return decoded_result, algorithm
except XRPLAddressCodecException:
# prefix is incorrect, wrong prefix
continue
raise XRPLAddressCodecException("Wrong algorithm for the seed type.")

for algorithm in CryptoAlgorithm: # use default prefix
prefix = _ALGORITHM_TO_PREFIX_MAP[algorithm][0]
try:
decoded_result = _decode(seed, bytes(prefix))
return decoded_result, algorithm
Expand Down
8 changes: 6 additions & 2 deletions xrpl/core/keypairs/main.py
Expand Up @@ -45,7 +45,9 @@ def generate_seed(
return addresscodec.encode_seed(parsed_entropy, algorithm)


def derive_keypair(seed: str, validator: bool = False) -> Tuple[str, str]:
def derive_keypair(
seed: str, validator: bool = False, algorithm: Optional[CryptoAlgorithm] = None
) -> Tuple[str, str]:
"""
Derive the public and private keys from a given seed value.
Expand All @@ -54,6 +56,8 @@ def derive_keypair(seed: str, validator: bool = False) -> Tuple[str, str]:
:func:`generate_seed() <xrpl.core.keypairs.generate_seed>` to generate an
appropriate value.
validator: Whether the keypair is a validator keypair.
algorithm: The algorithm used to encode the keys. Inferred from the seed if not
included.
Returns:
A (public key, private key) pair derived from the given seed.
Expand All @@ -62,7 +66,7 @@ def derive_keypair(seed: str, validator: bool = False) -> Tuple[str, str]:
XRPLKeypairsException: If the derived keypair did not generate a
verifiable signature.
"""
decoded_seed, algorithm = addresscodec.decode_seed(seed)
decoded_seed, algorithm = addresscodec.decode_seed(seed, algorithm)
module = _ALGORITHM_TO_MODULE_MAP[algorithm]
public_key, private_key = module.derive_keypair(decoded_seed, validator)
signature = module.sign(_VERIFICATION_MESSAGE, private_key)
Expand Down
14 changes: 11 additions & 3 deletions xrpl/wallet/main.py
Expand Up @@ -16,21 +16,29 @@ class Wallet:
details.
"""

def __init__(self: Wallet, seed: str, sequence: int) -> None:
def __init__(
self: Wallet,
seed: str,
sequence: int,
*,
algorithm: Optional[CryptoAlgorithm] = None,
) -> None:
"""
Generate a new Wallet.
Args:
seed: The seed from which the public and private keys are derived.
sequence: The next sequence number for the account.
algorithm: The algorithm used to encode the keys. Inferred from the seed if
not included.
"""
self.seed = seed
"""
The core value that is used to derive all other information about
this wallet. MUST be kept secret!
"""

pk, sk = derive_keypair(self.seed)
pk, sk = derive_keypair(self.seed, algorithm=algorithm)
self.public_key = pk
"""
The public key that is used to identify this wallet's signatures, as
Expand Down Expand Up @@ -70,7 +78,7 @@ def create(
The wallet that is generated from the given seed.
"""
seed = generate_seed(algorithm=crypto_algorithm)
return cls(seed, sequence=0)
return cls(seed, sequence=0, algorithm=crypto_algorithm)

def get_xaddress(
self: Wallet, *, tag: Optional[int] = None, is_test: bool = False
Expand Down

0 comments on commit 56c1270

Please sign in to comment.