Skip to content

Commit 4ade1b1

Browse files
committed
feat(scripts): implement ScriptContext
1 parent e5a19b7 commit 4ade1b1

File tree

4 files changed

+134
-100
lines changed

4 files changed

+134
-100
lines changed

hathor/transaction/scripts/execute.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import NamedTuple, Optional, Union
1717

1818
from hathor.transaction import BaseTransaction, Transaction, TxInput
19-
from hathor.transaction.exceptions import DataIndexError, FinalStackInvalid, InvalidScriptError, OutOfData, ScriptError
19+
from hathor.transaction.exceptions import DataIndexError, FinalStackInvalid, InvalidScriptError, OutOfData
2020

2121

2222
class ScriptExtras(NamedTuple):
@@ -54,21 +54,19 @@ def execute_eval(data: bytes, log: list[str], extras: ScriptExtras) -> None:
5454
:raises ScriptError: case opcode is not found
5555
:raises FinalStackInvalid: case the evaluation fails
5656
"""
57-
from hathor.transaction.scripts.opcode import MAP_OPCODE_TO_FN, Opcode
57+
from hathor.transaction.scripts.opcode import Opcode, execute_op_code
58+
from hathor.transaction.scripts.script_context import ScriptContext
59+
context = ScriptContext()
5860
stack: Stack = []
5961
data_len = len(data)
6062
pos = 0
6163
while pos < data_len:
6264
opcode, pos = get_script_op(pos, data, stack)
6365
if Opcode.is_pushdata(opcode):
6466
continue
65-
# this is an opcode manipulating the stack
66-
fn = MAP_OPCODE_TO_FN.get(opcode, None)
67-
if fn is None:
68-
# throw error
69-
raise ScriptError('unknown opcode')
7067

71-
fn(stack, log, extras)
68+
# this is an opcode manipulating the stack
69+
execute_op_code(Opcode(opcode), stack, log, extras, context)
7270

7371
evaluate_final_stack(stack, log)
7472

hathor/transaction/scripts/opcode.py

+45-28
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import datetime
1616
import struct
1717
from enum import IntEnum
18-
from typing import Callable
1918

2019
from cryptography.exceptions import InvalidSignature
2120
from cryptography.hazmat.primitives import hashes
@@ -46,6 +45,7 @@
4645
get_data_value,
4746
get_script_op,
4847
)
48+
from hathor.transaction.scripts.script_context import ScriptContext
4949

5050

5151
class Opcode(IntEnum):
@@ -157,7 +157,7 @@ def op_pushdata1(position: int, full_data: bytes, stack: Stack) -> int:
157157
return new_pos
158158

159159

160-
def op_dup(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
160+
def op_dup(stack: Stack) -> None:
161161
"""Duplicates item on top of stack
162162
163163
:param stack: the stack used when evaluating the script
@@ -170,7 +170,7 @@ def op_dup(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
170170
stack.append(stack[-1])
171171

172172

173-
def op_greaterthan_timestamp(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
173+
def op_greaterthan_timestamp(stack: Stack, extras: ScriptExtras) -> None:
174174
"""Check whether transaction's timestamp is greater than the top of stack
175175
176176
The top of stack must be a big-endian u32int.
@@ -190,7 +190,7 @@ def op_greaterthan_timestamp(stack: Stack, log: list[str], extras: ScriptExtras)
190190
datetime.datetime.fromtimestamp(timelock).strftime("%m/%d/%Y %I:%M:%S %p")))
191191

192192

193-
def op_equalverify(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
193+
def op_equalverify(stack: Stack, log: list[str]) -> None:
194194
"""Verifies top 2 elements from stack are equal
195195
196196
:param stack: the stack used when evaluating the script
@@ -201,13 +201,13 @@ def op_equalverify(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
201201
"""
202202
if len(stack) < 2:
203203
raise MissingStackItems('OP_EQUALVERIFY: need 2 elements on stack, currently {}'.format(len(stack)))
204-
op_equal(stack, log, extras)
204+
op_equal(stack, log)
205205
is_equal = stack.pop()
206206
if not is_equal:
207207
raise EqualVerifyFailed('Failed to verify if elements are equal')
208208

209209

210-
def op_equal(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
210+
def op_equal(stack: Stack, log: list[str]) -> None:
211211
"""Verifies top 2 elements from stack are equal
212212
213213
In case they are the same, we push 1 to the stack and push 0 if they are different
@@ -265,7 +265,7 @@ def op_checksig(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
265265
log.append('OP_CHECKSIG: failed')
266266

267267

268-
def op_hash160(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
268+
def op_hash160(stack: Stack) -> None:
269269
"""Top stack item is hashed twice: first with SHA-256 and then with RIPEMD-160.
270270
Result is pushed back to stack.
271271
@@ -282,7 +282,7 @@ def op_hash160(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
282282
stack.append(new_elem)
283283

284284

285-
def op_checkdatasig(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
285+
def op_checkdatasig(stack: Stack) -> None:
286286
"""Verifies public key, signature and data match. Expects public key to be on top of stack, followed
287287
by signature and data. If they match, put data on stack; otherwise, fail.
288288
@@ -316,7 +316,7 @@ def op_checkdatasig(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
316316
raise OracleChecksigFailed from e
317317

318318

319-
def op_data_strequal(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
319+
def op_data_strequal(stack: Stack) -> None:
320320
"""Equivalent to an OP_GET_DATA_STR followed by an OP_EQUALVERIFY.
321321
322322
Consumes three parameters from stack: <data> <k> <value>. Gets the kth value
@@ -347,7 +347,7 @@ def op_data_strequal(stack: Stack, log: list[str], extras: ScriptExtras) -> None
347347
stack.append(data)
348348

349349

350-
def op_data_greaterthan(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
350+
def op_data_greaterthan(stack: Stack) -> None:
351351
"""Equivalent to an OP_GET_DATA_INT followed by an OP_GREATERTHAN.
352352
353353
Consumes three parameters from stack: <data> <k> <n>. Gets the kth value
@@ -383,7 +383,7 @@ def op_data_greaterthan(stack: Stack, log: list[str], extras: ScriptExtras) -> N
383383
stack.append(data)
384384

385385

386-
def op_data_match_interval(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
386+
def op_data_match_interval(stack: Stack) -> None:
387387
"""Equivalent to an OP_GET_DATA_INT followed by an OP_MATCH_INTERVAL.
388388
389389
:param stack: the stack used when evaluating the script
@@ -435,7 +435,7 @@ def op_data_match_interval(stack: Stack, log: list[str], extras: ScriptExtras) -
435435
stack.append(last_pubkey)
436436

437437

438-
def op_data_match_value(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
438+
def op_data_match_value(stack: Stack) -> None:
439439
"""Equivalent to an OP_GET_DATA_STR followed by an OP_MATCH_VALUE.
440440
441441
:param stack: the stack used when evaluating the script
@@ -484,7 +484,7 @@ def op_data_match_value(stack: Stack, log: list[str], extras: ScriptExtras) -> N
484484
stack.append(winner_pubkey)
485485

486486

487-
def op_find_p2pkh(stack: Stack, log: list[str], extras: ScriptExtras) -> None:
487+
def op_find_p2pkh(stack: Stack, extras: ScriptExtras) -> None:
488488
"""Checks whether the current transaction has an output with a P2PKH script with
489489
the given public key hash and the same amount as the input.
490490
@@ -604,7 +604,7 @@ def op_checkmultisig(stack: Stack, log: list[str], extras: ScriptExtras) -> None
604604
stack.append(1)
605605

606606

607-
def op_integer(opcode: int, stack: Stack, log: list[str], extras: ScriptExtras) -> None:
607+
def op_integer(opcode: int, stack: Stack) -> None:
608608
""" Appends an integer to the stack
609609
We get the opcode comparing to all integers opcodes
610610
@@ -624,17 +624,34 @@ def op_integer(opcode: int, stack: Stack, log: list[str], extras: ScriptExtras)
624624
raise ScriptError(e) from e
625625

626626

627-
MAP_OPCODE_TO_FN: dict[int, Callable[[Stack, list[str], ScriptExtras], None]] = {
628-
Opcode.OP_DUP: op_dup,
629-
Opcode.OP_EQUAL: op_equal,
630-
Opcode.OP_EQUALVERIFY: op_equalverify,
631-
Opcode.OP_CHECKSIG: op_checksig,
632-
Opcode.OP_HASH160: op_hash160,
633-
Opcode.OP_GREATERTHAN_TIMESTAMP: op_greaterthan_timestamp,
634-
Opcode.OP_CHECKMULTISIG: op_checkmultisig,
635-
Opcode.OP_DATA_STREQUAL: op_data_strequal,
636-
Opcode.OP_DATA_GREATERTHAN: op_data_greaterthan,
637-
Opcode.OP_DATA_MATCH_VALUE: op_data_match_value,
638-
Opcode.OP_CHECKDATASIG: op_checkdatasig,
639-
Opcode.OP_FIND_P2PKH: op_find_p2pkh,
640-
}
627+
def execute_op_code(
628+
opcode: Opcode,
629+
stack: Stack,
630+
log: list[str],
631+
extras: ScriptExtras,
632+
context: ScriptContext,
633+
) -> None:
634+
"""
635+
Execute a function opcode.
636+
637+
Args:
638+
opcode: the opcode to be executed.
639+
stack: the stack to be manipulated.
640+
log: the log list to be appended.
641+
extras: auxiliary opcode arguments.
642+
context: the script context to be manipulated.
643+
"""
644+
match opcode:
645+
case Opcode.OP_DUP: op_dup(stack)
646+
case Opcode.OP_EQUAL: op_equal(stack, log)
647+
case Opcode.OP_EQUALVERIFY: op_equalverify(stack, log)
648+
case Opcode.OP_CHECKSIG: op_checksig(stack, log, extras)
649+
case Opcode.OP_HASH160: op_hash160(stack)
650+
case Opcode.OP_GREATERTHAN_TIMESTAMP: op_greaterthan_timestamp(stack, extras)
651+
case Opcode.OP_CHECKMULTISIG: op_checkmultisig(stack, log, extras)
652+
case Opcode.OP_DATA_STREQUAL: op_data_strequal(stack)
653+
case Opcode.OP_DATA_GREATERTHAN: op_data_greaterthan(stack)
654+
case Opcode.OP_DATA_MATCH_VALUE: op_data_match_value(stack)
655+
case Opcode.OP_CHECKDATASIG: op_checkdatasig(stack)
656+
case Opcode.OP_FIND_P2PKH: op_find_p2pkh(stack, extras)
657+
case _: raise ScriptError(f'unknown opcode: {opcode}')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
class ScriptContext:
17+
"""A context to be manipulated during script execution. A separate instance must be used for each script."""
18+
__slots__ = ()

0 commit comments

Comments
 (0)