diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d33e3d67..964a0dbf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/tests/unit/core/addresscodec/test_codec.py b/tests/unit/core/addresscodec/test_codec.py index 222d1b6a7..350f278c5 100644 --- a/tests/unit/core/addresscodec/test_codec.py +++ b/tests/unit/core/addresscodec/test_codec.py @@ -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) diff --git a/tests/unit/core/keypairs/test_main.py b/tests/unit/core/keypairs/test_main.py index f7a5a9942..3efb11fe3 100644 --- a/tests/unit/core/keypairs/test_main.py +++ b/tests/unit/core/keypairs/test_main.py @@ -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( diff --git a/xrpl/core/addresscodec/codec.py b/xrpl/core/addresscodec/codec.py index c0339d5b0..479023acf 100644 --- a/xrpl/core/addresscodec/codec.py +++ b/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 @@ -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: @@ -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) @@ -84,16 +86,19 @@ 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). @@ -101,8 +106,19 @@ def decode_seed(seed: str) -> Tuple[bytes, CryptoAlgorithm]: 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 diff --git a/xrpl/core/keypairs/main.py b/xrpl/core/keypairs/main.py index 13587fd9a..e61dbf1a7 100644 --- a/xrpl/core/keypairs/main.py +++ b/xrpl/core/keypairs/main.py @@ -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. @@ -54,6 +56,8 @@ def derive_keypair(seed: str, validator: bool = False) -> Tuple[str, str]: :func:`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. @@ -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) diff --git a/xrpl/wallet/main.py b/xrpl/wallet/main.py index c41863fc8..29c42dabc 100644 --- a/xrpl/wallet/main.py +++ b/xrpl/wallet/main.py @@ -16,13 +16,21 @@ 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 """ @@ -30,7 +38,7 @@ def __init__(self: Wallet, seed: str, sequence: int) -> None: 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 @@ -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