bip46 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bip46-1.0.0/.github/workflows/mypy.yml +27 -0
- bip46-1.0.0/.github/workflows/release.yaml +27 -0
- bip46-1.0.0/.github/workflows/ruff.yml +12 -0
- bip46-1.0.0/.github/workflows/tests.yml +27 -0
- bip46-1.0.0/.gitignore +2 -0
- bip46-1.0.0/.python-version +1 -0
- bip46-1.0.0/LICENSE +21 -0
- bip46-1.0.0/Makefile +19 -0
- bip46-1.0.0/PKG-INFO +89 -0
- bip46-1.0.0/README.md +74 -0
- bip46-1.0.0/bip46/__init__.py +76 -0
- bip46-1.0.0/bip46/certificate.py +69 -0
- bip46-1.0.0/bip46/cli.py +135 -0
- bip46-1.0.0/bip46/consts.py +25 -0
- bip46-1.0.0/bip46/derivation.py +37 -0
- bip46-1.0.0/bip46/electrs.py +38 -0
- bip46-1.0.0/bip46/exceptions.py +21 -0
- bip46-1.0.0/bip46/hdkey.py +106 -0
- bip46-1.0.0/bip46/py.typed +0 -0
- bip46-1.0.0/bip46/script.py +44 -0
- bip46-1.0.0/examples/example_create_timelock.py +33 -0
- bip46-1.0.0/examples/example_hd_derive.py +22 -0
- bip46-1.0.0/examples/example_withdraw_timelock.py +56 -0
- bip46-1.0.0/pyproject.toml +66 -0
- bip46-1.0.0/tests/test_certificate_message.py +52 -0
- bip46-1.0.0/tests/test_create_timelock_address.py +203 -0
- bip46-1.0.0/tests/test_timelock_derivation.py +78 -0
- bip46-1.0.0/tests/test_vectors.py +187 -0
- bip46-1.0.0/uv.lock +436 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: mypy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
pull_request:
|
|
7
|
+
jobs:
|
|
8
|
+
check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ["3.12"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync --locked --all-extras --dev
|
|
26
|
+
- name: Run mypy
|
|
27
|
+
run: make mypy
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- name: Set up Python 3.12
|
|
12
|
+
uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v6
|
|
17
|
+
- name: Build the project
|
|
18
|
+
run: uv build
|
|
19
|
+
- name: Publish to pypi
|
|
20
|
+
env:
|
|
21
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
|
22
|
+
run: uv publish
|
|
23
|
+
- name: Create github release
|
|
24
|
+
env:
|
|
25
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
tag: ${{ github.ref_name }}
|
|
27
|
+
run: gh release create "$tag" --generate-notes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
pull_request:
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ["3.12"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync --locked --all-extras --dev
|
|
26
|
+
- name: Test with pytest
|
|
27
|
+
run: make test
|
bip46-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
bip46-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 dni ⚡
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bip46-1.0.0/Makefile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
all: ruff mypy test
|
|
2
|
+
format: ruff
|
|
3
|
+
lint: ruff mypy
|
|
4
|
+
check: checkruff mypy test
|
|
5
|
+
|
|
6
|
+
install:
|
|
7
|
+
uv sync
|
|
8
|
+
|
|
9
|
+
ruff:
|
|
10
|
+
uv run ruff check . --fix
|
|
11
|
+
|
|
12
|
+
checkruff:
|
|
13
|
+
uv run ruff check .
|
|
14
|
+
|
|
15
|
+
mypy:
|
|
16
|
+
uv run mypy .
|
|
17
|
+
|
|
18
|
+
test:
|
|
19
|
+
uv run pytest tests
|
bip46-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bip46
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python implementation of BIP46: Address Scheme for Timelocked Fidelity Bonds
|
|
5
|
+
Author-email: dni <dni@lnbits.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: bech32<2.0.0,>=1.2.0
|
|
10
|
+
Requires-Dist: click<9.0.0,>=8.1.7
|
|
11
|
+
Requires-Dist: embit<0.9.0,>=0.8.0
|
|
12
|
+
Requires-Dist: httpx<1.0.0,>=0.27.2
|
|
13
|
+
Requires-Dist: secp256k1<0.15.0,>=0.14.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# BIP46 - Address Scheme for Timelocked Fidelity Bonds
|
|
17
|
+
|
|
18
|
+
Python implementation of BIP46: Address Scheme for Timelocked Fidelity Bonds.
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
|
|
22
|
+
### Create a redeem script from a mnemonic
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from datetime import UTC, datetime
|
|
26
|
+
|
|
27
|
+
from bip46 import (
|
|
28
|
+
create_redeemscript,
|
|
29
|
+
hdkey_derive,
|
|
30
|
+
hdkey_from_mnemonic,
|
|
31
|
+
hdkey_to_pubkey,
|
|
32
|
+
lockdate_to_derivation_path,
|
|
33
|
+
redeemscript_address,
|
|
34
|
+
redeemscript_pubkey,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
network = "mainnet"
|
|
38
|
+
lock_date = datetime(2042, 6, 1, tzinfo=UTC)
|
|
39
|
+
lock_path = lockdate_to_derivation_path(lock_date, network=network)
|
|
40
|
+
|
|
41
|
+
hdkey = hdkey_from_mnemonic(
|
|
42
|
+
"abandon abandon abandon abandon abandon abandon"
|
|
43
|
+
" abandon abandon abandon abandon abandon about",
|
|
44
|
+
network=network,
|
|
45
|
+
)
|
|
46
|
+
redeem_key = hdkey_derive(hdkey, lock_path)
|
|
47
|
+
redeem_pub_key = hdkey_to_pubkey(redeem_key)
|
|
48
|
+
redeem_script = create_redeemscript(lock_date, redeem_pub_key)
|
|
49
|
+
script_pubkey = redeemscript_pubkey(redeem_script)
|
|
50
|
+
script_address = redeemscript_address(script_pubkey, network=network)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Create addresses from an xpub
|
|
54
|
+
|
|
55
|
+
BIP46 paths include hardened account components: `m/84'/coin_type'/0'/2/index`.
|
|
56
|
+
A public key cannot derive those hardened components, so pass an account-level xpub
|
|
57
|
+
for `m/84'/coin_type'/0'`. The library will derive the public BIP46 suffix
|
|
58
|
+
`2/index` from that account xpub.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from datetime import UTC, datetime
|
|
62
|
+
|
|
63
|
+
from bip46 import (
|
|
64
|
+
create_redeemscript,
|
|
65
|
+
hdkey_derive,
|
|
66
|
+
hdkey_from_xpub,
|
|
67
|
+
hdkey_to_pubkey,
|
|
68
|
+
lockdate_to_derivation_path,
|
|
69
|
+
redeemscript_address,
|
|
70
|
+
redeemscript_pubkey,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
network = "mainnet"
|
|
74
|
+
lock_date = datetime(2042, 6, 1, tzinfo=UTC)
|
|
75
|
+
lock_path = lockdate_to_derivation_path(lock_date, network=network)
|
|
76
|
+
|
|
77
|
+
hdkey = hdkey_from_xpub("xpub...")
|
|
78
|
+
redeem_key = hdkey_derive(hdkey, lock_path)
|
|
79
|
+
redeem_pub_key = hdkey_to_pubkey(redeem_key)
|
|
80
|
+
redeem_script = create_redeemscript(lock_date, redeem_pub_key)
|
|
81
|
+
script_pubkey = redeemscript_pubkey(redeem_script)
|
|
82
|
+
script_address = redeemscript_address(script_pubkey, network=network)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The CLI also accepts an account xpub:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
XPUB="xpub..." uv run bip46 create-timelock 2042 6 mainnet
|
|
89
|
+
```
|
bip46-1.0.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# BIP46 - Address Scheme for Timelocked Fidelity Bonds
|
|
2
|
+
|
|
3
|
+
Python implementation of BIP46: Address Scheme for Timelocked Fidelity Bonds.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
### Create a redeem script from a mnemonic
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
|
|
12
|
+
from bip46 import (
|
|
13
|
+
create_redeemscript,
|
|
14
|
+
hdkey_derive,
|
|
15
|
+
hdkey_from_mnemonic,
|
|
16
|
+
hdkey_to_pubkey,
|
|
17
|
+
lockdate_to_derivation_path,
|
|
18
|
+
redeemscript_address,
|
|
19
|
+
redeemscript_pubkey,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
network = "mainnet"
|
|
23
|
+
lock_date = datetime(2042, 6, 1, tzinfo=UTC)
|
|
24
|
+
lock_path = lockdate_to_derivation_path(lock_date, network=network)
|
|
25
|
+
|
|
26
|
+
hdkey = hdkey_from_mnemonic(
|
|
27
|
+
"abandon abandon abandon abandon abandon abandon"
|
|
28
|
+
" abandon abandon abandon abandon abandon about",
|
|
29
|
+
network=network,
|
|
30
|
+
)
|
|
31
|
+
redeem_key = hdkey_derive(hdkey, lock_path)
|
|
32
|
+
redeem_pub_key = hdkey_to_pubkey(redeem_key)
|
|
33
|
+
redeem_script = create_redeemscript(lock_date, redeem_pub_key)
|
|
34
|
+
script_pubkey = redeemscript_pubkey(redeem_script)
|
|
35
|
+
script_address = redeemscript_address(script_pubkey, network=network)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Create addresses from an xpub
|
|
39
|
+
|
|
40
|
+
BIP46 paths include hardened account components: `m/84'/coin_type'/0'/2/index`.
|
|
41
|
+
A public key cannot derive those hardened components, so pass an account-level xpub
|
|
42
|
+
for `m/84'/coin_type'/0'`. The library will derive the public BIP46 suffix
|
|
43
|
+
`2/index` from that account xpub.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from datetime import UTC, datetime
|
|
47
|
+
|
|
48
|
+
from bip46 import (
|
|
49
|
+
create_redeemscript,
|
|
50
|
+
hdkey_derive,
|
|
51
|
+
hdkey_from_xpub,
|
|
52
|
+
hdkey_to_pubkey,
|
|
53
|
+
lockdate_to_derivation_path,
|
|
54
|
+
redeemscript_address,
|
|
55
|
+
redeemscript_pubkey,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
network = "mainnet"
|
|
59
|
+
lock_date = datetime(2042, 6, 1, tzinfo=UTC)
|
|
60
|
+
lock_path = lockdate_to_derivation_path(lock_date, network=network)
|
|
61
|
+
|
|
62
|
+
hdkey = hdkey_from_xpub("xpub...")
|
|
63
|
+
redeem_key = hdkey_derive(hdkey, lock_path)
|
|
64
|
+
redeem_pub_key = hdkey_to_pubkey(redeem_key)
|
|
65
|
+
redeem_script = create_redeemscript(lock_date, redeem_pub_key)
|
|
66
|
+
script_pubkey = redeemscript_pubkey(redeem_script)
|
|
67
|
+
script_address = redeemscript_address(script_pubkey, network=network)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The CLI also accepts an account xpub:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
XPUB="xpub..." uv run bip46 create-timelock 2042 6 mainnet
|
|
74
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from embit.script import address_to_scriptpubkey
|
|
2
|
+
from embit.transaction import Transaction, TransactionInput, TransactionOutput
|
|
3
|
+
|
|
4
|
+
from .certificate import (
|
|
5
|
+
create_certificate_message,
|
|
6
|
+
recover_from_signature_and_message,
|
|
7
|
+
sign_certificate_message,
|
|
8
|
+
)
|
|
9
|
+
from .derivation import (
|
|
10
|
+
index_to_lockdate,
|
|
11
|
+
lockdate_to_derivation_path,
|
|
12
|
+
lockdate_to_index,
|
|
13
|
+
lockindex_to_derivation_path,
|
|
14
|
+
)
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
Bip46Bech32Error,
|
|
17
|
+
Bip46IndexError,
|
|
18
|
+
Bip46PathError,
|
|
19
|
+
Bip46RecoverPubkeyError,
|
|
20
|
+
Bip46TimeError,
|
|
21
|
+
)
|
|
22
|
+
from .hdkey import (
|
|
23
|
+
hdkey_derive,
|
|
24
|
+
hdkey_from_mnemonic,
|
|
25
|
+
hdkey_from_seed,
|
|
26
|
+
hdkey_from_xpub,
|
|
27
|
+
hdkey_scan,
|
|
28
|
+
hdkey_scan_all,
|
|
29
|
+
hdkey_to_pubkey,
|
|
30
|
+
)
|
|
31
|
+
from .script import create_redeemscript, redeemscript_address, redeemscript_pubkey
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# WIP
|
|
35
|
+
def create_redeem_transaction(
|
|
36
|
+
prevtxid: str,
|
|
37
|
+
vout: int,
|
|
38
|
+
redeemscript: bytes,
|
|
39
|
+
address: str,
|
|
40
|
+
fee_sats_vbyte: int = 10,
|
|
41
|
+
# network: str = DEFAULT_NETWORK,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Create a redeem transaction for a BIP46 timelock"""
|
|
44
|
+
tx_size = 100
|
|
45
|
+
amount = 100000 - fee_sats_vbyte * tx_size
|
|
46
|
+
receive_address = address_to_scriptpubkey(address)
|
|
47
|
+
txout = TransactionOutput(amount, receive_address)
|
|
48
|
+
txin = TransactionInput(prevtxid, vout, redeemscript)
|
|
49
|
+
tx = Transaction(2, [txin], [txout])
|
|
50
|
+
return tx.serialize().hex()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"Bip46Bech32Error",
|
|
55
|
+
"Bip46IndexError",
|
|
56
|
+
"Bip46PathError",
|
|
57
|
+
"Bip46RecoverPubkeyError",
|
|
58
|
+
"Bip46TimeError",
|
|
59
|
+
"create_certificate_message",
|
|
60
|
+
"create_redeemscript",
|
|
61
|
+
"hdkey_derive",
|
|
62
|
+
"hdkey_from_mnemonic",
|
|
63
|
+
"hdkey_from_seed",
|
|
64
|
+
"hdkey_from_xpub",
|
|
65
|
+
"hdkey_scan",
|
|
66
|
+
"hdkey_scan_all",
|
|
67
|
+
"hdkey_to_pubkey",
|
|
68
|
+
"index_to_lockdate",
|
|
69
|
+
"lockdate_to_derivation_path",
|
|
70
|
+
"lockdate_to_index",
|
|
71
|
+
"lockindex_to_derivation_path",
|
|
72
|
+
"recover_from_signature_and_message",
|
|
73
|
+
"redeemscript_address",
|
|
74
|
+
"redeemscript_pubkey",
|
|
75
|
+
"sign_certificate_message",
|
|
76
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from base64 import b64decode
|
|
2
|
+
from hashlib import sha256
|
|
3
|
+
|
|
4
|
+
from secp256k1 import PrivateKey, PublicKey
|
|
5
|
+
|
|
6
|
+
from .exceptions import Bip46RecoverPubkeyError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_certificate_message(
|
|
10
|
+
pubkey: str,
|
|
11
|
+
message: str = "fidelity-bond-cert",
|
|
12
|
+
expiry: int = 375
|
|
13
|
+
) -> str:
|
|
14
|
+
"""Create a certificate message for a BIP46 timelock"""
|
|
15
|
+
return f"{message}|{pubkey}|{expiry}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def prepare_certificate_message(message: str) -> bytes:
|
|
19
|
+
""" message hash for signing """
|
|
20
|
+
msg = message.encode()
|
|
21
|
+
prefix = b"\x18Bitcoin Signed Message:\n"
|
|
22
|
+
data = prefix + bytes([len(msg)]) + msg
|
|
23
|
+
return sha256(sha256(data).digest()).digest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sign_certificate_message(private_key: bytes, message: str) -> bytes:
|
|
27
|
+
"""Sign a certificate message for a BIP46 timelock"""
|
|
28
|
+
message_hash = prepare_certificate_message(message)
|
|
29
|
+
key = PrivateKey(private_key)
|
|
30
|
+
raw_sig = key.ecdsa_sign_recoverable(message_hash, raw=True)
|
|
31
|
+
sig, recid = key.ecdsa_recoverable_serialize(raw_sig)
|
|
32
|
+
# 31 for p2pkh_address
|
|
33
|
+
return bytes([recid + 31]) + sig
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def convert_recoverable_message_signature(sig_base64: str) -> tuple[bytes, int, bool]:
|
|
37
|
+
""" Convert a recoverable signature to a normal signature """
|
|
38
|
+
sig = b64decode(sig_base64)
|
|
39
|
+
if len(sig) != 65:
|
|
40
|
+
raise Bip46RecoverPubkeyError("Invalid signature length")
|
|
41
|
+
header = int(sig[0])
|
|
42
|
+
if header < 27 or header > 42:
|
|
43
|
+
raise Bip46RecoverPubkeyError(f"Header out of range: {header}")
|
|
44
|
+
r = sig[1:33]
|
|
45
|
+
s = sig[33:]
|
|
46
|
+
compressed = False
|
|
47
|
+
if(header >= 39): # this is a bech32 signature
|
|
48
|
+
header -= 12
|
|
49
|
+
compressed = True
|
|
50
|
+
# this is a segwit p2sh signature
|
|
51
|
+
elif header >= 35:
|
|
52
|
+
header -= 8
|
|
53
|
+
compressed = True
|
|
54
|
+
# this is a compressed key signature
|
|
55
|
+
elif header >= 31:
|
|
56
|
+
compressed = True
|
|
57
|
+
header -= 4
|
|
58
|
+
rec_id = header - 27
|
|
59
|
+
return r + s, rec_id, compressed
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def recover_from_signature_and_message(sig_base64: str, message: str) -> bytes:
|
|
63
|
+
"""Recover a pubkey from signature and message"""
|
|
64
|
+
message_hash = prepare_certificate_message(message)
|
|
65
|
+
sig, recid, _ = convert_recoverable_message_signature(sig_base64)
|
|
66
|
+
empty = PublicKey()
|
|
67
|
+
sig = empty.ecdsa_recoverable_deserialize(sig, recid)
|
|
68
|
+
pubkey = empty.ecdsa_recover(message_hash, sig, raw=True)
|
|
69
|
+
return PublicKey(pubkey).serialize()
|
bip46-1.0.0/bip46/cli.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
""" bip46 CLI """
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from embit.bip32 import HDKey
|
|
10
|
+
|
|
11
|
+
from bip46 import (
|
|
12
|
+
create_redeemscript,
|
|
13
|
+
hdkey_derive,
|
|
14
|
+
hdkey_from_mnemonic,
|
|
15
|
+
hdkey_from_seed,
|
|
16
|
+
hdkey_from_xpub,
|
|
17
|
+
hdkey_scan,
|
|
18
|
+
hdkey_scan_all,
|
|
19
|
+
hdkey_to_pubkey,
|
|
20
|
+
index_to_lockdate,
|
|
21
|
+
lockdate_to_derivation_path,
|
|
22
|
+
redeemscript_address,
|
|
23
|
+
redeemscript_pubkey,
|
|
24
|
+
)
|
|
25
|
+
from bip46.consts import DEFAULT_NETWORK
|
|
26
|
+
|
|
27
|
+
# disable tracebacks on exceptions
|
|
28
|
+
sys.tracebacklimit = 0
|
|
29
|
+
|
|
30
|
+
def _check_hdkey(network: str) -> HDKey:
|
|
31
|
+
"""check if the environment is set"""
|
|
32
|
+
if "XPUB" in os.environ:
|
|
33
|
+
return hdkey_from_xpub(os.environ["XPUB"])
|
|
34
|
+
if "SEED" in os.environ:
|
|
35
|
+
return hdkey_from_seed(os.environ["SEED"].encode(), network)
|
|
36
|
+
if "MNEMONIC" in os.environ:
|
|
37
|
+
return hdkey_from_mnemonic(os.environ["MNEMONIC"], network)
|
|
38
|
+
|
|
39
|
+
click.echo("please set `XPUB`, `SEED` or `MNEMONIC` environment variable")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group()
|
|
44
|
+
def command_group():
|
|
45
|
+
"""
|
|
46
|
+
Python CLI for bip46 timelocks\n
|
|
47
|
+
set `SEED` or `MNEMONIC` environment variable to set the seed
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.command()
|
|
52
|
+
@click.argument("network", type=str, default=DEFAULT_NETWORK)
|
|
53
|
+
def scan_all(network: str):
|
|
54
|
+
"""
|
|
55
|
+
scan for all timelocks
|
|
56
|
+
"""
|
|
57
|
+
key = _check_hdkey(network)
|
|
58
|
+
bonds = hdkey_scan_all(key, network)
|
|
59
|
+
print(f"Found {len(bonds)} timelocked bonds:")
|
|
60
|
+
for bond in bonds:
|
|
61
|
+
print(bond.index, bond.txid, bond.address)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.command()
|
|
65
|
+
@click.argument("index", type=int)
|
|
66
|
+
@click.argument("network", type=str, default=DEFAULT_NETWORK)
|
|
67
|
+
def scan(index: int, network: str):
|
|
68
|
+
"""
|
|
69
|
+
scan for a timelock at a specific index
|
|
70
|
+
"""
|
|
71
|
+
key = _check_hdkey(network)
|
|
72
|
+
bonds = hdkey_scan(index, key, network)
|
|
73
|
+
print(f"Found {len(bonds)} timelocked bonds")
|
|
74
|
+
for bond in bonds:
|
|
75
|
+
print(bond.index, bond.txid, bond.address)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.command()
|
|
79
|
+
@click.argument("year", type=int)
|
|
80
|
+
@click.argument("month", type=int)
|
|
81
|
+
@click.argument("network", type=str, default=DEFAULT_NETWORK)
|
|
82
|
+
def create_timelock(year: int, month: int, network: str):
|
|
83
|
+
"""
|
|
84
|
+
create a timelock address
|
|
85
|
+
"""
|
|
86
|
+
key = _check_hdkey(network)
|
|
87
|
+
lock_date = datetime(year, month, 1, tzinfo=UTC)
|
|
88
|
+
lock_path = lockdate_to_derivation_path(lock_date, network)
|
|
89
|
+
redeem_child = hdkey_derive(key, lock_path)
|
|
90
|
+
redeem_pub_key = hdkey_to_pubkey(redeem_child)
|
|
91
|
+
redeem_script = create_redeemscript(lock_date, redeem_pub_key)
|
|
92
|
+
script_pubkey = redeemscript_pubkey(redeem_script)
|
|
93
|
+
script_address = redeemscript_address(script_pubkey, network=network)
|
|
94
|
+
|
|
95
|
+
print(f"network: {network}")
|
|
96
|
+
print(f"lock_date: {lock_date.isoformat()}")
|
|
97
|
+
print(f"lock_path: {lock_path}")
|
|
98
|
+
print(f"script address: {script_address}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@click.command()
|
|
102
|
+
@click.argument("year", type=int)
|
|
103
|
+
@click.argument("month", type=int)
|
|
104
|
+
@click.argument("network", type=str, default=DEFAULT_NETWORK)
|
|
105
|
+
def get_derivation_path(year: int, month: int, network: str):
|
|
106
|
+
"""
|
|
107
|
+
create a timelock address
|
|
108
|
+
"""
|
|
109
|
+
lock_date = datetime(year, month, 1, tzinfo=UTC)
|
|
110
|
+
lock_path = lockdate_to_derivation_path(lock_date, network)
|
|
111
|
+
print(lock_path)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@click.command()
|
|
115
|
+
@click.argument("index", type=int)
|
|
116
|
+
def get_lockdate(index: int):
|
|
117
|
+
"""
|
|
118
|
+
get the lock date from an index
|
|
119
|
+
"""
|
|
120
|
+
lock_path = index_to_lockdate(index)
|
|
121
|
+
print(lock_path.isoformat())
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main():
|
|
125
|
+
"""main function"""
|
|
126
|
+
command_group.add_command(create_timelock)
|
|
127
|
+
command_group.add_command(scan)
|
|
128
|
+
command_group.add_command(scan_all)
|
|
129
|
+
command_group.add_command(get_derivation_path)
|
|
130
|
+
command_group.add_command(get_lockdate)
|
|
131
|
+
command_group()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
ELECTRS_SERVER = "https://testnet-electrs.b1tco1n.org"
|
|
2
|
+
MAX_INDEX = 959
|
|
3
|
+
DEFAULT_NETWORK = "testnet"
|
|
4
|
+
NETWORKS = {
|
|
5
|
+
"mainnet": {
|
|
6
|
+
"bech32": "bc",
|
|
7
|
+
"xprv": b"\x04\x88\xad\xe4",
|
|
8
|
+
"bip32": 0, # coin type for bip32 derivation
|
|
9
|
+
},
|
|
10
|
+
"testnet": {
|
|
11
|
+
"bech32": "tb",
|
|
12
|
+
"xprv": b"\x04\x35\x83\x94",
|
|
13
|
+
"bip32": 1,
|
|
14
|
+
},
|
|
15
|
+
"regtest": {
|
|
16
|
+
"bech32": "bcrt",
|
|
17
|
+
"xprv": b"\x04\x35\x83\x94",
|
|
18
|
+
"bip32": 1,
|
|
19
|
+
},
|
|
20
|
+
"signet": {
|
|
21
|
+
"bech32": "tb",
|
|
22
|
+
"xprv": b"\x04\x35\x83\x94",
|
|
23
|
+
"bip32": 1,
|
|
24
|
+
},
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
from .consts import DEFAULT_NETWORK, NETWORKS
|
|
4
|
+
from .exceptions import Bip46IndexError, Bip46TimeError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def index_to_lockdate(index: int) -> datetime:
|
|
8
|
+
"""Convert a BIP46 timelock index to a datetime"""
|
|
9
|
+
if index < 0 or index > 959:
|
|
10
|
+
raise Bip46IndexError("index must be between 0 and 959")
|
|
11
|
+
year = 2020 + index // 12
|
|
12
|
+
month = 1 + index % 12
|
|
13
|
+
day = 1
|
|
14
|
+
return datetime(year, month, day, tzinfo=UTC)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def lockdate_to_index(lock_date: datetime) -> int:
|
|
18
|
+
"""Convert a datetime to a BIP46 timelock index"""
|
|
19
|
+
if lock_date.year < 2020:
|
|
20
|
+
raise Bip46TimeError("lock_date must be after 2020")
|
|
21
|
+
if lock_date.year > 2099:
|
|
22
|
+
raise Bip46TimeError("lock_date must be before 2100")
|
|
23
|
+
return (lock_date.year - 2020) * 12 + lock_date.month - 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def lockdate_to_derivation_path(
|
|
27
|
+
lock_date: datetime, network: str = DEFAULT_NETWORK
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Derive the path for a BIP46 timelock from a date"""
|
|
30
|
+
i = lockdate_to_index(lock_date)
|
|
31
|
+
return lockindex_to_derivation_path(i, network)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def lockindex_to_derivation_path(index: int, network: str = DEFAULT_NETWORK) -> str:
|
|
35
|
+
"""Derive the path for a BIP46 timelock from an index"""
|
|
36
|
+
bip32 = NETWORKS[network]["bip32"]
|
|
37
|
+
return f"m/84'/{bip32}'/0'/2/{index}"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
import httpx
|
|
3
|
+
|
|
4
|
+
from .consts import ELECTRS_SERVER
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def request(url: str) -> dict:
|
|
8
|
+
with httpx.Client() as client:
|
|
9
|
+
resp = client.get(url)
|
|
10
|
+
resp.raise_for_status()
|
|
11
|
+
return resp.json()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_tx(txid: str, electrs_server: str = ELECTRS_SERVER) -> dict:
|
|
15
|
+
"""Get a transaction from a block explorer"""
|
|
16
|
+
url = f"{electrs_server}/tx/{txid}"
|
|
17
|
+
return request(url)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_txs_from_address(address: str, electrs_server: str = ELECTRS_SERVER) -> dict:
|
|
21
|
+
"""Get a transaction from a block explorer"""
|
|
22
|
+
url = f"{electrs_server}/address/{address}/txs"
|
|
23
|
+
return request(url)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_vout_from_tx(tx: dict, address: str) -> tuple[str, int, int] | None:
|
|
27
|
+
for i, out in enumerate(tx["vout"]):
|
|
28
|
+
if out["scriptpubkey_address"] == address:
|
|
29
|
+
return tx["txid"], i, out["value"]
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_vout_from_txs(txs: dict, address: str) -> tuple[str, int, int] | None:
|
|
34
|
+
for tx in txs:
|
|
35
|
+
vout = get_vout_from_tx(tx, address)
|
|
36
|
+
if vout:
|
|
37
|
+
return vout
|
|
38
|
+
return None
|