Skip to content

Commit 950a193

Browse files
committed
to_entropy, more doctests, light refactor / cleanup, pyproject, remove openssl
1 parent 1c9f4df commit 950a193

20 files changed

+240
-499
lines changed

README.md

+1-20
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
# `bits10001010010101101`
22

3-
`bits` is a cli tool and pure Python library enabling easy and convenient use for all things Bitcoin
4-
5-
## Highlights
6-
7-
- Generate private key and calculate corresponding public key
8-
- Calculate standard address types (P2PKH, P2SH, Multisig, Segwit)
9-
- Create and sign transactions
10-
- Calculate mnemonic phrase and derive extended keys from seed (per BIP32, BIP39, BIP43, BIP44)
11-
- RPC interface with local `bitcoind` node
3+
`bits` is a cli tool and pure Python library for Bitcoin
124

135
## Dependencies
146

@@ -19,14 +11,3 @@
1911
```bash
2012
pip install bits
2113
```
22-
23-
v0.1.0 - MVP
24-
25-
v0.2.0
26-
27-
- `sweep` cli command i.e. send all (or fraction) associated with from_addr to to_addr (optional change_addr)
28-
29-
v1.0.0
30-
31-
- full node
32-
- `scan` cli command

bits/__init__.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ def load_config():
6060
bitsconfig_file.close()
6161

6262

63-
bitsconfig = {}
64-
load_config()
65-
66-
6763
def read_bytes(file_: Optional[IO] = None, input_format: str = "raw") -> bytes:
6864
"""
6965
Read from optional file or stdin and convert to bytes
@@ -98,3 +94,7 @@ def print_bytes(data: bytes, output_format: str = "raw"):
9894
print(format(int.from_bytes(data, "big"), format_spec))
9995
else:
10096
raise ValueError(f"unrecognized output format: {output_format}")
97+
98+
99+
bitsconfig = {}
100+
load_config()

bits/bips/bip39/__init__.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
"""
2+
https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
3+
"""
14
import hashlib
25
import secrets
36
import unicodedata
@@ -10,7 +13,11 @@ def load_wordlist():
1013
return words
1114

1215

13-
def calculate_mnemonic_phrase(entropy: bytes):
16+
def calculate_mnemonic_phrase(entropy: bytes) -> str:
17+
"""
18+
>>> calculate_mnemonic_phrase(bytes.fromhex("6610b25967cdcca9d59875f5cb50b0ea75433311869e930b"))
19+
'gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog'
20+
"""
1421
strength = len(entropy) * 8
1522
if strength not in [128, 160, 192, 224, 256]:
1623
raise ValueError(
@@ -33,7 +40,43 @@ def calculate_mnemonic_phrase(entropy: bytes):
3340
return " ".join([words[bit_group] for bit_group in reversed(bit_groups)])
3441

3542

36-
def to_seed(mnemonic, passphrase: str = "") -> bytes:
43+
def to_entropy(mnemonic: str) -> bytes:
44+
"""
45+
Inverse of calculate_mnemonic_phrase(entropy)
46+
Get original entropy from mnemonic
47+
Args:
48+
mnemonic: str, mnemonic phrase
49+
50+
>>> to_entropy("gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog").hex()
51+
'6610b25967cdcca9d59875f5cb50b0ea75433311869e930b'
52+
"""
53+
words = mnemonic.split()
54+
if len(words) not in [12, 15, 18, 21, 24]:
55+
raise ValueError("word length does not indicate a valid entropy bit length")
56+
entropy_w_checksum_bitlen = len(words) * 11
57+
entropy_w_checksum_len = (entropy_w_checksum_bitlen + 7) // 8
58+
checksum_bitlen = len(words) // 3
59+
entropy_bitlen = entropy_w_checksum_bitlen - checksum_bitlen
60+
entropy_len = (entropy_bitlen + 7) // 8
61+
62+
wordlist = load_wordlist()
63+
64+
data = 0
65+
for idx, word in enumerate(reversed(words)):
66+
bit_group = wordlist.index(word)
67+
data |= bit_group << (idx * 11)
68+
69+
checksum = (
70+
data.to_bytes(entropy_w_checksum_len, "big")[-1] & 2**checksum_bitlen - 1
71+
)
72+
entropy = data >> checksum_bitlen
73+
entropy = entropy.to_bytes(entropy_len, "big")
74+
checksum_check = hashlib.sha256(entropy).digest()[0] >> (8 - checksum_bitlen)
75+
assert checksum_check == checksum, "checksum validation error"
76+
return entropy
77+
78+
79+
def to_seed(mnemonic: str, passphrase: str = "") -> bytes:
3780
"""
3881
Defined in BIP39
3982
https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
@@ -45,7 +88,3 @@ def to_seed(mnemonic, passphrase: str = "") -> bytes:
4588
unicodedata.normalize("NFKD", "mnemonic" + passphrase).encode("utf-8"),
4689
ITERATIONS,
4790
)
48-
49-
50-
def to_entropy():
51-
raise NotImplementedError

