blockchainkit 0.2.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.
- blockchainkit/__init__.py +12 -0
- blockchainkit/_validation.py +44 -0
- blockchainkit/consensus/__init__.py +66 -0
- blockchainkit/consensus/core/__init__.py +25 -0
- blockchainkit/consensus/core/base.py +97 -0
- blockchainkit/consensus/systems/__init__.py +46 -0
- blockchainkit/consensus/systems/byzantine.py +91 -0
- blockchainkit/consensus/systems/catch_up.py +60 -0
- blockchainkit/consensus/systems/difficulty.py +74 -0
- blockchainkit/consensus/systems/finality.py +92 -0
- blockchainkit/consensus/systems/fork_choice.py +44 -0
- blockchainkit/consensus/systems/pbft.py +78 -0
- blockchainkit/consensus/systems/pos.py +53 -0
- blockchainkit/consensus/systems/pow.py +58 -0
- blockchainkit/consensus/systems/pricing.py +50 -0
- blockchainkit/consensus/systems/randomized.py +114 -0
- blockchainkit/consensus/systems/selfish.py +81 -0
- blockchainkit/consensus/systems/sortition.py +48 -0
- blockchainkit/consensus/systems/stake_games.py +44 -0
- blockchainkit/consensus/systems/synchrony.py +49 -0
- blockchainkit/consensus/visualizers/__init__.py +9 -0
- blockchainkit/consensus/visualizers/plots.py +80 -0
- blockchainkit/constants.py +66 -0
- blockchainkit/crypto/__init__.py +157 -0
- blockchainkit/crypto/core/__init__.py +21 -0
- blockchainkit/crypto/core/base.py +109 -0
- blockchainkit/crypto/systems/__init__.py +137 -0
- blockchainkit/crypto/systems/asymmetric.py +136 -0
- blockchainkit/crypto/systems/commitments.py +93 -0
- blockchainkit/crypto/systems/curves.py +205 -0
- blockchainkit/crypto/systems/discrete_log.py +156 -0
- blockchainkit/crypto/systems/hashing.py +95 -0
- blockchainkit/crypto/systems/lamport.py +65 -0
- blockchainkit/crypto/systems/mac.py +34 -0
- blockchainkit/crypto/systems/merkle_damgard.py +139 -0
- blockchainkit/crypto/systems/multisig.py +98 -0
- blockchainkit/crypto/systems/one_time_pad.py +42 -0
- blockchainkit/crypto/systems/puzzles.py +96 -0
- blockchainkit/crypto/systems/sharing.py +159 -0
- blockchainkit/crypto/systems/signatures.py +232 -0
- blockchainkit/crypto/utils/__init__.py +5 -0
- blockchainkit/crypto/utils/primes.py +39 -0
- blockchainkit/crypto/visualizers/__init__.py +9 -0
- blockchainkit/crypto/visualizers/plots.py +88 -0
- blockchainkit/network/__init__.py +70 -0
- blockchainkit/network/core/__init__.py +21 -0
- blockchainkit/network/core/base.py +149 -0
- blockchainkit/network/systems/__init__.py +54 -0
- blockchainkit/network/systems/addresses.py +126 -0
- blockchainkit/network/systems/broadcast.py +124 -0
- blockchainkit/network/systems/clocks.py +149 -0
- blockchainkit/network/systems/epidemics.py +111 -0
- blockchainkit/network/systems/gossip.py +201 -0
- blockchainkit/network/systems/kademlia.py +147 -0
- blockchainkit/network/systems/privacy.py +98 -0
- blockchainkit/network/systems/propagation.py +60 -0
- blockchainkit/network/systems/relay.py +123 -0
- blockchainkit/network/systems/replication.py +122 -0
- blockchainkit/network/systems/topology.py +238 -0
- blockchainkit/network/visualizers/__init__.py +14 -0
- blockchainkit/network/visualizers/plots.py +177 -0
- blockchainkit/py.typed +0 -0
- blockchainkit/structures/__init__.py +61 -0
- blockchainkit/structures/core/__init__.py +21 -0
- blockchainkit/structures/core/base.py +100 -0
- blockchainkit/structures/systems/__init__.py +45 -0
- blockchainkit/structures/systems/block.py +144 -0
- blockchainkit/structures/systems/bloom.py +81 -0
- blockchainkit/structures/systems/chain.py +131 -0
- blockchainkit/structures/systems/hash_chain.py +35 -0
- blockchainkit/structures/systems/headers.py +54 -0
- blockchainkit/structures/systems/ledger.py +87 -0
- blockchainkit/structures/systems/merkle.py +273 -0
- blockchainkit/structures/systems/mmr.py +112 -0
- blockchainkit/structures/systems/sparse_merkle.py +139 -0
- blockchainkit/structures/systems/transaction.py +116 -0
- blockchainkit/structures/systems/utxo.py +156 -0
- blockchainkit/structures/utils/__init__.py +6 -0
- blockchainkit/structures/utils/accounts.py +13 -0
- blockchainkit/structures/utils/encoding.py +11 -0
- blockchainkit/structures/visualizers/__init__.py +13 -0
- blockchainkit/structures/visualizers/plots.py +154 -0
- blockchainkit/vm/__init__.py +60 -0
- blockchainkit/vm/core/__init__.py +25 -0
- blockchainkit/vm/core/base.py +171 -0
- blockchainkit/vm/systems/__init__.py +40 -0
- blockchainkit/vm/systems/assembler.py +80 -0
- blockchainkit/vm/systems/expressions.py +91 -0
- blockchainkit/vm/systems/programs.py +143 -0
- blockchainkit/vm/systems/reentrancy.py +70 -0
- blockchainkit/vm/systems/script.py +268 -0
- blockchainkit/vm/systems/stack_machine.py +239 -0
- blockchainkit/vm/systems/turing.py +111 -0
- blockchainkit/vm/systems/verifier.py +81 -0
- blockchainkit/vm/visualizers/__init__.py +9 -0
- blockchainkit/vm/visualizers/plots.py +97 -0
- blockchainkit-0.2.0.dist-info/METADATA +195 -0
- blockchainkit-0.2.0.dist-info/RECORD +101 -0
- blockchainkit-0.2.0.dist-info/WHEEL +5 -0
- blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- blockchainkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Result containers for blockchainkit.structures."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from blockchainkit._validation import integer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class MerkleProof:
|
|
10
|
+
"""Leaf position, leaf count, and bottom-up siblings (None for promotion)."""
|
|
11
|
+
|
|
12
|
+
index: int
|
|
13
|
+
leaf_count: int
|
|
14
|
+
siblings: tuple[bytes | None, ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProofStep:
|
|
19
|
+
"""One level of Merkle-proof verification, from the leaf toward the root.
|
|
20
|
+
|
|
21
|
+
Attributes
|
|
22
|
+
----------
|
|
23
|
+
side : {"left", "right", "promoted"}
|
|
24
|
+
Where the sibling sits: ``"left"`` means hash(sibling || current),
|
|
25
|
+
``"right"`` means hash(current || sibling), and ``"promoted"`` means
|
|
26
|
+
the node had no sibling and moved up unchanged.
|
|
27
|
+
sibling : bytes or None
|
|
28
|
+
The sibling digest from the proof (``None`` when promoted).
|
|
29
|
+
digest : bytes
|
|
30
|
+
The node digest after this step.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
side: str
|
|
34
|
+
sibling: bytes | None
|
|
35
|
+
digest: bytes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class MerkleTrace:
|
|
40
|
+
"""Every step of reconstructing a root from a leaf and its proof.
|
|
41
|
+
|
|
42
|
+
Attributes
|
|
43
|
+
----------
|
|
44
|
+
leaf_digest : bytes
|
|
45
|
+
The domain-separated hash of the leaf payload.
|
|
46
|
+
steps : tuple of ProofStep
|
|
47
|
+
Bottom-up reconstruction steps.
|
|
48
|
+
root : bytes
|
|
49
|
+
The count-bound root the proof reconstructs.
|
|
50
|
+
valid : bool
|
|
51
|
+
Whether that root equals the trusted root.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
leaf_digest: bytes
|
|
55
|
+
steps: tuple[ProofStep, ...]
|
|
56
|
+
root: bytes
|
|
57
|
+
valid: bool
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, order=True)
|
|
61
|
+
class OutPoint:
|
|
62
|
+
"""A reference to one coin: the creating transaction's id and the output index."""
|
|
63
|
+
|
|
64
|
+
txid: bytes
|
|
65
|
+
index: int
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
if not isinstance(self.txid, bytes) or len(self.txid) != 32:
|
|
69
|
+
raise ValueError("txid must be 32 bytes")
|
|
70
|
+
integer(self.index, "index")
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"OutPoint({self.txid.hex()[:12]}..., {self.index})"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class Coin:
|
|
78
|
+
"""An unspent output: who owns it and how much it holds."""
|
|
79
|
+
|
|
80
|
+
owner: str
|
|
81
|
+
amount: int
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class SparseMerkleProof:
|
|
86
|
+
"""Siblings from the leaf up, and the value at the key (None if absent)."""
|
|
87
|
+
|
|
88
|
+
key: bytes
|
|
89
|
+
value: bytes | None
|
|
90
|
+
siblings: tuple[bytes, ...]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class MMRProof:
|
|
95
|
+
"""Path from a leaf to its peak, plus every peak and the range size."""
|
|
96
|
+
|
|
97
|
+
index: int
|
|
98
|
+
size: int
|
|
99
|
+
siblings: tuple[bytes, ...]
|
|
100
|
+
peaks: tuple[bytes, ...]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Concrete authenticated structures, ledgers, and chains."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.structures.systems.block import Block, BlockHeader
|
|
4
|
+
from blockchainkit.structures.systems.bloom import BloomFilter
|
|
5
|
+
from blockchainkit.structures.systems.chain import Blockchain
|
|
6
|
+
from blockchainkit.structures.systems.hash_chain import hash_chain, verify_one_time_password
|
|
7
|
+
from blockchainkit.structures.systems.headers import verify_header_chain
|
|
8
|
+
from blockchainkit.structures.systems.ledger import Ledger
|
|
9
|
+
from blockchainkit.structures.systems.merkle import (
|
|
10
|
+
MerkleTree,
|
|
11
|
+
bitcoin_merkle_root,
|
|
12
|
+
consistency_proof,
|
|
13
|
+
trace_proof,
|
|
14
|
+
verify_consistency,
|
|
15
|
+
verify_proof,
|
|
16
|
+
)
|
|
17
|
+
from blockchainkit.structures.systems.mmr import MerkleMountainRange, verify_mmr_proof
|
|
18
|
+
from blockchainkit.structures.systems.sparse_merkle import SparseMerkleTree, verify_sparse_proof
|
|
19
|
+
from blockchainkit.structures.systems.transaction import Transaction, address
|
|
20
|
+
from blockchainkit.structures.systems.utxo import UTXOSet, UTXOTransaction
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Block",
|
|
24
|
+
"BlockHeader",
|
|
25
|
+
"BloomFilter",
|
|
26
|
+
"Blockchain",
|
|
27
|
+
"hash_chain",
|
|
28
|
+
"verify_one_time_password",
|
|
29
|
+
"verify_header_chain",
|
|
30
|
+
"Ledger",
|
|
31
|
+
"MerkleTree",
|
|
32
|
+
"bitcoin_merkle_root",
|
|
33
|
+
"consistency_proof",
|
|
34
|
+
"trace_proof",
|
|
35
|
+
"verify_consistency",
|
|
36
|
+
"verify_proof",
|
|
37
|
+
"MerkleMountainRange",
|
|
38
|
+
"verify_mmr_proof",
|
|
39
|
+
"SparseMerkleTree",
|
|
40
|
+
"verify_sparse_proof",
|
|
41
|
+
"Transaction",
|
|
42
|
+
"address",
|
|
43
|
+
"UTXOSet",
|
|
44
|
+
"UTXOTransaction",
|
|
45
|
+
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Immutable blocks with canonical headers and transaction commitments."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
from blockchainkit._validation import integer
|
|
7
|
+
from blockchainkit.constants import UINT64_LIMIT
|
|
8
|
+
from blockchainkit.crypto import sha256
|
|
9
|
+
from blockchainkit.structures.systems.merkle import MerkleTree
|
|
10
|
+
from blockchainkit.structures.systems.transaction import Transaction
|
|
11
|
+
from blockchainkit.structures.utils.encoding import canonical_json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _check_uint64(value: int, name: str) -> None:
|
|
15
|
+
integer(value, name)
|
|
16
|
+
if value >= UINT64_LIMIT:
|
|
17
|
+
raise ValueError(f"{name} must fit an unsigned 64-bit integer")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class BlockHeader:
|
|
22
|
+
"""The 80-byte-style summary a light client downloads instead of a block.
|
|
23
|
+
|
|
24
|
+
It commits to the block's transactions through ``merkle_root`` alone, so
|
|
25
|
+
its hash, and therefore its proof of work, can be checked without them.
|
|
26
|
+
``Block.to_header().hash == Block.hash``.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
previous_hash, merkle_root : bytes
|
|
31
|
+
32-byte digests.
|
|
32
|
+
height, timestamp, difficulty, nonce : int
|
|
33
|
+
As for :class:`Block`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
previous_hash: bytes = bytes(32)
|
|
37
|
+
merkle_root: bytes = bytes(32)
|
|
38
|
+
height: int = 0
|
|
39
|
+
timestamp: int = 0
|
|
40
|
+
difficulty: int = 8
|
|
41
|
+
nonce: int = 0
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
for name in ("previous_hash", "merkle_root"):
|
|
45
|
+
value = getattr(self, name)
|
|
46
|
+
if not isinstance(value, bytes) or len(value) != 32:
|
|
47
|
+
raise ValueError(f"{name} must be 32 bytes")
|
|
48
|
+
for name in ("height", "timestamp", "nonce", "difficulty"):
|
|
49
|
+
_check_uint64(getattr(self, name), name)
|
|
50
|
+
if self.difficulty > 256:
|
|
51
|
+
raise ValueError("difficulty cannot exceed 256 bits")
|
|
52
|
+
|
|
53
|
+
def encode(self) -> bytes:
|
|
54
|
+
"""Return the canonical bytes that are hashed."""
|
|
55
|
+
return canonical_json(
|
|
56
|
+
{
|
|
57
|
+
"version": 1,
|
|
58
|
+
"previous_hash": self.previous_hash.hex(),
|
|
59
|
+
"merkle_root": self.merkle_root.hex(),
|
|
60
|
+
"height": self.height,
|
|
61
|
+
"timestamp": self.timestamp,
|
|
62
|
+
"difficulty": self.difficulty,
|
|
63
|
+
"nonce": self.nonce,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@cached_property
|
|
68
|
+
def hash(self) -> bytes:
|
|
69
|
+
"""Return SHA-256 of the canonical header."""
|
|
70
|
+
return sha256(self.encode())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class Block:
|
|
75
|
+
"""A teaching block, not a Bitcoin/Ethereum wire-format block.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
previous_hash : bytes
|
|
80
|
+
Parent digest, or 32 zero bytes for genesis.
|
|
81
|
+
transactions : tuple of Transaction
|
|
82
|
+
Ordered signed transfers; copied into an immutable tuple.
|
|
83
|
+
height, timestamp, nonce : int
|
|
84
|
+
Nonnegative 64-bit integers; timestamps use simulation units.
|
|
85
|
+
difficulty : int
|
|
86
|
+
Number of required leading zero hash bits, between 0 and 256.
|
|
87
|
+
|
|
88
|
+
Notes
|
|
89
|
+
-----
|
|
90
|
+
The Merkle root and the block hash are computed once and cached. A
|
|
91
|
+
miner never rebuilds the transaction tree: it calls ``header(nonce=...)``
|
|
92
|
+
with candidate nonces, as real miners vary only the header.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
previous_hash: bytes = bytes(32)
|
|
96
|
+
transactions: tuple[Transaction, ...] = ()
|
|
97
|
+
height: int = 0
|
|
98
|
+
timestamp: int = 0
|
|
99
|
+
difficulty: int = 8
|
|
100
|
+
nonce: int = 0
|
|
101
|
+
|
|
102
|
+
def __post_init__(self) -> None:
|
|
103
|
+
if not isinstance(self.previous_hash, bytes) or len(self.previous_hash) != 32:
|
|
104
|
+
raise ValueError("previous_hash must be 32 bytes")
|
|
105
|
+
object.__setattr__(self, "transactions", tuple(self.transactions))
|
|
106
|
+
if any(not isinstance(tx, Transaction) for tx in self.transactions):
|
|
107
|
+
raise TypeError("transactions must contain Transaction instances")
|
|
108
|
+
for name in ("height", "timestamp", "nonce", "difficulty"):
|
|
109
|
+
_check_uint64(getattr(self, name), name)
|
|
110
|
+
if self.difficulty > 256:
|
|
111
|
+
raise ValueError("difficulty cannot exceed 256 bits")
|
|
112
|
+
|
|
113
|
+
@cached_property
|
|
114
|
+
def merkle_root(self) -> bytes:
|
|
115
|
+
"""Return the count-bound commitment to ordered signed transactions."""
|
|
116
|
+
return MerkleTree(tx.to_bytes() for tx in self.transactions).root
|
|
117
|
+
|
|
118
|
+
def header(self, nonce: int | None = None) -> bytes:
|
|
119
|
+
"""Return canonical bytes committing to all consensus-relevant fields.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
nonce : int, optional
|
|
124
|
+
A candidate nonce to place in the header instead of ``self.nonce``.
|
|
125
|
+
``block.header(nonce=n)`` equals ``replace(block, nonce=n).header()``
|
|
126
|
+
without rebuilding the Merkle tree.
|
|
127
|
+
"""
|
|
128
|
+
return self.to_header(nonce).encode()
|
|
129
|
+
|
|
130
|
+
def to_header(self, nonce: int | None = None) -> BlockHeader:
|
|
131
|
+
"""Return this block's header, optionally with a candidate nonce."""
|
|
132
|
+
return BlockHeader(
|
|
133
|
+
self.previous_hash,
|
|
134
|
+
self.merkle_root,
|
|
135
|
+
self.height,
|
|
136
|
+
self.timestamp,
|
|
137
|
+
self.difficulty,
|
|
138
|
+
self.nonce if nonce is None else nonce,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@cached_property
|
|
142
|
+
def hash(self) -> bytes:
|
|
143
|
+
"""Return SHA-256 of the canonical header."""
|
|
144
|
+
return sha256(self.header())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Bloom filters (1970): compact set membership with false positives but no false negatives.
|
|
2
|
+
|
|
3
|
+
Bitcoin's lightweight clients (BIP 37, 2012) sent full nodes a Bloom filter
|
|
4
|
+
of their addresses, so nodes could forward matching transactions without
|
|
5
|
+
the client listing its addresses outright. The false positives were meant
|
|
6
|
+
to give privacy; in practice they leaked much of it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from math import exp, log
|
|
10
|
+
|
|
11
|
+
from blockchainkit._validation import integer
|
|
12
|
+
from blockchainkit.constants import BLOOM_DOMAIN
|
|
13
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BloomFilter:
|
|
17
|
+
"""An array of ``size`` bits, set at ``hashes`` positions per added item.
|
|
18
|
+
|
|
19
|
+
An item is reported present if all its positions are set. Items that
|
|
20
|
+
were added are always found; others are wrongly found with probability
|
|
21
|
+
about ``(1 - exp(-k n / m))**k``.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
size : int
|
|
26
|
+
Number of bits m.
|
|
27
|
+
hashes : int
|
|
28
|
+
Number of positions k per item.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> from blockchainkit.structures import BloomFilter
|
|
33
|
+
>>> bloom = BloomFilter(256, 3)
|
|
34
|
+
>>> bloom.add(b"alice")
|
|
35
|
+
>>> b"alice" in bloom
|
|
36
|
+
True
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, size: int, hashes: int) -> None:
|
|
40
|
+
integer(size, "size", 1)
|
|
41
|
+
integer(hashes, "hashes", 1)
|
|
42
|
+
self._size, self._hashes = size, hashes
|
|
43
|
+
self._bits = bytearray((size + 7) // 8)
|
|
44
|
+
self._count = 0
|
|
45
|
+
|
|
46
|
+
def _positions(self, item: bytes) -> list[int]:
|
|
47
|
+
if not isinstance(item, bytes):
|
|
48
|
+
raise TypeError("items must be bytes")
|
|
49
|
+
return [
|
|
50
|
+
int.from_bytes(sha256(BLOOM_DOMAIN + i.to_bytes(4, "big") + item), "big") % self._size
|
|
51
|
+
for i in range(self._hashes)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
def add(self, item: bytes) -> None:
|
|
55
|
+
"""Set the item's bit positions."""
|
|
56
|
+
for position in self._positions(item):
|
|
57
|
+
self._bits[position // 8] |= 1 << (position % 8)
|
|
58
|
+
self._count += 1
|
|
59
|
+
|
|
60
|
+
def __contains__(self, item: object) -> bool:
|
|
61
|
+
if not isinstance(item, bytes):
|
|
62
|
+
return False
|
|
63
|
+
return all(self._bits[p // 8] >> (p % 8) & 1 for p in self._positions(item))
|
|
64
|
+
|
|
65
|
+
def __len__(self) -> int:
|
|
66
|
+
return self._count
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def fill_ratio(self) -> float:
|
|
70
|
+
"""Fraction of bits set."""
|
|
71
|
+
return sum(bin(byte).count("1") for byte in self._bits) / self._size
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def false_positive_rate(size: int, hashes: int, items: int) -> float:
|
|
75
|
+
"""Return Bloom's estimate ``(1 - exp(-k n / m))**k``."""
|
|
76
|
+
return float((1 - exp(-hashes * items / size)) ** hashes)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def optimal_hash_count(size: int, items: int) -> int:
|
|
80
|
+
"""Return the k minimizing false positives, ``round((m / n) ln 2)``."""
|
|
81
|
+
return max(1, round(size / items * log(2)))
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Cumulative-work fork selection for a fixed-difficulty chain."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
|
|
6
|
+
from blockchainkit.consensus.systems.pow import expected_trials, valid_pow
|
|
7
|
+
from blockchainkit.structures.systems.block import Block
|
|
8
|
+
from blockchainkit.structures.systems.ledger import Ledger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Blockchain:
|
|
12
|
+
"""Store valid forks and select the tip with greatest cumulative work.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
genesis : Block
|
|
17
|
+
Mined, empty height-zero block with a zero parent hash.
|
|
18
|
+
initial_state : Ledger, optional
|
|
19
|
+
Shared initial allocation and chain ID.
|
|
20
|
+
|
|
21
|
+
Notes
|
|
22
|
+
-----
|
|
23
|
+
Difficulty is fixed by genesis; a child cannot lower its own target.
|
|
24
|
+
Equal-work ties select the lexicographically smaller hash, so peers with
|
|
25
|
+
the same block set converge independent of arrival order. This teaching
|
|
26
|
+
tie-break is not Bitcoin's first-seen behavior. Each fork retains its own
|
|
27
|
+
ledger snapshot, so reorganizations restore balances and nonces.
|
|
28
|
+
|
|
29
|
+
Keeping a full snapshot per block makes reorganizations easy to inspect,
|
|
30
|
+
at a memory cost proportional to blocks times accounts. Real nodes keep
|
|
31
|
+
one state and undo data instead.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, genesis: Block, initial_state: Ledger | None = None) -> None:
|
|
35
|
+
if genesis.height != 0 or genesis.previous_hash != bytes(32) or genesis.transactions:
|
|
36
|
+
raise ValueError("genesis must have height zero, zero parent, and no transactions")
|
|
37
|
+
if not valid_pow(genesis):
|
|
38
|
+
raise ValueError("genesis proof of work is invalid")
|
|
39
|
+
self._blocks = {genesis.hash: genesis}
|
|
40
|
+
self._states = {genesis.hash: Ledger() if initial_state is None else initial_state}
|
|
41
|
+
self._work = {genesis.hash: expected_trials(genesis.difficulty)}
|
|
42
|
+
self._tip = genesis.hash
|
|
43
|
+
self._difficulty = genesis.difficulty
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return (
|
|
47
|
+
f"Blockchain(height={self.tip.height}, blocks={len(self._blocks)}, "
|
|
48
|
+
f"tip={self._tip.hex()[:16]}..., cumulative_work={self.cumulative_work})"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def tip(self) -> Block:
|
|
53
|
+
"""Return the selected canonical tip."""
|
|
54
|
+
return self._blocks[self._tip]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def state(self) -> Ledger:
|
|
58
|
+
"""Return the selected tip's validated ledger snapshot."""
|
|
59
|
+
return self._states[self._tip]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def cumulative_work(self) -> int:
|
|
63
|
+
"""Return total expected hash trials represented by the canonical chain."""
|
|
64
|
+
return self._work[self._tip]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def blocks(self) -> Mapping[bytes, Block]:
|
|
68
|
+
"""Read-only view of every stored block by hash, including side forks."""
|
|
69
|
+
return MappingProxyType(self._blocks)
|
|
70
|
+
|
|
71
|
+
def tips(self) -> tuple[Block, ...]:
|
|
72
|
+
"""Return the blocks that have no child: the tip of every fork.
|
|
73
|
+
|
|
74
|
+
Ordered as fork choice ranks them: most cumulative work first, ties
|
|
75
|
+
broken by the smaller hash. The first tip is always :attr:`tip`.
|
|
76
|
+
"""
|
|
77
|
+
parents = {block.previous_hash for block in self._blocks.values()}
|
|
78
|
+
leaves = [digest for digest in self._blocks if digest not in parents]
|
|
79
|
+
leaves.sort(key=lambda digest: (-self._work[digest], digest))
|
|
80
|
+
return tuple(self._blocks[digest] for digest in leaves)
|
|
81
|
+
|
|
82
|
+
def work_at(self, block_hash: bytes) -> int:
|
|
83
|
+
"""Return the cumulative expected work of the chain ending at a stored block.
|
|
84
|
+
|
|
85
|
+
Raises KeyError for an unknown hash.
|
|
86
|
+
"""
|
|
87
|
+
return self._work[block_hash]
|
|
88
|
+
|
|
89
|
+
def state_at(self, block_hash: bytes) -> Ledger:
|
|
90
|
+
"""Return the ledger snapshot after a stored block, on whichever fork it lies.
|
|
91
|
+
|
|
92
|
+
Raises KeyError for an unknown hash.
|
|
93
|
+
"""
|
|
94
|
+
return self._states[block_hash]
|
|
95
|
+
|
|
96
|
+
def contains(self, block_hash: bytes) -> bool:
|
|
97
|
+
"""Return whether this node already knows a block, including side forks."""
|
|
98
|
+
return block_hash in self._blocks
|
|
99
|
+
|
|
100
|
+
def add(self, block: Block) -> bool:
|
|
101
|
+
"""Validate and store a child; return whether the preferred tip changed.
|
|
102
|
+
|
|
103
|
+
Unknown parents raise ValueError. Networking experiments can buffer
|
|
104
|
+
such blocks until their parents arrive. Duplicate blocks are ignored.
|
|
105
|
+
"""
|
|
106
|
+
digest = block.hash
|
|
107
|
+
if digest in self._blocks:
|
|
108
|
+
return False
|
|
109
|
+
parent = self._blocks.get(block.previous_hash)
|
|
110
|
+
if parent is None:
|
|
111
|
+
raise ValueError("unknown parent")
|
|
112
|
+
if block.height != parent.height + 1 or block.timestamp < parent.timestamp:
|
|
113
|
+
raise ValueError("invalid height or timestamp before parent")
|
|
114
|
+
if block.difficulty != self._difficulty or not valid_pow(block):
|
|
115
|
+
raise ValueError("invalid difficulty or proof of work")
|
|
116
|
+
state = self._states[parent.hash].apply(block.transactions)
|
|
117
|
+
work = self._work[parent.hash] + expected_trials(block.difficulty)
|
|
118
|
+
self._blocks[digest], self._states[digest], self._work[digest] = block, state, work
|
|
119
|
+
preferred = work > self._work[self._tip] or (
|
|
120
|
+
work == self._work[self._tip] and digest < self._tip
|
|
121
|
+
)
|
|
122
|
+
if preferred:
|
|
123
|
+
self._tip = digest
|
|
124
|
+
return preferred
|
|
125
|
+
|
|
126
|
+
def canonical_blocks(self) -> tuple[Block, ...]:
|
|
127
|
+
"""Return the selected chain from genesis through tip."""
|
|
128
|
+
result = [self.tip]
|
|
129
|
+
while result[-1].height:
|
|
130
|
+
result.append(self._blocks[result[-1].previous_hash])
|
|
131
|
+
return tuple(reversed(result))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Lamport's hash chains (1981): one-time passwords from repeated hashing.
|
|
2
|
+
|
|
3
|
+
Hash a secret seed n times. The server stores only the last link. To log in,
|
|
4
|
+
the user reveals the link before it; the server hashes it once, compares,
|
|
5
|
+
and stores the revealed link as the new anchor. An eavesdropper who captures
|
|
6
|
+
a password learns a value the server will never accept again, and cannot
|
|
7
|
+
compute the next one without inverting the hash. This became S/KEY (1995).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import hmac
|
|
11
|
+
|
|
12
|
+
from blockchainkit._validation import integer
|
|
13
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def hash_chain(seed: bytes, length: int) -> tuple[bytes, ...]:
|
|
17
|
+
"""Return ``(H(seed), H(H(seed)), ..., H^length(seed))``.
|
|
18
|
+
|
|
19
|
+
>>> from blockchainkit.structures import hash_chain
|
|
20
|
+
>>> len(hash_chain(b"seed", 3))
|
|
21
|
+
3
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(seed, bytes):
|
|
24
|
+
raise TypeError("seed must be bytes")
|
|
25
|
+
integer(length, "length", 1)
|
|
26
|
+
links, current = [], seed
|
|
27
|
+
for _ in range(length):
|
|
28
|
+
current = sha256(current)
|
|
29
|
+
links.append(current)
|
|
30
|
+
return tuple(links)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def verify_one_time_password(password: bytes, anchor: bytes) -> bool:
|
|
34
|
+
"""Return whether ``H(password)`` equals the stored anchor (constant-time)."""
|
|
35
|
+
return isinstance(password, bytes) and hmac.compare_digest(sha256(password), anchor)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Simplified payment verification: checking a chain of headers without blocks.
|
|
2
|
+
|
|
3
|
+
Nakamoto's whitepaper (section 8) observed that a client can verify a payment
|
|
4
|
+
without downloading the blockchain: keep only the block headers, check that
|
|
5
|
+
they link and carry proof of work, and ask for a Merkle proof that the
|
|
6
|
+
transaction is in one of them. Trust shifts to an assumption: the
|
|
7
|
+
heaviest header chain is the one honest miners extended.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
from blockchainkit.consensus.systems.pow import expected_trials, target
|
|
13
|
+
from blockchainkit.structures.systems.block import BlockHeader
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def verify_header_chain(headers: Sequence[BlockHeader]) -> int:
|
|
17
|
+
"""Check that headers link by hash and carry their proof of work.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
headers : sequence of BlockHeader
|
|
22
|
+
Consecutive headers, oldest first (not necessarily from genesis).
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
int
|
|
27
|
+
The expected work they represent, the sum of ``2**difficulty``.
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
ValueError
|
|
32
|
+
The list is empty, a header does not link to its predecessor, or a
|
|
33
|
+
hash misses its target.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> import blockchainkit as bk
|
|
38
|
+
>>> genesis = bk.consensus.mine(bk.structures.Block(difficulty=4)).block
|
|
39
|
+
>>> bk.structures.verify_header_chain([genesis.to_header()])
|
|
40
|
+
16
|
|
41
|
+
"""
|
|
42
|
+
if not headers:
|
|
43
|
+
raise ValueError("an empty header chain proves nothing")
|
|
44
|
+
work = 0
|
|
45
|
+
for index, header in enumerate(headers):
|
|
46
|
+
if index and (
|
|
47
|
+
header.previous_hash != headers[index - 1].hash
|
|
48
|
+
or header.height != headers[index - 1].height + 1
|
|
49
|
+
):
|
|
50
|
+
raise ValueError(f"header {index} does not link to its predecessor")
|
|
51
|
+
if int.from_bytes(header.hash, "big") > target(header.difficulty):
|
|
52
|
+
raise ValueError(f"header {index} misses its proof of work target")
|
|
53
|
+
work += expected_trials(header.difficulty)
|
|
54
|
+
return work
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Immutable account balances and nonces, updated atomically by signed transfers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
|
|
6
|
+
from blockchainkit._validation import integer
|
|
7
|
+
from blockchainkit.constants import DEFAULT_CHAIN_ID
|
|
8
|
+
from blockchainkit.structures.systems.transaction import Transaction
|
|
9
|
+
from blockchainkit.structures.utils.accounts import is_account_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Ledger:
|
|
13
|
+
"""An immutable view of account balances and next expected nonces.
|
|
14
|
+
|
|
15
|
+
Initial balances are a shared simulation configuration, not a minting
|
|
16
|
+
transaction. All peers must start with the same allocation and chain ID.
|
|
17
|
+
Applying a batch returns a new state; failures leave the old state intact.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
balances: Mapping[str, int] | None = None,
|
|
23
|
+
nonces: Mapping[str, int] | None = None,
|
|
24
|
+
*,
|
|
25
|
+
chain_id: str = DEFAULT_CHAIN_ID,
|
|
26
|
+
) -> None:
|
|
27
|
+
if not isinstance(chain_id, str) or not 1 <= len(chain_id) <= 128:
|
|
28
|
+
raise ValueError("chain_id must contain 1 to 128 characters")
|
|
29
|
+
for mapping in (balances or {}, nonces or {}):
|
|
30
|
+
for account, value in mapping.items():
|
|
31
|
+
if not is_account_id(account):
|
|
32
|
+
raise ValueError("account IDs must be 64 lowercase hex characters")
|
|
33
|
+
integer(value, "account value")
|
|
34
|
+
self._balances = MappingProxyType(dict(balances or {}))
|
|
35
|
+
self._nonces = MappingProxyType(dict(nonces or {}))
|
|
36
|
+
self._chain_id = chain_id
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return (
|
|
40
|
+
f"Ledger(chain_id={self._chain_id!r}, accounts={len(self._balances)}, "
|
|
41
|
+
f"supply={self.total_supply})"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def total_supply(self) -> int:
|
|
46
|
+
"""Return the sum of all balances, which transfers never change.
|
|
47
|
+
|
|
48
|
+
Every transfer debits one account and credits another by the same
|
|
49
|
+
amount: Pacioli's double-entry rule. A change in total supply would
|
|
50
|
+
mean money was created or destroyed.
|
|
51
|
+
"""
|
|
52
|
+
return sum(self._balances.values())
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def balances(self) -> Mapping[str, int]:
|
|
56
|
+
"""Read-only account balances in integer units."""
|
|
57
|
+
return self._balances
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def nonces(self) -> Mapping[str, int]:
|
|
61
|
+
"""Read-only next expected sequence numbers (missing accounts start at 0)."""
|
|
62
|
+
return self._nonces
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def chain_id(self) -> str:
|
|
66
|
+
"""Return the signature domain accepted by this ledger."""
|
|
67
|
+
return self._chain_id
|
|
68
|
+
|
|
69
|
+
def apply(self, transactions: Iterable[Transaction]) -> "Ledger":
|
|
70
|
+
"""Validate and atomically apply transfers in order, returning a new ledger.
|
|
71
|
+
|
|
72
|
+
Rejects invalid signatures, wrong chain IDs, replayed/out-of-order
|
|
73
|
+
nonces, and insufficient funds. Self-transfers still consume a nonce.
|
|
74
|
+
"""
|
|
75
|
+
balances, nonces = dict(self.balances), dict(self.nonces)
|
|
76
|
+
for tx in transactions:
|
|
77
|
+
if tx.chain_id != self.chain_id or not tx.is_valid():
|
|
78
|
+
raise ValueError("wrong chain ID or invalid signature")
|
|
79
|
+
sender = tx.sender_address
|
|
80
|
+
if tx.nonce != nonces.get(sender, 0):
|
|
81
|
+
raise ValueError("unexpected account nonce (replay or out-of-order transfer)")
|
|
82
|
+
if tx.amount > balances.get(sender, 0):
|
|
83
|
+
raise ValueError("insufficient funds")
|
|
84
|
+
balances[sender] = balances.get(sender, 0) - tx.amount
|
|
85
|
+
balances[tx.recipient] = balances.get(tx.recipient, 0) + tx.amount
|
|
86
|
+
nonces[sender] = tx.nonce + 1
|
|
87
|
+
return Ledger(balances, nonces, chain_id=self.chain_id)
|