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,80 @@
1
+ """A two-pass assembler: write programs as text, with labels for jump targets.
2
+
3
+ Each line holds one instruction, an opcode with an optional operand, or a
4
+ label ending in ``:``. ``#`` starts a comment. A jump may name a label
5
+ instead of an address; the first pass records where each label points, the
6
+ second replaces label names by those addresses.
7
+
8
+ Example::
9
+
10
+ # Sum 1 + 2 + ... + n, with n in slot 0 and the sum in slot 1.
11
+ loop:
12
+ LOAD 0
13
+ JZ done
14
+ LOAD 1
15
+ LOAD 0
16
+ ADD
17
+ STORE 1
18
+ LOAD 0
19
+ PUSH 1
20
+ SUB
21
+ STORE 0
22
+ JMP loop
23
+ done:
24
+ STOP
25
+ """
26
+
27
+ from blockchainkit.vm.core.base import Instruction, VMError
28
+ from blockchainkit.vm.systems.stack_machine import OPERAND_OPCODES, validate_program
29
+
30
+
31
+ def assemble(source: str) -> tuple[Instruction, ...]:
32
+ """Translate assembly text into ``(opcode, operand)`` instructions.
33
+
34
+ Operands are decimal or ``0x`` hexadecimal integers, or labels for
35
+ ``JMP`` and ``JZ``.
36
+
37
+ Examples
38
+ --------
39
+ >>> from blockchainkit.vm import assemble
40
+ >>> assemble('''
41
+ ... start:
42
+ ... PUSH 0x10 # sixteen
43
+ ... JZ start
44
+ ... ''')
45
+ (('PUSH', 16), ('JZ', 0))
46
+ """
47
+ if not isinstance(source, str):
48
+ raise TypeError("source must be a str")
49
+ labels: dict[str, int] = {}
50
+ lines: list[tuple[int, list[str]]] = []
51
+ for number, raw in enumerate(source.splitlines(), start=1):
52
+ text = raw.split("#", 1)[0].strip()
53
+ if not text:
54
+ continue
55
+ if text.endswith(":"):
56
+ name = text[:-1].strip()
57
+ if not name.isidentifier() or name in labels:
58
+ raise VMError(f"line {number}: bad or repeated label {name!r}")
59
+ labels[name] = len(lines)
60
+ continue
61
+ lines.append((number, text.split()))
62
+ program: list[Instruction] = []
63
+ for number, words in lines:
64
+ op = words[0].upper()
65
+ if len(words) > 2:
66
+ raise VMError(f"line {number}: one operand at most")
67
+ if len(words) == 1:
68
+ program.append((op, None))
69
+ continue
70
+ word = words[1]
71
+ if op in {"JMP", "JZ"} and word in labels:
72
+ program.append((op, labels[word]))
73
+ elif op in OPERAND_OPCODES:
74
+ try:
75
+ program.append((op, int(word, 0)))
76
+ except ValueError:
77
+ raise VMError(f"line {number}: unknown label or number {word!r}") from None
78
+ else:
79
+ raise VMError(f"line {number}: {op} takes no operand")
80
+ return validate_program(program)
@@ -0,0 +1,91 @@
1
+ """Reverse Polish notation (Łukasiewicz 1924; Hamblin 1957) and its compilation.
2
+
3
+ Łukasiewicz wrote logical formulas with the operator first, which needs no
4
+ parentheses: ``+ 1 * 2 3``. Hamblin saw that the reversed form,
5
+ ``1 2 3 * +``, is exactly the order in which a stack machine computes: push
6
+ operands, and let each operator replace the top two values by its result.
7
+ Dijkstra's shunting-yard algorithm (1961) converts ordinary infix notation
8
+ to that order, which is how expressions become stack-machine code.
9
+ """
10
+
11
+ import re
12
+
13
+ from blockchainkit.vm.core.base import Instruction, VMError
14
+ from blockchainkit.vm.systems.stack_machine import check_word
15
+
16
+ _PRECEDENCE = {"+": 1, "-": 1, "*": 2, "/": 2}
17
+ _OPCODES = {"+": "ADD", "-": "SUB", "*": "MUL", "/": "DIV"}
18
+ _TOKEN = re.compile(r"\s*(?:(\d+)|(.))")
19
+
20
+
21
+ def to_rpn(expression: str) -> tuple[str, ...]:
22
+ """Convert an infix expression to reverse Polish notation (shunting yard).
23
+
24
+ Supports non-negative integers, ``+ - * /`` with the usual precedence
25
+ and left associativity, and parentheses.
26
+
27
+ Examples
28
+ --------
29
+ >>> from blockchainkit.vm import to_rpn
30
+ >>> to_rpn("(1 + 2) * 3 - 4")
31
+ ('1', '2', '+', '3', '*', '4', '-')
32
+ """
33
+ if not isinstance(expression, str):
34
+ raise TypeError("expression must be a str")
35
+ output: list[str] = []
36
+ operators: list[str] = []
37
+ expect_operand = True
38
+ for number, symbol in _TOKEN.findall(expression.strip()):
39
+ if number:
40
+ if not expect_operand:
41
+ raise VMError("two operands in a row")
42
+ output.append(number)
43
+ expect_operand = False
44
+ elif symbol == "(":
45
+ if not expect_operand:
46
+ raise VMError("missing operator before '('")
47
+ operators.append(symbol)
48
+ elif symbol == ")":
49
+ while operators and operators[-1] != "(":
50
+ output.append(operators.pop())
51
+ if not operators or expect_operand:
52
+ raise VMError("unbalanced or empty parentheses")
53
+ operators.pop()
54
+ elif symbol in _PRECEDENCE:
55
+ if expect_operand:
56
+ raise VMError(f"operator {symbol!r} is missing an operand")
57
+ while operators and _PRECEDENCE.get(operators[-1], 0) >= _PRECEDENCE[symbol]:
58
+ output.append(operators.pop())
59
+ operators.append(symbol)
60
+ expect_operand = True
61
+ else:
62
+ raise VMError(f"unexpected character {symbol!r}")
63
+ if expect_operand:
64
+ raise VMError("the expression is empty or ends with an operator")
65
+ while operators:
66
+ if operators[-1] == "(":
67
+ raise VMError("unbalanced parentheses")
68
+ output.append(operators.pop())
69
+ return tuple(output)
70
+
71
+
72
+ def compile_expression(expression: str) -> tuple[Instruction, ...]:
73
+ """Compile an infix expression to stack-machine instructions via RPN.
74
+
75
+ Each number becomes ``PUSH`` and each operator its opcode. Arithmetic is
76
+ then the machine's: modulo ``2**256``, with ``/`` as floor division.
77
+
78
+ Examples
79
+ --------
80
+ >>> from blockchainkit.vm import compile_expression, execute
81
+ >>> execute(compile_expression("(1 + 2) * 3 - 4")).stack
82
+ (5,)
83
+ """
84
+ program: list[Instruction] = []
85
+ for token in to_rpn(expression):
86
+ if token in _OPCODES:
87
+ program.append((_OPCODES[token], None))
88
+ else:
89
+ check_word(int(token), "every number")
90
+ program.append(("PUSH", int(token)))
91
+ return tuple(program)
@@ -0,0 +1,143 @@
1
+ """Small contracts written for the stack machine.
2
+
3
+ * :func:`vending_machine`: Szabo's (1994) example of a contract enforced by
4
+ a mechanism rather than a court: pay at least the price and an item and
5
+ your change come out; otherwise nothing happens.
6
+ * :func:`batch_transfer`: the token function behind the 2018 BeautyChain
7
+ (BEC) exploit, where ``count * value`` overflowed 256 bits.
8
+
9
+ Both are assembled with :func:`~blockchainkit.vm.systems.assembler.assemble`
10
+ and run with :func:`~blockchainkit.vm.systems.stack_machine.execute`.
11
+ """
12
+
13
+ from collections.abc import Sequence
14
+
15
+ from blockchainkit._validation import integer
16
+ from blockchainkit.vm.core.base import Instruction
17
+ from blockchainkit.vm.systems.assembler import assemble
18
+
19
+ STOCK, PRICE, REVENUE = 0, 1, 2
20
+ """Storage slots of the vending machine."""
21
+
22
+
23
+ def vending_machine() -> tuple[Instruction, ...]:
24
+ """A vending machine: argument ``payment``; storage holds stock, price and revenue.
25
+
26
+ On success the stock drops by one, the revenue grows by the price, and
27
+ the change ``payment - price`` is left on the stack. Paying too little,
28
+ or buying from an empty machine, reverts, so neither side can be cheated.
29
+
30
+ Examples
31
+ --------
32
+ >>> from blockchainkit.vm import execute, vending_machine
33
+ >>> result = execute(vending_machine(), arguments=(5,), storage={0: 3, 1: 2})
34
+ >>> result.stack, dict(result.storage)
35
+ ((3,), {0: 2, 1: 2, 2: 2})
36
+ """
37
+ return assemble(
38
+ f"""
39
+ # stack: payment
40
+ DUP
41
+ LOAD {PRICE}
42
+ LT # payment < price?
43
+ JZ paid
44
+ REVERT # too little: nothing happens
45
+ paid:
46
+ LOAD {STOCK}
47
+ JZ sold_out
48
+ LOAD {STOCK}
49
+ PUSH 1
50
+ SUB
51
+ STORE {STOCK} # hand over one item
52
+ LOAD {REVENUE}
53
+ LOAD {PRICE}
54
+ ADD
55
+ STORE {REVENUE}
56
+ LOAD {PRICE}
57
+ SUB # change = payment - price
58
+ STOP
59
+ sold_out:
60
+ REVERT
61
+ """
62
+ )
63
+
64
+
65
+ def batch_transfer(
66
+ sender: int, recipients: Sequence[int], *, checked: bool = False
67
+ ) -> tuple[Instruction, ...]:
68
+ """Pay ``value`` (the one argument) from ``sender`` to each recipient.
69
+
70
+ Balances live in storage, one slot per account. Like BeautyChain's
71
+ ``batchTransfer``, the program computes ``amount = count * value``,
72
+ checks the sender's balance against ``amount``, debits ``amount`` once
73
+ and credits ``value`` to every recipient. The multiplication wraps
74
+ modulo ``2**256``: with two recipients and ``value = 2**255``, ``amount``
75
+ is 0, every check passes, and each recipient receives ``2**255`` tokens
76
+ from nothing. ``checked=True`` adds the SafeMath test
77
+ ``amount / count == value``, which rejects the overflow.
78
+
79
+ Examples
80
+ --------
81
+ >>> from blockchainkit.vm import batch_transfer, execute
82
+ >>> program = batch_transfer(1, [2, 3])
83
+ >>> dict(execute(program, arguments=(10,), storage={1: 100}).storage)
84
+ {1: 80, 2: 10, 3: 10}
85
+ """
86
+ integer(sender, "sender")
87
+ if not recipients:
88
+ raise ValueError("provide at least one recipient")
89
+ for account in recipients:
90
+ integer(account, "recipient")
91
+ count = len(recipients)
92
+ overflow_check = (
93
+ f"""
94
+ DUP # value amount amount
95
+ PUSH {count}
96
+ DIV # value amount amount/count
97
+ ROT # amount amount/count value
98
+ DUP
99
+ ROT # amount value value amount/count
100
+ EQ
101
+ JZ overflow # SafeMath: the product must divide back
102
+ SWAP # value amount
103
+ """
104
+ if checked
105
+ else ""
106
+ )
107
+ credits = "\n".join(
108
+ f"""
109
+ DUP
110
+ LOAD {account}
111
+ ADD
112
+ STORE {account}"""
113
+ for account in recipients
114
+ )
115
+ return assemble(
116
+ f"""
117
+ # stack: value
118
+ DUP
119
+ JZ zero_value # require(value > 0)
120
+ DUP
121
+ PUSH {count}
122
+ MUL # value amount (wraps modulo 2**256)
123
+ {overflow_check}
124
+ DUP
125
+ LOAD {sender}
126
+ SWAP
127
+ LT # balance < amount?
128
+ JZ funded
129
+ REVERT # balance too low
130
+ funded:
131
+ LOAD {sender}
132
+ SWAP
133
+ SUB
134
+ STORE {sender} # debit amount once
135
+ {credits}
136
+ DROP
137
+ STOP
138
+ zero_value:
139
+ REVERT
140
+ {"overflow:" if checked else ""}
141
+ {"REVERT" if checked else ""}
142
+ """
143
+ )
@@ -0,0 +1,70 @@
1
+ """Reentrancy: the DAO attack (2016).
2
+
3
+ The DAO's withdrawal code sent ether *before* zeroing the caller's balance.
4
+ Sending ether to a contract runs that contract's code, so the attacker's
5
+ contract called ``withdraw`` again from inside the payment, while its
6
+ balance still showed the full deposit, and again, until the funds ran out
7
+ or the call stack's depth limit stopped it. About 3.6 million ether were
8
+ drained in June 2016.
9
+
10
+ The fix is the *checks-effects-interactions* order: check the conditions,
11
+ update the contract's own state, and only then call out. A re-entrant call
12
+ then finds a zero balance and gets nothing.
13
+
14
+ This module models the two orders directly in Python: the bank's state is
15
+ two numbers, and "sending" calls the attacker's fallback.
16
+ """
17
+
18
+ from blockchainkit._validation import integer
19
+ from blockchainkit.vm.core.base import ReentrancyResult
20
+
21
+
22
+ def drain_bank(
23
+ other_deposits: int,
24
+ attacker_deposit: int,
25
+ *,
26
+ checks_effects_interactions: bool = False,
27
+ max_depth: int = 1024,
28
+ ) -> ReentrancyResult:
29
+ """Let an attacker that re-enters on every payment withdraw from a bank.
30
+
31
+ Parameters
32
+ ----------
33
+ other_deposits : int
34
+ Funds belonging to everyone else.
35
+ attacker_deposit : int
36
+ The attacker's own deposit, at least 1.
37
+ checks_effects_interactions : bool
38
+ False: the vulnerable order (pay, then zero the balance). True: zero
39
+ the balance before paying.
40
+ max_depth : int
41
+ Maximum call depth (1024 in Ethereum then), bounding the recursion.
42
+
43
+ Examples
44
+ --------
45
+ >>> from blockchainkit.vm import drain_bank
46
+ >>> drain_bank(90, 10).stolen, drain_bank(90, 10, checks_effects_interactions=True).stolen
47
+ (90, 0)
48
+ """
49
+ integer(other_deposits, "other_deposits")
50
+ integer(attacker_deposit, "attacker_deposit", 1)
51
+ integer(max_depth, "max_depth", 1)
52
+ bank = other_deposits + attacker_deposit
53
+ balance = attacker_deposit
54
+ withdrawn = calls = 0
55
+ # Each pass is one nested call of withdraw, made from inside the
56
+ # previous call's payment. (A loop rather than recursion, because the
57
+ # 1024-call depth exceeds Python's own recursion limit.)
58
+ for _ in range(max_depth):
59
+ calls += 1
60
+ amount = balance # Checks: the caller's recorded balance.
61
+ if amount == 0 or bank < amount:
62
+ break
63
+ if checks_effects_interactions:
64
+ balance = 0 # Effects before the interaction.
65
+ bank -= amount
66
+ withdrawn += amount
67
+ # Interaction: the payment runs the attacker's fallback, which calls
68
+ # withdraw again (the next pass) unless the depth limit ends the loop.
69
+ # In the vulnerable order the balance is zeroed only as the calls unwind.
70
+ return ReentrancyResult(withdrawn, attacker_deposit, calls, bank)
@@ -0,0 +1,268 @@
1
+ """Bitcoin-style Script (2009): spending conditions as tiny stack programs.
2
+
3
+ A coin is locked by a *locking script*; to spend it, one supplies an
4
+ *unlocking script*. The unlocking script runs first, then the locking script
5
+ runs on the stack it left, and the spend is valid if no step fails and the
6
+ top of the stack is true. Script deliberately has no loops, so a script of n
7
+ operations runs at most n steps and needs no gas.
8
+
9
+ Teaching differences from Bitcoin: stack items are byte strings, but numbers
10
+ are big-endian; ``OP_HASH256`` (double SHA-256) replaces ``OP_HASH160`` in
11
+ pay-to-public-key-hash; and ``OP_CHECKSIG`` checks a blockchainkit Schnorr
12
+ signature over an explicit message rather than a transaction digest.
13
+ """
14
+
15
+ from collections.abc import Sequence
16
+
17
+ from blockchainkit._validation import integer
18
+ from blockchainkit.crypto.core.base import SchnorrSignature
19
+ from blockchainkit.crypto.systems.curves import SECP256K1, encode_point
20
+ from blockchainkit.crypto.systems.hashing import hash256, sha256
21
+ from blockchainkit.crypto.systems.signatures import verify
22
+ from blockchainkit.vm.core.base import ScriptResult
23
+
24
+ ScriptItem = bytes | str
25
+ """A data push (bytes) or an opcode name such as ``"OP_DUP"`` (str)."""
26
+
27
+ OPCODES = frozenset(
28
+ {
29
+ "OP_TRUE",
30
+ "OP_FALSE",
31
+ "OP_DUP",
32
+ "OP_DROP",
33
+ "OP_EQUAL",
34
+ "OP_EQUALVERIFY",
35
+ "OP_VERIFY",
36
+ "OP_SHA256",
37
+ "OP_HASH256",
38
+ "OP_CHECKSIG",
39
+ "OP_CHECKLOCKTIMEVERIFY",
40
+ "OP_IF",
41
+ "OP_ELSE",
42
+ "OP_ENDIF",
43
+ "OP_RETURN",
44
+ }
45
+ )
46
+ """Every opcode the interpreter accepts."""
47
+
48
+ _WIDTH = 32
49
+
50
+
51
+ def encode_public_key(public: tuple[int, int]) -> bytes:
52
+ """A public key as stack bytes (uncompressed point encoding)."""
53
+ return encode_point(public)
54
+
55
+
56
+ def encode_signature(signature: SchnorrSignature) -> bytes:
57
+ """A signature as stack bytes: the commitment point, then the 32-byte response."""
58
+ return encode_point(signature.commitment) + signature.response.to_bytes(_WIDTH, "big")
59
+
60
+
61
+ def _point(data: bytes) -> tuple[int, int]:
62
+ if len(data) != 1 + 2 * _WIDTH or data[0] != 4:
63
+ raise ValueError("not an encoded point")
64
+ return int.from_bytes(data[1 : 1 + _WIDTH], "big"), int.from_bytes(data[1 + _WIDTH :], "big")
65
+
66
+
67
+ def number(value: int) -> bytes:
68
+ """Encode a non-negative integer as minimal big-endian bytes (0 is empty)."""
69
+ integer(value, "value")
70
+ return value.to_bytes((value.bit_length() + 7) // 8, "big")
71
+
72
+
73
+ def _true(item: bytes) -> bool:
74
+ return any(item)
75
+
76
+
77
+ class _Fail(Exception):
78
+ pass
79
+
80
+
81
+ def _run(
82
+ script: Sequence[ScriptItem],
83
+ stack: list[bytes],
84
+ message: bytes,
85
+ locktime: int,
86
+ operations: list[int],
87
+ ) -> None:
88
+ executing: list[bool] = [] # One entry per open OP_IF.
89
+ for item in script:
90
+ active = all(executing)
91
+ if isinstance(item, bytes):
92
+ operations[0] += 1
93
+ if active:
94
+ stack.append(item)
95
+ continue
96
+ if item not in OPCODES:
97
+ raise _Fail(f"unknown opcode {item!r}")
98
+ if item in ("OP_IF", "OP_ELSE", "OP_ENDIF"):
99
+ operations[0] += 1
100
+ if item == "OP_IF":
101
+ if active:
102
+ if not stack:
103
+ raise _Fail("OP_IF on an empty stack")
104
+ executing.append(_true(stack.pop()))
105
+ else:
106
+ executing.append(False)
107
+ elif not executing:
108
+ raise _Fail(f"{item} without OP_IF")
109
+ elif item == "OP_ELSE":
110
+ executing[-1] = not executing[-1] and all(executing[:-1])
111
+ else:
112
+ executing.pop()
113
+ continue
114
+ if not active:
115
+ continue
116
+ operations[0] += 1
117
+ if item == "OP_RETURN":
118
+ raise _Fail("OP_RETURN marks the output unspendable")
119
+ if item == "OP_TRUE":
120
+ stack.append(b"\x01")
121
+ continue
122
+ if item == "OP_FALSE":
123
+ stack.append(b"")
124
+ continue
125
+ needed = 2 if item in ("OP_EQUAL", "OP_EQUALVERIFY", "OP_CHECKSIG") else 1
126
+ if len(stack) < needed:
127
+ raise _Fail(f"{item} needs {needed} stack items")
128
+ if item == "OP_DUP":
129
+ stack.append(stack[-1])
130
+ elif item == "OP_DROP":
131
+ stack.pop()
132
+ elif item in ("OP_EQUAL", "OP_EQUALVERIFY"):
133
+ equal = stack.pop() == stack.pop()
134
+ if item == "OP_EQUALVERIFY" and not equal:
135
+ raise _Fail("OP_EQUALVERIFY: items differ")
136
+ if item == "OP_EQUAL":
137
+ stack.append(b"\x01" if equal else b"")
138
+ elif item == "OP_VERIFY":
139
+ if not _true(stack.pop()):
140
+ raise _Fail("OP_VERIFY: false")
141
+ elif item == "OP_SHA256":
142
+ stack.append(sha256(stack.pop()))
143
+ elif item == "OP_HASH256":
144
+ stack.append(hash256(stack.pop()))
145
+ elif item == "OP_CHECKSIG":
146
+ public, signature = stack.pop(), stack.pop()
147
+ try:
148
+ point = _point(signature[: 1 + 2 * _WIDTH])
149
+ response = int.from_bytes(signature[1 + 2 * _WIDTH :], "big")
150
+ ok = len(signature) == 1 + 3 * _WIDTH and verify(
151
+ message, SchnorrSignature(point, response), _point(public), SECP256K1
152
+ )
153
+ except ValueError:
154
+ ok = False
155
+ stack.append(b"\x01" if ok else b"")
156
+ else: # OP_CHECKLOCKTIMEVERIFY leaves its operand, as in BIP 65.
157
+ if int.from_bytes(stack[-1], "big") > locktime:
158
+ raise _Fail("OP_CHECKLOCKTIMEVERIFY: the lock time has not been reached")
159
+ if executing:
160
+ raise _Fail("OP_IF without OP_ENDIF")
161
+
162
+
163
+ def verify_script(
164
+ unlocking: Sequence[ScriptItem],
165
+ locking: Sequence[ScriptItem],
166
+ *,
167
+ message: bytes = b"",
168
+ locktime: int = 0,
169
+ ) -> ScriptResult:
170
+ """Run the unlocking script, then the locking script on its stack, and judge the spend.
171
+
172
+ Parameters
173
+ ----------
174
+ unlocking, locking : sequence of bytes or str
175
+ Data pushes and opcode names.
176
+ message : bytes
177
+ What ``OP_CHECKSIG`` signatures must sign (the spending transaction).
178
+ locktime : int
179
+ The spending transaction's lock time, compared by
180
+ ``OP_CHECKLOCKTIMEVERIFY``.
181
+
182
+ Returns
183
+ -------
184
+ ScriptResult
185
+
186
+ Notes
187
+ -----
188
+ Running the scripts separately matters: until 2010 Bitcoin concatenated
189
+ them, and the unlocking script ``OP_TRUE OP_RETURN`` ended execution
190
+ early with true on top, spending any coin (CVE-2010-5141).
191
+
192
+ Examples
193
+ --------
194
+ >>> from blockchainkit.crypto import sha256
195
+ >>> from blockchainkit.vm import verify_script
196
+ >>> verify_script([b"secret"], ["OP_SHA256", sha256(b"secret"), "OP_EQUAL"]).valid
197
+ True
198
+ """
199
+ if not isinstance(message, bytes):
200
+ raise TypeError("message must be bytes")
201
+ integer(locktime, "locktime")
202
+ stack: list[bytes] = []
203
+ operations = [0] # Shared counter, so a failed run still reports its work.
204
+ try:
205
+ _run(unlocking, stack, message, locktime, operations)
206
+ _run(locking, stack, message, locktime, operations)
207
+ except _Fail as failure:
208
+ return ScriptResult(False, tuple(stack), str(failure), operations[0])
209
+ if not stack or not _true(stack[-1]):
210
+ return ScriptResult(False, tuple(stack), "the top of the stack is not true", operations[0])
211
+ return ScriptResult(True, tuple(stack), None, operations[0])
212
+
213
+
214
+ def p2pkh_locking(public: tuple[int, int]) -> tuple[ScriptItem, ...]:
215
+ """Pay to public-key hash: ``OP_DUP OP_HASH256 <hash> OP_EQUALVERIFY OP_CHECKSIG``.
216
+
217
+ The coin names only the hash of a key; the spender reveals the key and
218
+ a signature.
219
+ """
220
+ return (
221
+ "OP_DUP",
222
+ "OP_HASH256",
223
+ hash256(encode_public_key(public)),
224
+ "OP_EQUALVERIFY",
225
+ "OP_CHECKSIG",
226
+ )
227
+
228
+
229
+ def p2pkh_unlocking(signature: SchnorrSignature, public: tuple[int, int]) -> tuple[ScriptItem, ...]:
230
+ """The matching unlocking script: ``<signature> <public key>``."""
231
+ return (encode_signature(signature), encode_public_key(public))
232
+
233
+
234
+ def htlc_locking(
235
+ payment_hash: bytes,
236
+ recipient: tuple[int, int],
237
+ refund: tuple[int, int],
238
+ timeout: int,
239
+ ) -> tuple[ScriptItem, ...]:
240
+ """A hash time-locked contract.
241
+
242
+ The recipient can claim with a signature and the preimage of
243
+ ``payment_hash`` (SHA-256); after ``timeout``, the sender can take a
244
+ refund with its own signature::
245
+
246
+ OP_IF
247
+ OP_SHA256 <payment_hash> OP_EQUALVERIFY <recipient>
248
+ OP_ELSE
249
+ <timeout> OP_CHECKLOCKTIMEVERIFY OP_DROP <refund>
250
+ OP_ENDIF
251
+ OP_CHECKSIG
252
+ """
253
+ if not isinstance(payment_hash, bytes) or len(payment_hash) != 32:
254
+ raise ValueError("payment_hash must be a 32-byte SHA-256 digest")
255
+ return (
256
+ "OP_IF",
257
+ "OP_SHA256",
258
+ payment_hash,
259
+ "OP_EQUALVERIFY",
260
+ encode_public_key(recipient),
261
+ "OP_ELSE",
262
+ number(timeout),
263
+ "OP_CHECKLOCKTIMEVERIFY",
264
+ "OP_DROP",
265
+ encode_public_key(refund),
266
+ "OP_ENDIF",
267
+ "OP_CHECKSIG",
268
+ )