Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ bipsea --help

## Commands

bipsea offers four commands that work together:
bipsea offers five commands that work together:

1. `mnemonic` creates BIP-39 seed mnemonics in 9 languages
1. `validate` validates BIP-39 in 9 languages
1. `xprv` derives a BIP-32 extended private key
1. `derive` applies BIP-85 to an xprv to derive child secrets
1. `entropy` derives raw BIP-85 entropy at any path (developer tool)


# Tutorial
Expand Down Expand Up @@ -222,6 +223,35 @@ For a 6-digit PIN roll a 10-sided virtual die.
4,9,9,3,7,6


## `bipsea entropy`

`entropy` is a developer tool that derives raw BIP-85 entropy at any fully
hardened path so that you can prototype and test new applications before
they have a named `--application`.

```sh
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea entropy -p "m/83696968'/0'/0'"
```
<64 bytes (128 hex characters) of BIP-85 derived entropy>

`-n` truncates output to the first n bytes. To reproduce the HEX application
by hand:

```sh
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea entropy -p "m/83696968'/128169'/32'/0'" -n 32
```

`-d` instead reads n bytes from the BIP85-DRNG, for applications that consume
more than 64 bytes:

```sh
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea entropy -p "m/83696968'/0'/0'" -d 80
```

`entropy` requires fully hardened paths and warns if the path does not begin
with the BIP-85 purpose code `m/83696968'`.


# Technical discussion

