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,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
|
+
)
|