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.
Files changed (101) hide show
  1. blockchainkit/__init__.py +12 -0
  2. blockchainkit/_validation.py +44 -0
  3. blockchainkit/consensus/__init__.py +66 -0
  4. blockchainkit/consensus/core/__init__.py +25 -0
  5. blockchainkit/consensus/core/base.py +97 -0
  6. blockchainkit/consensus/systems/__init__.py +46 -0
  7. blockchainkit/consensus/systems/byzantine.py +91 -0
  8. blockchainkit/consensus/systems/catch_up.py +60 -0
  9. blockchainkit/consensus/systems/difficulty.py +74 -0
  10. blockchainkit/consensus/systems/finality.py +92 -0
  11. blockchainkit/consensus/systems/fork_choice.py +44 -0
  12. blockchainkit/consensus/systems/pbft.py +78 -0
  13. blockchainkit/consensus/systems/pos.py +53 -0
  14. blockchainkit/consensus/systems/pow.py +58 -0
  15. blockchainkit/consensus/systems/pricing.py +50 -0
  16. blockchainkit/consensus/systems/randomized.py +114 -0
  17. blockchainkit/consensus/systems/selfish.py +81 -0
  18. blockchainkit/consensus/systems/sortition.py +48 -0
  19. blockchainkit/consensus/systems/stake_games.py +44 -0
  20. blockchainkit/consensus/systems/synchrony.py +49 -0
  21. blockchainkit/consensus/visualizers/__init__.py +9 -0
  22. blockchainkit/consensus/visualizers/plots.py +80 -0
  23. blockchainkit/constants.py +66 -0
  24. blockchainkit/crypto/__init__.py +157 -0
  25. blockchainkit/crypto/core/__init__.py +21 -0
  26. blockchainkit/crypto/core/base.py +109 -0
  27. blockchainkit/crypto/systems/__init__.py +137 -0
  28. blockchainkit/crypto/systems/asymmetric.py +136 -0
  29. blockchainkit/crypto/systems/commitments.py +93 -0
  30. blockchainkit/crypto/systems/curves.py +205 -0
  31. blockchainkit/crypto/systems/discrete_log.py +156 -0
  32. blockchainkit/crypto/systems/hashing.py +95 -0
  33. blockchainkit/crypto/systems/lamport.py +65 -0
  34. blockchainkit/crypto/systems/mac.py +34 -0
  35. blockchainkit/crypto/systems/merkle_damgard.py +139 -0
  36. blockchainkit/crypto/systems/multisig.py +98 -0
  37. blockchainkit/crypto/systems/one_time_pad.py +42 -0
  38. blockchainkit/crypto/systems/puzzles.py +96 -0
  39. blockchainkit/crypto/systems/sharing.py +159 -0
  40. blockchainkit/crypto/systems/signatures.py +232 -0
  41. blockchainkit/crypto/utils/__init__.py +5 -0
  42. blockchainkit/crypto/utils/primes.py +39 -0
  43. blockchainkit/crypto/visualizers/__init__.py +9 -0
  44. blockchainkit/crypto/visualizers/plots.py +88 -0
  45. blockchainkit/network/__init__.py +70 -0
  46. blockchainkit/network/core/__init__.py +21 -0
  47. blockchainkit/network/core/base.py +149 -0
  48. blockchainkit/network/systems/__init__.py +54 -0
  49. blockchainkit/network/systems/addresses.py +126 -0
  50. blockchainkit/network/systems/broadcast.py +124 -0
  51. blockchainkit/network/systems/clocks.py +149 -0
  52. blockchainkit/network/systems/epidemics.py +111 -0
  53. blockchainkit/network/systems/gossip.py +201 -0
  54. blockchainkit/network/systems/kademlia.py +147 -0
  55. blockchainkit/network/systems/privacy.py +98 -0
  56. blockchainkit/network/systems/propagation.py +60 -0
  57. blockchainkit/network/systems/relay.py +123 -0
  58. blockchainkit/network/systems/replication.py +122 -0
  59. blockchainkit/network/systems/topology.py +238 -0
  60. blockchainkit/network/visualizers/__init__.py +14 -0
  61. blockchainkit/network/visualizers/plots.py +177 -0
  62. blockchainkit/py.typed +0 -0
  63. blockchainkit/structures/__init__.py +61 -0
  64. blockchainkit/structures/core/__init__.py +21 -0
  65. blockchainkit/structures/core/base.py +100 -0
  66. blockchainkit/structures/systems/__init__.py +45 -0
  67. blockchainkit/structures/systems/block.py +144 -0
  68. blockchainkit/structures/systems/bloom.py +81 -0
  69. blockchainkit/structures/systems/chain.py +131 -0
  70. blockchainkit/structures/systems/hash_chain.py +35 -0
  71. blockchainkit/structures/systems/headers.py +54 -0
  72. blockchainkit/structures/systems/ledger.py +87 -0
  73. blockchainkit/structures/systems/merkle.py +273 -0
  74. blockchainkit/structures/systems/mmr.py +112 -0
  75. blockchainkit/structures/systems/sparse_merkle.py +139 -0
  76. blockchainkit/structures/systems/transaction.py +116 -0
  77. blockchainkit/structures/systems/utxo.py +156 -0
  78. blockchainkit/structures/utils/__init__.py +6 -0
  79. blockchainkit/structures/utils/accounts.py +13 -0
  80. blockchainkit/structures/utils/encoding.py +11 -0
  81. blockchainkit/structures/visualizers/__init__.py +13 -0
  82. blockchainkit/structures/visualizers/plots.py +154 -0
  83. blockchainkit/vm/__init__.py +60 -0
  84. blockchainkit/vm/core/__init__.py +25 -0
  85. blockchainkit/vm/core/base.py +171 -0
  86. blockchainkit/vm/systems/__init__.py +40 -0
  87. blockchainkit/vm/systems/assembler.py +80 -0
  88. blockchainkit/vm/systems/expressions.py +91 -0
  89. blockchainkit/vm/systems/programs.py +143 -0
  90. blockchainkit/vm/systems/reentrancy.py +70 -0
  91. blockchainkit/vm/systems/script.py +268 -0
  92. blockchainkit/vm/systems/stack_machine.py +239 -0
  93. blockchainkit/vm/systems/turing.py +111 -0
  94. blockchainkit/vm/systems/verifier.py +81 -0
  95. blockchainkit/vm/visualizers/__init__.py +9 -0
  96. blockchainkit/vm/visualizers/plots.py +97 -0
  97. blockchainkit-0.2.0.dist-info/METADATA +195 -0
  98. blockchainkit-0.2.0.dist-info/RECORD +101 -0
  99. blockchainkit-0.2.0.dist-info/WHEEL +5 -0
  100. blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
  101. 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)