bits/blockchain.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
from bits.script.utils import p2pk_script_pubkey
99
from bits.tx import coinbase_txin
1010
from bits.tx import tx
11+
from bits.tx import tx_deser
1112
from bits.tx import txout
1213
from bits.utils import compact_size_uint
1314
from bits.utils import d_hash
15+
from bits.utils import parse_compact_size_uint
1416

1517

1618
COIN = 100000000 # satoshis / bitcoin
@@ -121,24 +123,41 @@ def genesis_coinbase_tx():
121123

122124

123125
def genesis_block():
126+
"""
127+
Hard coded genesis block - mainnet
128+
>>> gb = genesis_block()
129+
>>> header = gb[:80]
130+
>>> import hashlib
131+
>>> hashlib.sha256(hashlib.sha256(header).digest()).digest()[::-1].hex()
132+
'000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
133+
"""
134+
124135
# https://github.com/bitcoin/bitcoin/blob/v0.1.5/main.cpp#L1495-L1498
125136
version: int = 1
126137
nTime = 1231006505
127-
nBits = b"\x1D\x00\xFF\xFF"
138+
nBits = 0x1D00FFFF
128139
nNonce = 2083236893
129140

130141
coinbase_tx = genesis_coinbase_tx()
131142
merkle_ = merkle_root([coinbase_tx])
132-
133143
return block_ser(
134-
block_header(1, NULL_32, merkle_, nTime, nBits, nNonce), [coinbase_tx]
144+
block_header(1, NULL_32, merkle_, nTime, nBits.to_bytes(4, "little"), nNonce),
145+
[coinbase_tx],
135146
)
136147

137148

138149
def block_ser(blk_hdr: bytes, txns: List[bytes]) -> bytes:
139150
return blk_hdr + compact_size_uint(len(txns)) + b"".join(txns)
140151

141152

142-
def block_deser(block: bytes) -> Tuple[bytes, List[bytes]]:
143-
# return block_header_, txns
144-
raise NotImplementedError
153+
def block_deser(block: bytes) -> Tuple[bytes, List[dict]]:
154+
header = block[:80]
155+
number_of_txns, block_prime = parse_compact_size_uint(block[80:])
156+
txns = []
157+
while block_prime:
158+
deserialized_tx, block_prime = tx_deser(block_prime)
159+
txns.append(deserialized_tx)
160+
assert (
161+
len(txns) == number_of_txns
162+
), "error during parsing - number of txns does not match"
163+
return header, txns

bits/db.py

-27
This file was deleted.

bits/integrations.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import bits.keys
1111
import bits.rpc
1212
from bits.script.utils import null_data_script_pubkey
13-
from bits.script.utils import p2pkh_script_pubkey
13+
from bits.script.utils import scriptpubkey
1414
from bits.utils import d_hash
1515
from bits.utils import pubkey_hash
1616
from bits.utils import to_bitcoin_address
@@ -77,7 +77,7 @@ def mine_block(recv_addr: Optional[bytes] = b"", network: str = "regtest"):
7777
"""
7878
Retrieve all raw mempool transactions and submit in a block
7979
Args:
80-
recv_addr: Optional[bytes], p2pkh addr to receive block reward
80+
recv_addr: Optional[bytes], addr to receive block reward
8181
"""
8282
current_block_height = bits.rpc.rpc_method("getblockcount")
8383
current_block_hash = bits.rpc.rpc_method("getblockhash", current_block_height)
@@ -98,8 +98,7 @@ def mine_block(recv_addr: Optional[bytes] = b"", network: str = "regtest"):
9898
]
9999

100100
if recv_addr:
101-
pk_hash = bits.base58.base58check_decode(recv_addr)[1:]
102-
script_pubkey = p2pkh_script_pubkey(pk_hash)
101+
script_pubkey = scriptpubkey(recv_addr)
103102
else:
104103
script_pubkey = null_data_script_pubkey(recv_addr)
105104

bits/openssl.py

-85
This file was deleted.

bits/script/utils.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,25 @@
1919