## How are bipsea and hierarchical wallet derivation (BIP-85) useful?
Expand Down
78 changes: 78 additions & 0 deletions src/bipsea/bipsea.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
RANGES,
apply_85,
derive,
split_and_validate,
to_entropy,
)
from .util import (
Expand Down Expand Up @@ -263,6 +264,82 @@ def derive_cli(application, number, index, special, xprv, to):
click.echo(output)


@click.command(
name="entropy",
help=(
"Derive raw BIP-85 entropy at any fully hardened path."
" A developer tool for testing new applications."
),
)
@click.option(
"-p",
"--path",
required=True,
help="Fully hardened derivation path, e.g. m/83696968'/128169'/32'/0'.",
)
@click.option(
"-n",
"--number",
type=click.IntRange(1, 64),
help="Truncate entropy to the first n bytes (default 64).",
)
@click.option(
"-d",
"--drng",
"drng_bytes",
type=click.IntRange(min=1),
help="Read n bytes from the BIP85-DRNG seeded with the derived entropy.",
)
@click.option(
"-x",
"--xprv",
help="Extended private master key from which all secrets are derived.",
)
def entropy_cli(path, number, drng_bytes, xprv):
if xprv:
xprv = xprv.strip()
else:
xprv = try_for_pipe_input()
no_empty_param("--xprv", xprv)

if not validate_prv_str(xprv, private=True):
raise click.BadParameter("Bad xprv or tprv.", param_hint="--xprv (or pipe)")

if number is not None and drng_bytes is not None:
raise click.BadOptionUsage(
option_name="--number",
message="`--number` and `--drng` are mutually exclusive.",
)

try:
segments = split_and_validate(path)
except ValueError as err:
raise click.BadParameter(str(err), param_hint="--path")
if len(segments) < 2:
raise click.BadParameter("Expected at least one child.", param_hint="--path")
if not all(s[-1] in "'hH" for s in segments[1:]):
raise click.BadParameter(
"BIP-85 requires fully hardened paths, e.g. 0'.", param_hint="--path"
)
purpose = PURPOSE_CODES["BIP-85"]
if segments[1].rstrip("'hH") != purpose.rstrip("'"):
click.secho(
f"Warning: Path does not start with m/{purpose} (BIP-85).",
fg="yellow",
err=True,
)

master = parse_ext_key(xprv)
derived = derive(master, path)
entropy = to_entropy(derived.data[1:])
if drng_bytes:
output = DRNG(entropy).read(drng_bytes)
else:
output = entropy[: number or 64]

click.echo(to_hex_string(output))


@click.group()
@click.version_option(version=__version__, prog_name=__app_name__)
def cli():
Expand All @@ -273,6 +350,7 @@ def cli():
cli.add_command(validate)
cli.add_command(xprv)
cli.add_command(derive_cli)
cli.add_command(entropy_cli)


def check_range(number: int, application: str):
Expand Down
97 changes: 95 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def runner():


class TestBase:
@pytest.mark.parametrize("cmd", ["", "mnemonic", "validate", "xprv", "derive"])
@pytest.mark.parametrize(
"cmd", ["", "mnemonic", "validate", "xprv", "derive", "entropy"]
)
def test_help(self, runner, cmd):
result = runner.invoke(cli, [cmd, "--help"])
result.exit_code == 0
Expand Down Expand Up @@ -213,7 +215,7 @@ def test_bad_n(self, runner, app, n):
xprv = COMMON_XPRV
if n == 1025 and app == "drng":
return
result = runner.invoke(cli, ["entropy", "-a", app, "-n", n, "--input", xprv])
result = runner.invoke(cli, ["derive", "-a", app, "-n", n, "-x", xprv])
assert result.exit_code != 0
assert "Error" in result.output

Expand Down Expand Up @@ -343,6 +345,96 @@ def test_bad_to(self, runner):
assert "--to" in result.output


class TestEntropy:
"""BIP-85 spec vectors from Test case 1, Test case 2, BIP85-DRNG, and HEX."""

VECTORS = [
(
"m/83696968'/0'/0'",
None,
"efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f0"
"0b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7",
),
(
"m/83696968'/0'/1'",
None,
"70c6e3e8ebee8dc4c0dbba66076819bb8c09672527c4277ca8729532ad711872"
"218f826919f6b67218adde99018a6df9095ab2b58d803b5b93ec9802085a690e",
),
(
"m/83696968'/128169'/64'/0'",
64,
"492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e71aa2d645442f8785"
"55d078fd1f1f67e368976f04137b1f7a0d19232136ca50c44614af72b5582a5c",
),
]

@pytest.mark.parametrize("path, number, expected", VECTORS, ids=lambda v: str(v))
def test_spec_vectors(self, runner, path, number, expected):
cmd = ["entropy", "-p", path, "-x", COMMON_XPRV]
if number:
cmd += ["-n", number]
result = runner.invoke(cli, cmd)
assert result.exit_code == 0
assert result.output.strip() == expected

def test_drng_spec_vector(self, runner):
cmd = ["entropy", "-p", "m/83696968'/0'/0'", "-x", COMMON_XPRV, "-d", 80]
result = runner.invoke(cli, cmd)
assert result.exit_code == 0
assert result.output.strip() == (
"b78b1ee6b345eae6836c2d53d33c64cdaf9a696487be81b03e822dc84b3f1cd8"
"83d7559e53d175f243e4c349e822a957bbff9224bc5dde9492ef54e8a439f6bc"
"8c7355b87a925a37ee405a7502991111"
)

@pytest.mark.parametrize("n_bytes", (16, 32, 64))
def test_matches_derive_hex(self, runner, n_bytes):
"""`entropy` at a HEX path must agree with `derive -a hex`."""
path = f"m/83696968'/128169'/{n_bytes}'/0'"
raw = runner.invoke(
cli, ["entropy", "-p", path, "-n", n_bytes, "-x", COMMON_XPRV]
)
hex_ = runner.invoke(
cli, ["derive", "-a", "hex", "-n", n_bytes, "-x", COMMON_XPRV]
)
assert raw.exit_code == hex_.exit_code == 0
assert raw.output == hex_.output

def test_warns_off_purpose(self, runner):
result = runner.invoke(cli, ["entropy", "-p", "m/44'/0'/0'", "-x", COMMON_XPRV])
assert result.exit_code == 0
assert "Warning" in result.output

@pytest.mark.parametrize("path", ("m/83696968'/0/0'", "m", "44'/0'", "m/x'"))
def test_bad_path(self, runner, path):
result = runner.invoke(cli, ["entropy", "-p", path, "-x", COMMON_XPRV])
assert result.exit_code != 0
assert "Error" in result.output

def test_number_drng_exclusive(self, runner):
cmd = [
"entropy",
"-p",
"m/83696968'/0'/0'",
"-x",
COMMON_XPRV,
"-n",
8,
"-d",
8,
]
result = runner.invoke(cli, cmd)
assert result.exit_code != 0
assert "mutually exclusive" in result.output

def test_bad_xprv(self, runner):
cmd = ["entropy", "-p", "m/83696968'/0'/0'", "-x", COMMON_XPRV[1:]]
result = runner.invoke(cli, cmd)
assert result.exit_code != 0
assert "--xprv" in result.output


class TestIntegration:
def test_chain_no_pipe(self, runner):
"""this also tests that the default options are compatible"""
Expand Down Expand Up @@ -416,6 +508,7 @@ def test_no_m(self, runner, cmd, opt):
'bipsea validate -m "elder major green sting survey canoe inmate funny bright jewel anchor volcano" | bipsea xprv | bipsea derive -a drng -n 1000',
'bipsea validate -m "elder major green sting survey canoe inmate funny bright jewel anchor volcano" | bipsea xprv | bipsea derive -a dice -n 100 -s 6',
'bipsea xprv -m "elder major green sting survey canoe inmate funny bright jewel anchor volcano" | bipsea derive -a mnemonic -n 12',
'bipsea xprv -m "elder major green sting survey canoe inmate funny bright jewel anchor volcano" | bipsea entropy -p "m/83696968\'/0\'/0\'"',
],
}

Expand Down
Loading