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

feat: add support for ED25519 seeds that don't use the sEd prefix #415

Merged
merged 7 commits into from
Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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",
)

mvadari marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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
JST5000 marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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,
*,
JST5000 marked this conversation as resolved.
Show resolved Hide resolved
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