2020
def scriptpubkey(data: bytes) -> bytes:
2121
"""
22-
Create scriptpubkey by inferring input data addr type.
22+
Create scriptpubkey by inferring input data type.
2323
Returns identity if data not identified as pubkey, base58check, nor segwit
2424
25-
>>> scriptpubkey(bytes.fromhex("02726b45a5b1b506015dc926630b2627454d635d87eeb72bb7d5476d545d6769f9")) # p2pk
26-
27-
>>> scriptpubkey(b"1GhhwzPms6aKhzK5EcSYdeJ8T35BvAsn7y") # p2pkh
28-
29-
>>> scriptpubkey(b"3djjfdfkj) # p2sh
30-
31-
>>> scriptpubkey(b"bc1q4s7tflrwuenru6tuwsa26rvflk8tfs2lk5gysg") # p2wpkh
25+
>>> # p2pk
26+
>>> scriptpubkey(bytes.fromhex("025a058ec9fb35845ce07b6ec4929b443132b2fce2bb154e3aa66c19b851b0c449")).hex()
27+
'21025a058ec9fb35845ce07b6ec4929b443132b2fce2bb154e3aa66c19b851b0c449ac'
28+
>>> # p2pkh
29+
>>> scriptpubkey(b"1A4wionHnAtthCbCb9CTmDJaKuEPNXZp8R").hex()
30+
'76a91463780efe21b54d462d399b4c5b9902235aa570ec88ac'
31+
>>> # p2sh
32+
>>> scriptpubkey(b"3PSFZTX6WxhFTmPBLnCh6gwxomb4vvxSpP").hex()
33+
'a914ee87e9344a5ef0f83a0aa250256a3cc394ab750387'
34+
>>> # p2wpkh
35+
>>> scriptpubkey(b"bc1qvduqal3pk4x5vtfendx9hxgzydd22u8v0pzd7h").hex()
36+
'001463780efe21b54d462d399b4c5b9902235aa570ec'
37+
>>> # TODO: p2wsh
38+
>>> # raw
39+
>>> scriptpubkey(bytes.fromhex("5221024c9b21035e4823d6f09d5a948201d14086d854dfa5bba828c06f5131d9cfe14f2103fe0b5ca0ab60705b21a00cbd9900026f282c7188427123e87e0dc344ce742eb02102528e776c2bf0be68f4503151fd036c9cb720c4977f6f5b0248d5472c654aebe453ae")).hex()
40+
'5221024c9b21035e4823d6f09d5a948201d14086d854dfa5bba828c06f5131d9cfe14f2103fe0b5ca0ab60705b21a00cbd9900026f282c7188427123e87e0dc344ce742eb02102528e776c2bf0be68f4503151fd036c9cb720c4977f6f5b0248d5472c654aebe453ae'
3241
"""
3342
# data is either pubkey, base58check, or segwit
3443
if is_point(data):
@@ -227,7 +236,9 @@ def script(args: list[str]) -> bytes:
227236
Generic script
228237
Args:
229238
args: list, script ops / data
230-
>>> script(["OP_2", "03ffe6319b68b781d654e32b7b068e946eef2b0f094ba9eeb84308c6c58af71208", "03377714f72611b81ee5dfbe6f52bfe0e9b1f6827ca00b6ab90d899720b1df00fd", "02726b45a5b1b506015dc926630b2627454d635d87eeb72bb7d5476d545d6769f9", "OP_3"])
239+
240+
>>> script(["OP_2", "024c9b21035e4823d6f09d5a948201d14086d854dfa5bba828c06f5131d9cfe14f", "03fe0b5ca0ab60705b21a00cbd9900026f282c7188427123e87e0dc344ce742eb0", "02528e776c2bf0be68f4503151fd036c9cb720c4977f6f5b0248d5472c654aebe4", "OP_3", "OP_CHECKMULTISIG"]).hex()
241+
'5221024c9b21035e4823d6f09d5a948201d14086d854dfa5bba828c06f5131d9cfe14f2103fe0b5ca0ab60705b21a00cbd9900026f282c7188427123e87e0dc344ce742eb02102528e776c2bf0be68f4503151fd036c9cb720c4977f6f5b0248d5472c654aebe453ae'
231242
"""
232243
scriptbytes = b""
233244
for arg in args:

bits/tx.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from typing import Tuple
1212

1313
import bits.keys
14-
import bits.openssl
1514
import bits.script.constants
1615
import bits.utils
1716
from bits.base58 import base58check_decode
@@ -166,7 +165,7 @@ def coinbase_txin(
166165
(now required per BIP34)
167166
168167
"""
169-
if block_height:
168+
if block_height is not None:
170169
# "minimally encoded serialized CScript"
171170
if block_height <= 16:
172171
op = getattr(bits.script.constants, f"OP_{block_height}")

0 commit comments

Comments
 (0)