Skip to content

Commit 4e7ce5e

Browse files
committed
test(mnemonic): add property-based tests for key derivation
Add property-based tests for `derive_from_mnemonic` to verify acceptance of valid account numbers and rejection of invalid account/key numbers. Includes hypothesis-based checks for boundary values and invalid types.
1 parent 354192f commit 4e7ce5e

File tree

1 file changed

+80
-2
lines changed

1 file changed

+80
-2
lines changed

cardano_node_tests/tests/test_mnemonic.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
KEY_TYPE_IDS = tuple(k.value.replace("-", "_") for k in KEY_TYPES)
2525
# pyrefly: ignore # no-matching-overload
2626
OUT_FORMATS = tuple(clusterlib.OutputFormat)
27-
OUT_FORMAT_IDS = (k.value.replace("-", "_") for k in OUT_FORMATS)
27+
OUT_FORMAT_IDS = tuple(k.value.replace("-", "_") for k in OUT_FORMATS)
2828

2929
# A small embedded list of *valid* BIP39 words (subset).
3030
# Enough to build syntactically plausible phrases without importing anything.
@@ -98,7 +98,7 @@ class BadMnemonicCase:
9898
bad_token = st.one_of(
9999
ascii_word, # Unknown word
100100
st.text(min_size=1, max_size=8).filter(
101-
lambda s: any(c for c in s if not c.isalpha())
101+
lambda s: any(not c.isalpha() for c in s)
102102
), # Punctuation/digits
103103
st.sampled_from(["Über", "naïve", "résumé", "café"]), # Diacritics / non-ASCII
104104
st.sampled_from(["🚀", "🔥", "🙂"]), # Emoji
@@ -381,6 +381,41 @@ def test_golden_deriv(
381381

382382
assert helpers.checksum(filename=key_file) == helpers.checksum(filename=golden_key_file)
383383

384+
@allure.link(helpers.get_vcs_link())
385+
@pytest.mark.parametrize("key_type", KEY_TYPES, ids=KEY_TYPE_IDS)
386+
@common.hypothesis_settings(max_examples=300)
387+
@hypothesis.given(account_number=st.integers(min_value=0, max_value=2**31 - 1))
388+
@hypothesis.example(account_number=0)
389+
@hypothesis.example(account_number=2**31 - 1)
390+
def test_derive_account_key_number_property(
391+
self,
392+
cluster: clusterlib.ClusterLib,
393+
key_type: clusterlib.KeyType,
394+
account_number: int,
395+
) -> None:
396+
"""Test that `derive-from-mnemonic` accepts any valid account_number in [0, 2^31-1].
397+
398+
For payment/stake keys, pass the same value as key_number, otherwise omit key_number.
399+
"""
400+
temp_template = f"{common.get_test_id(cluster)}_{common.unique_time_str()}"
401+
mnemonic_file = DATA_DIR / "gold_[0-bech32-payment-24]_mnemonic"
402+
403+
key_number = (
404+
account_number
405+
if key_type in (clusterlib.KeyType.PAYMENT, clusterlib.KeyType.STAKE)
406+
else None
407+
)
408+
409+
key_file = cluster.g_key.derive_from_mnemonic(
410+
key_name=f"{temp_template}_derived",
411+
key_type=key_type,
412+
mnemonic_file=mnemonic_file,
413+
account_number=account_number,
414+
key_number=key_number,
415+
out_format=clusterlib.OutputFormat.BECH32,
416+
)
417+
assert key_file.exists()
418+
384419

385420
@common.SKIPIF_WRONG_ERA
386421
class TestNegativeMnemonic:
@@ -435,3 +470,46 @@ def test_rejects_noncompliant_mnemonics(
435470
"Error reading mnemonic file" in err_value
436471
or "Error converting the mnemonic into a key" in err_value
437472
)
473+
474+
@allure.link(helpers.get_vcs_link())
475+
@pytest.mark.parametrize("key_type", KEY_TYPES, ids=KEY_TYPE_IDS)
476+
@common.hypothesis_settings(max_examples=300)
477+
@hypothesis.given(
478+
bad_value=st.one_of(
479+
# Integers outside the valid range
480+
st.integers(max_value=-1),
481+
st.integers(min_value=2**31),
482+
# Non-integer types
483+
st.floats(allow_nan=True, allow_infinity=True),
484+
st.text(
485+
alphabet=st.characters(blacklist_categories=["C", "Nd"]), min_size=1, max_size=5
486+
),
487+
st.sampled_from([3.14, object(), [], {}]),
488+
)
489+
)
490+
@hypothesis.example(bad_value=-1)
491+
@hypothesis.example(bad_value=2**31)
492+
def test_reject_invalid_account_or_key_number(
493+
self,
494+
cluster: clusterlib.ClusterLib,
495+
key_type: clusterlib.KeyType,
496+
bad_value: object,
497+
) -> None:
498+
"""Property: derive_from_mnemonic rejects out-of-range or non-int account/key numbers."""
499+
temp_template = f"{common.get_test_id(cluster)}_{common.unique_time_str()}"
500+
mnemonic_file = DATA_DIR / "gold_[0-bech32-payment-24]_mnemonic"
501+
502+
kwargs: dict[str, tp.Any] = {
503+
"key_name": f"{temp_template}_derived",
504+
"key_type": key_type,
505+
"mnemonic_file": mnemonic_file,
506+
"account_number": bad_value,
507+
}
508+
509+
if key_type in (clusterlib.KeyType.PAYMENT, clusterlib.KeyType.STAKE):
510+
kwargs["key_number"] = bad_value
511+
512+
with pytest.raises(clusterlib.CLIError) as excinfo:
513+
cluster.g_key.derive_from_mnemonic(**kwargs)
514+
err_value = str(excinfo.value)
515+
assert "unexpected" in err_value or "Error converting the mnemonic into a key" in err_value

0 commit comments

Comments
 (0)