bip46 1.0.0__py3-none-any.whl
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/__init__.py +76 -0
- bip46/certificate.py +69 -0
- bip46/cli.py +135 -0
- bip46/consts.py +25 -0
- bip46/derivation.py +37 -0
- bip46/electrs.py +38 -0
- bip46/exceptions.py +21 -0
- bip46/hdkey.py +106 -0
- bip46/py.typed +0 -0
- bip46/script.py +44 -0
- bip46-1.0.0.dist-info/METADATA +89 -0
- bip46-1.0.0.dist-info/RECORD +15 -0
- bip46-1.0.0.dist-info/WHEEL +4 -0
- bip46-1.0.0.dist-info/entry_points.txt +2 -0
- bip46-1.0.0.dist-info/licenses/LICENSE +21 -0
bip46/__init__.py
ADDED
|
@@ -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
|
+
]
|
bip46/certificate.py
ADDED
|
@@ -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/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()
|
bip46/consts.py
ADDED
|
@@ -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
|
+
}
|
bip46/derivation.py
ADDED
|
@@ -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}"
|
bip46/electrs.py
ADDED
|
@@ -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
|
bip46/exceptions.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
""" Custom exceptions for the bip46 module """
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Bip46TimeError(Exception):
|
|
5
|
+
"""Raised when a locktime is not in the valid range for BIP46 timelocks"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Bip46IndexError(Exception):
|
|
9
|
+
"""Raised when a locktime index is not in the valid range for BIP46 timelocks"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Bip46PathError(Exception):
|
|
13
|
+
"""Raised when the derivation path is invalid"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Bip46Bech32Error(Exception):
|
|
17
|
+
"""Raised when the bech32 encoding fails"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Bip46RecoverPubkeyError(Exception):
|
|
21
|
+
"""Raised when we could not recover the pubkey from signature and message"""
|
bip46/hdkey.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from embit.bip32 import HDKey
|
|
4
|
+
from embit.bip39 import mnemonic_to_seed
|
|
5
|
+
|
|
6
|
+
from .consts import DEFAULT_NETWORK, MAX_INDEX, NETWORKS
|
|
7
|
+
from .derivation import index_to_lockdate, lockindex_to_derivation_path
|
|
8
|
+
from .electrs import get_txs_from_address
|
|
9
|
+
from .exceptions import Bip46PathError
|
|
10
|
+
from .script import create_redeemscript, redeemscript_address, redeemscript_pubkey
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Bond:
|
|
15
|
+
index: int
|
|
16
|
+
address: str
|
|
17
|
+
txid: str
|
|
18
|
+
hdkey: HDKey
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hdkey_scan(index: int, hdkey: HDKey, network: str = DEFAULT_NETWORK) -> list[Bond]:
|
|
22
|
+
"""
|
|
23
|
+
Scan a path for all possible keys timelocks
|
|
24
|
+
"""
|
|
25
|
+
bonds = []
|
|
26
|
+
path = lockindex_to_derivation_path(index, network)
|
|
27
|
+
hdkey_i = hdkey.derive(path)
|
|
28
|
+
pubkey = hdkey_to_pubkey(hdkey_i)
|
|
29
|
+
lock_date = index_to_lockdate(index)
|
|
30
|
+
redeemscript = create_redeemscript(lock_date, pubkey)
|
|
31
|
+
address = redeemscript_address(redeemscript_pubkey(redeemscript), network)
|
|
32
|
+
print(f"({index}/{MAX_INDEX}) Scanning timelocks for address {address}")
|
|
33
|
+
txs = get_txs_from_address(address)
|
|
34
|
+
if len(txs) > 0:
|
|
35
|
+
print(f"Found {len(txs)} timelocks for index {index} !!!")
|
|
36
|
+
for tx in txs:
|
|
37
|
+
bonds.append(Bond(index, address, tx.get("txid"), hdkey_i))
|
|
38
|
+
return bonds
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def hdkey_scan_all(hdkey: HDKey, network: str = DEFAULT_NETWORK) -> list[Bond]:
|
|
42
|
+
"""
|
|
43
|
+
Scan a path for all possible keys timelocks
|
|
44
|
+
"""
|
|
45
|
+
bonds = []
|
|
46
|
+
for i in range(MAX_INDEX + 1):
|
|
47
|
+
bonds.extend(hdkey_scan(i, hdkey, network))
|
|
48
|
+
return bonds
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def hdkey_from_seed(seed: bytes, network: str = DEFAULT_NETWORK) -> HDKey:
|
|
52
|
+
"""Create a HDKey from a seed"""
|
|
53
|
+
version = NETWORKS[network]["xprv"]
|
|
54
|
+
master = HDKey.from_seed(seed, version=version)
|
|
55
|
+
return master
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def hdkey_from_mnemonic(mnemonic: str, network: str = DEFAULT_NETWORK) -> HDKey:
|
|
59
|
+
"""Create a HDKey from a mnemonic"""
|
|
60
|
+
seed = mnemonic_to_seed(mnemonic)
|
|
61
|
+
return hdkey_from_seed(seed, network)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def hdkey_from_xpub(xpub: str) -> HDKey:
|
|
65
|
+
"""Create a public HDKey from an account-level xpub."""
|
|
66
|
+
hdkey = HDKey.from_base58(xpub)
|
|
67
|
+
if hdkey.is_private:
|
|
68
|
+
raise Bip46PathError("Expected a public extended key")
|
|
69
|
+
return hdkey
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def hdkey_derive(hdkey: HDKey, path: str) -> HDKey:
|
|
73
|
+
"""
|
|
74
|
+
Derive a child key from a hdkey
|
|
75
|
+
Path should be in the form of m/x/y/z where x' means hardened
|
|
76
|
+
first child path for mainnet is m/84'/0'/0'/2/0
|
|
77
|
+
"""
|
|
78
|
+
if not path.startswith("m"):
|
|
79
|
+
raise Bip46PathError(f"Invalid Path, should start with `m`: {path}")
|
|
80
|
+
if not path.endswith(f"/2/{path.split('/')[-1]}"):
|
|
81
|
+
raise Bip46PathError(f"Invalid Path, should end with `/2/x`: {path}")
|
|
82
|
+
if not hdkey.is_private:
|
|
83
|
+
path = _account_xpub_path(hdkey, path)
|
|
84
|
+
return hdkey.derive(path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def hdkey_to_pubkey(hdkey: HDKey) -> bytes:
|
|
88
|
+
"""Get the pubkey from an HDKey"""
|
|
89
|
+
return hdkey.get_public_key().sec()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _account_xpub_path(hdkey: HDKey, path: str) -> str:
|
|
93
|
+
if hdkey.depth != 3:
|
|
94
|
+
raise Bip46PathError(
|
|
95
|
+
"Public derivation requires an account-level xpub for m/84'/coin'/0'"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
parts = path.split("/")
|
|
99
|
+
if len(parts) == 6 and all(part.endswith("'") for part in parts[1:4]):
|
|
100
|
+
return "m/" + "/".join(parts[4:])
|
|
101
|
+
if len(parts) == 3:
|
|
102
|
+
return path
|
|
103
|
+
raise Bip46PathError(
|
|
104
|
+
"Public derivation requires an account-level xpub for m/84'/coin'/0' "
|
|
105
|
+
"and a BIP46 path ending in /2/index"
|
|
106
|
+
)
|
bip46/py.typed
ADDED
|
File without changes
|
bip46/script.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from hashlib import sha256
|
|
3
|
+
|
|
4
|
+
import bech32
|
|
5
|
+
|
|
6
|
+
from .consts import DEFAULT_NETWORK, NETWORKS
|
|
7
|
+
from .exceptions import Bip46Bech32Error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def lockdate_to_little_endian(locktime: datetime) -> bytes:
|
|
11
|
+
"""Convert a lockdate to little-endian bytes for use in a script"""
|
|
12
|
+
# max signed int for 4bytes
|
|
13
|
+
max_int = 2**31 - 1
|
|
14
|
+
ts = int(locktime.timestamp())
|
|
15
|
+
size = 4 if ts <= max_int else 5
|
|
16
|
+
return ts.to_bytes(size, "little")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_redeemscript(lock_date: datetime, pubkey: bytes) -> bytes:
|
|
20
|
+
"""Create a redeemscript pubkey for a BIP46 timelock"""
|
|
21
|
+
locktime_bytes = lockdate_to_little_endian(lock_date)
|
|
22
|
+
return (
|
|
23
|
+
bytes([len(locktime_bytes)]) # 1 byte len of locktime
|
|
24
|
+
+ locktime_bytes # 4 or 5 bytes of locktime
|
|
25
|
+
+ bytes([177, 117]) # OP_CLTV OP_DROP
|
|
26
|
+
+ bytes([len(pubkey)]) # 1 byte len of pubkey
|
|
27
|
+
+ pubkey # pubkey
|
|
28
|
+
+ bytes([172]) # OP_CHECKSIG
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def redeemscript_pubkey(redeemscript: bytes) -> bytes:
|
|
33
|
+
"""Create a redeemscript pubkey for a BIP46 timelock"""
|
|
34
|
+
hashed = sha256(redeemscript).digest()
|
|
35
|
+
return bytes([0, len(hashed)]) + hashed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def redeemscript_address(script_pubkey: bytes, network: str = DEFAULT_NETWORK) -> str:
|
|
39
|
+
"""Create a p2wpkh_address redeemscript address for a BIP46 timelock"""
|
|
40
|
+
hrp = NETWORKS[network]["bech32"]
|
|
41
|
+
address = bech32.encode(str(hrp), 0, script_pubkey[2:])
|
|
42
|
+
if not address:
|
|
43
|
+
raise Bip46Bech32Error("Could not encode address")
|
|
44
|
+
return address
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
bip46/__init__.py,sha256=QmOukqjhY3Wd1pw6YMjwcQ0rnrw1QD93obIguWJBWhE,1953
|
|
2
|
+
bip46/certificate.py,sha256=hK3ULNQKvyy79dvbd8N6wbZ1hySbGyNgX5WrJRYVQRc,2338
|
|
3
|
+
bip46/cli.py,sha256=h8OSBdSn71_C3Qhr5EUTLa3mnogE2fjWhw48djwBj4Y,3682
|
|
4
|
+
bip46/consts.py,sha256=9NyEhKSkKmZiNFZrrZCkTznL8btQN24dRiFhR5E2ud0,569
|
|
5
|
+
bip46/derivation.py,sha256=kFbu4zbZ0InSya3sNuJnoaxwc-Eibfgu1vHZ6dMpEr0,1290
|
|
6
|
+
bip46/electrs.py,sha256=3LJcE_UXL43kK74E25zrERm_iuuAw2n72dgrhrYBAyA,1043
|
|
7
|
+
bip46/exceptions.py,sha256=XchCU6VLnsUk-glgmAxtUzZa9NgMetXsGAopcrcULTc,580
|
|
8
|
+
bip46/hdkey.py,sha256=2U74EhFoNjXsKxHnZwcwe4CA7SeAnr-zjwx1QfB8i7Q,3466
|
|
9
|
+
bip46/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
bip46/script.py,sha256=a7cfNL59guLe8wPp_uyQAb-12_do3avOLsinou1aoug,1514
|
|
11
|
+
bip46-1.0.0.dist-info/METADATA,sha256=SPqaBnMgF2uoLOQw2l_DnOYkJoCjybEju9sdJT1qQTY,2573
|
|
12
|
+
bip46-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
13
|
+
bip46-1.0.0.dist-info/entry_points.txt,sha256=erLAErWuxedSmof6WoPRgby4KJuFotSBW_8Frln9v_Y,41
|
|
14
|
+
bip46-1.0.0.dist-info/licenses/LICENSE,sha256=5nXjp5shAeUfsQid_GPb2_rUzhDxjW4G96kn0RL8C94,1064
|
|
15
|
+
bip46-1.0.0.dist-info/RECORD,,
|
|
@@ -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.
|