runar 0.3.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.
- runar/__init__.py +113 -0
- runar/base.py +42 -0
- runar/builtins.py +454 -0
- runar/compile_check.py +46 -0
- runar/decorators.py +10 -0
- runar/ec.py +149 -0
- runar/ecdsa.py +296 -0
- runar/rabin_sig.py +50 -0
- runar/sdk/__init__.py +35 -0
- runar/sdk/anf_interpreter.py +553 -0
- runar/sdk/calling.py +190 -0
- runar/sdk/codegen.py +539 -0
- runar/sdk/contract.py +1079 -0
- runar/sdk/deployment.py +197 -0
- runar/sdk/local_signer.py +117 -0
- runar/sdk/oppushtx.py +274 -0
- runar/sdk/provider.py +145 -0
- runar/sdk/rpc_provider.py +140 -0
- runar/sdk/signer.py +89 -0
- runar/sdk/state.py +220 -0
- runar/sdk/types.py +217 -0
- runar/slhdsa_impl.py +88 -0
- runar/test_keys.py +57 -0
- runar/types.py +30 -0
- runar/wots.py +136 -0
- runar-0.3.0.dist-info/METADATA +9 -0
- runar-0.3.0.dist-info/RECORD +29 -0
- runar-0.3.0.dist-info/WHEEL +5 -0
- runar-0.3.0.dist-info/top_level.txt +1 -0
runar/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Runar - TypeScript-to-Bitcoin Script Compiler (Python runtime).
|
|
2
|
+
|
|
3
|
+
Provides types, real crypto, real hashes, EC operations, and base classes
|
|
4
|
+
for writing and testing Runar smart contracts in Python.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from runar.types import (
|
|
8
|
+
Bigint, Int, ByteString, PubKey, Sig, Addr, Sha256, Ripemd160,
|
|
9
|
+
SigHashPreimage, RabinSig, RabinPubKey, Point, Readonly,
|
|
10
|
+
)
|
|
11
|
+
from runar.builtins import (
|
|
12
|
+
assert_,
|
|
13
|
+
check_sig, check_multi_sig, check_preimage,
|
|
14
|
+
hash160, hash256, sha256, ripemd160,
|
|
15
|
+
extract_locktime, extract_output_hash, extract_amount,
|
|
16
|
+
extract_version, extract_sequence,
|
|
17
|
+
extract_hash_prevouts, extract_outpoint,
|
|
18
|
+
num2bin, bin2num, cat, substr, reverse_bytes, len_,
|
|
19
|
+
verify_rabin_sig,
|
|
20
|
+
safediv, safemod, clamp, sign, pow_, mul_div, percent_of,
|
|
21
|
+
sqrt, gcd, divmod_, log2, bool_cast,
|
|
22
|
+
mock_sig, mock_pub_key, mock_preimage,
|
|
23
|
+
verify_wots,
|
|
24
|
+
verify_slh_dsa_sha2_128s, verify_slh_dsa_sha2_128f,
|
|
25
|
+
verify_slh_dsa_sha2_192s, verify_slh_dsa_sha2_192f,
|
|
26
|
+
verify_slh_dsa_sha2_256s, verify_slh_dsa_sha2_256f,
|
|
27
|
+
blake3_compress, blake3_hash,
|
|
28
|
+
sha256_compress, sha256_finalize,
|
|
29
|
+
)
|
|
30
|
+
from runar.ecdsa import (
|
|
31
|
+
sign_test_message, pub_key_from_priv_key,
|
|
32
|
+
ecdsa_verify, ecdsa_sign,
|
|
33
|
+
TEST_MESSAGE, TEST_MESSAGE_DIGEST,
|
|
34
|
+
)
|
|
35
|
+
from runar.test_keys import (
|
|
36
|
+
TestKeyPair, TEST_KEYS,
|
|
37
|
+
ALICE, BOB, CHARLIE, DAVE, EVE,
|
|
38
|
+
FRANK, GRACE, HEIDI, IVAN, JUDY,
|
|
39
|
+
)
|
|
40
|
+
from runar.wots import wots_keygen, wots_sign, WOTSKeyPair
|
|
41
|
+
from runar.slhdsa_impl import slh_keygen, slh_verify, SLHKeyPair
|
|
42
|
+
from runar.ec import (
|
|
43
|
+
ec_add, ec_mul, ec_mul_gen, ec_negate, ec_on_curve,
|
|
44
|
+
ec_mod_reduce, ec_encode_compressed, ec_make_point,
|
|
45
|
+
ec_point_x, ec_point_y,
|
|
46
|
+
EC_P, EC_N, EC_G,
|
|
47
|
+
)
|
|
48
|
+
from runar.base import SmartContract, StatefulSmartContract
|
|
49
|
+
from runar.decorators import public
|
|
50
|
+
from runar.compile_check import compile_check
|
|
51
|
+
|
|
52
|
+
import builtins as _builtins
|
|
53
|
+
|
|
54
|
+
# Re-export Python builtins that Runar contracts use directly
|
|
55
|
+
abs = _builtins.abs
|
|
56
|
+
min = _builtins.min
|
|
57
|
+
max = _builtins.max
|
|
58
|
+
|
|
59
|
+
def within(x: int, lo: int, hi: int) -> bool:
|
|
60
|
+
return lo <= x < hi
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
# Types
|
|
64
|
+
'Bigint', 'Int', 'ByteString', 'PubKey', 'Sig', 'Addr', 'Sha256',
|
|
65
|
+
'Ripemd160', 'SigHashPreimage', 'RabinSig', 'RabinPubKey', 'Point',
|
|
66
|
+
'Readonly',
|
|
67
|
+
# Decorators
|
|
68
|
+
'public',
|
|
69
|
+
# Base classes
|
|
70
|
+
'SmartContract', 'StatefulSmartContract',
|
|
71
|
+
# Assertions
|
|
72
|
+
'assert_',
|
|
73
|
+
# Crypto
|
|
74
|
+
'check_sig', 'check_multi_sig', 'check_preimage',
|
|
75
|
+
'hash160', 'hash256', 'sha256', 'ripemd160',
|
|
76
|
+
'verify_rabin_sig',
|
|
77
|
+
'verify_wots',
|
|
78
|
+
'verify_slh_dsa_sha2_128s', 'verify_slh_dsa_sha2_128f',
|
|
79
|
+
'verify_slh_dsa_sha2_192s', 'verify_slh_dsa_sha2_192f',
|
|
80
|
+
'verify_slh_dsa_sha2_256s', 'verify_slh_dsa_sha2_256f',
|
|
81
|
+
# ECDSA
|
|
82
|
+
'ecdsa_verify', 'ecdsa_sign', 'sign_test_message', 'pub_key_from_priv_key',
|
|
83
|
+
'TEST_MESSAGE', 'TEST_MESSAGE_DIGEST',
|
|
84
|
+
# Test keys
|
|
85
|
+
'TestKeyPair', 'TEST_KEYS',
|
|
86
|
+
'ALICE', 'BOB', 'CHARLIE', 'DAVE', 'EVE',
|
|
87
|
+
'FRANK', 'GRACE', 'HEIDI', 'IVAN', 'JUDY',
|
|
88
|
+
# Preimage extraction
|
|
89
|
+
'extract_locktime', 'extract_output_hash', 'extract_amount',
|
|
90
|
+
'extract_version', 'extract_sequence',
|
|
91
|
+
'extract_hash_prevouts', 'extract_outpoint',
|
|
92
|
+
# Binary utilities
|
|
93
|
+
'num2bin', 'bin2num', 'cat', 'substr', 'reverse_bytes', 'len_',
|
|
94
|
+
# Math
|
|
95
|
+
'within', 'safediv', 'safemod', 'clamp', 'sign', 'pow_',
|
|
96
|
+
'mul_div', 'percent_of', 'sqrt', 'gcd', 'divmod_', 'log2', 'bool_cast',
|
|
97
|
+
# EC
|
|
98
|
+
'ec_add', 'ec_mul', 'ec_mul_gen', 'ec_negate', 'ec_on_curve',
|
|
99
|
+
'ec_mod_reduce', 'ec_encode_compressed', 'ec_make_point',
|
|
100
|
+
'ec_point_x', 'ec_point_y', 'EC_P', 'EC_N', 'EC_G',
|
|
101
|
+
# BLAKE3
|
|
102
|
+
'blake3_compress', 'blake3_hash',
|
|
103
|
+
# SHA-256 compression
|
|
104
|
+
'sha256_compress', 'sha256_finalize',
|
|
105
|
+
# Test helpers
|
|
106
|
+
'mock_sig', 'mock_pub_key', 'mock_preimage',
|
|
107
|
+
# WOTS+ keygen/sign
|
|
108
|
+
'wots_keygen', 'wots_sign', 'WOTSKeyPair',
|
|
109
|
+
# SLH-DSA keygen/verify
|
|
110
|
+
'slh_keygen', 'slh_verify', 'SLHKeyPair',
|
|
111
|
+
# Compile check
|
|
112
|
+
'compile_check',
|
|
113
|
+
]
|
runar/base.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Runar base contract classes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SmartContract:
|
|
7
|
+
"""Base class for stateless Runar smart contracts.
|
|
8
|
+
|
|
9
|
+
All properties are readonly. The contract logic is pure — no state
|
|
10
|
+
is carried between spending transactions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args: Any) -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StatefulSmartContract(SmartContract):
|
|
18
|
+
"""Base class for stateful Runar smart contracts.
|
|
19
|
+
|
|
20
|
+
Mutable properties are carried in the UTXO state. The compiler
|
|
21
|
+
auto-injects checkPreimage at method entry and state continuation
|
|
22
|
+
at exit.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
tx_preimage: bytes = b''
|
|
26
|
+
_outputs: list
|
|
27
|
+
|
|
28
|
+
def __init__(self, *args: Any) -> None:
|
|
29
|
+
super().__init__(*args)
|
|
30
|
+
self._outputs = []
|
|
31
|
+
|
|
32
|
+
def add_output(self, satoshis: int, *state_values: Any) -> None:
|
|
33
|
+
"""Add an output with the given satoshis and state values."""
|
|
34
|
+
self._outputs.append({"satoshis": satoshis, "values": list(state_values)})
|
|
35
|
+
|
|
36
|
+
def get_state_script(self) -> bytes:
|
|
37
|
+
"""Get the state script for the current contract state."""
|
|
38
|
+
return b''
|
|
39
|
+
|
|
40
|
+
def reset_outputs(self) -> None:
|
|
41
|
+
"""Reset the outputs list."""
|
|
42
|
+
self._outputs = []
|
runar/builtins.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""Runar built-in functions.
|
|
2
|
+
|
|
3
|
+
Real crypto verification for ECDSA, Rabin, WOTS+, and SLH-DSA.
|
|
4
|
+
Real hash functions use Python's hashlib (stdlib, no dependencies).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import math
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
from runar.ecdsa import ecdsa_verify, TEST_MESSAGE_DIGEST
|
|
12
|
+
from runar.rabin_sig import rabin_verify as _rabin_verify_real
|
|
13
|
+
from runar.wots import wots_verify as _wots_verify_real
|
|
14
|
+
from runar.slhdsa_impl import slh_verify as _slh_verify
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# -- Assertion ---------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def assert_(condition: bool) -> None:
|
|
20
|
+
"""Runar assertion. Raises AssertionError if condition is False."""
|
|
21
|
+
if not condition:
|
|
22
|
+
raise AssertionError("runar: assertion failed")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# -- Real ECDSA Verification ------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def check_sig(sig, pk) -> bool:
|
|
28
|
+
"""Verify an ECDSA signature over the fixed TEST_MESSAGE.
|
|
29
|
+
|
|
30
|
+
Uses real secp256k1 ECDSA verification against SHA256("runar-test-message-v1").
|
|
31
|
+
Accepts both raw bytes and hex-encoded strings (Runar ByteString convention).
|
|
32
|
+
"""
|
|
33
|
+
return ecdsa_verify(_as_bytes(sig), _as_bytes(pk), TEST_MESSAGE_DIGEST)
|
|
34
|
+
|
|
35
|
+
def check_multi_sig(sigs: list, pks: list) -> bool:
|
|
36
|
+
"""Verify multiple ECDSA signatures (Bitcoin-style multi-sig).
|
|
37
|
+
|
|
38
|
+
Each signature is verified against the public keys in order.
|
|
39
|
+
Accepts both raw bytes and hex-encoded strings.
|
|
40
|
+
"""
|
|
41
|
+
if len(sigs) > len(pks):
|
|
42
|
+
return False
|
|
43
|
+
pk_idx = 0
|
|
44
|
+
for s in sigs:
|
|
45
|
+
matched = False
|
|
46
|
+
while pk_idx < len(pks):
|
|
47
|
+
if check_sig(s, pks[pk_idx]):
|
|
48
|
+
pk_idx += 1
|
|
49
|
+
matched = True
|
|
50
|
+
break
|
|
51
|
+
pk_idx += 1
|
|
52
|
+
if not matched:
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def check_preimage(preimage: bytes) -> bool:
|
|
57
|
+
"""Mock preimage check — always returns True for business logic testing."""
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -- Real Rabin Verification ------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def verify_rabin_sig(msg: bytes, sig: bytes, padding: bytes, pk: bytes) -> bool:
|
|
64
|
+
"""Verify a Rabin signature.
|
|
65
|
+
|
|
66
|
+
All parameters are bytes. sig and pk are interpreted as unsigned
|
|
67
|
+
little-endian integers. Equation: (sig^2 + padding) mod n == SHA256(msg) mod n.
|
|
68
|
+
"""
|
|
69
|
+
return _rabin_verify_real(msg, sig, padding, pk)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# -- Real WOTS+ Verification ------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def verify_wots(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
75
|
+
return _wots_verify_real(msg, sig, pubkey)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# -- Real SLH-DSA Verification (falls back to mock if slhdsa not installed) -
|
|
79
|
+
|
|
80
|
+
def verify_slh_dsa_sha2_128s(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
81
|
+
return _slh_verify(msg, sig, pubkey, 'sha2_128s')
|
|
82
|
+
|
|
83
|
+
def verify_slh_dsa_sha2_128f(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
84
|
+
return _slh_verify(msg, sig, pubkey, 'sha2_128f')
|
|
85
|
+
|
|
86
|
+
def verify_slh_dsa_sha2_192s(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
87
|
+
return _slh_verify(msg, sig, pubkey, 'sha2_192s')
|
|
88
|
+
|
|
89
|
+
def verify_slh_dsa_sha2_192f(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
90
|
+
return _slh_verify(msg, sig, pubkey, 'sha2_192f')
|
|
91
|
+
|
|
92
|
+
def verify_slh_dsa_sha2_256s(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
93
|
+
return _slh_verify(msg, sig, pubkey, 'sha2_256s')
|
|
94
|
+
|
|
95
|
+
def verify_slh_dsa_sha2_256f(msg: bytes, sig: bytes, pubkey: bytes) -> bool:
|
|
96
|
+
return _slh_verify(msg, sig, pubkey, 'sha2_256f')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# -- Byte coercion -----------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _as_bytes(x) -> bytes:
|
|
102
|
+
"""Accept both raw bytes/bytearray and hex-encoded strings.
|
|
103
|
+
|
|
104
|
+
In Rúnar, ByteString literals are hex strings (e.g. "1976a914" = 4 bytes).
|
|
105
|
+
This mirrors the TypeScript interpreter which hex-decodes string literals.
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(x, (bytes, bytearray)):
|
|
108
|
+
return bytes(x)
|
|
109
|
+
if isinstance(x, str):
|
|
110
|
+
return bytes.fromhex(x)
|
|
111
|
+
raise TypeError(f"Expected bytes or hex-encoded string, got {type(x).__name__}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# -- Mock BLAKE3 Functions (compiler intrinsics — stubs return 32 zero bytes)
|
|
115
|
+
|
|
116
|
+
def blake3_compress(chaining_value, block) -> bytes:
|
|
117
|
+
"""Mock BLAKE3 single-block compression.
|
|
118
|
+
In compiled Bitcoin Script this expands to ~10,000 opcodes.
|
|
119
|
+
Returns 32 zero bytes for business-logic testing."""
|
|
120
|
+
return b'\x00' * 32
|
|
121
|
+
|
|
122
|
+
def blake3_hash(message) -> bytes:
|
|
123
|
+
"""Mock BLAKE3 hash for messages up to 64 bytes.
|
|
124
|
+
In compiled Bitcoin Script this uses the IV as the chaining value and
|
|
125
|
+
applies zero-padding before calling the compression function.
|
|
126
|
+
Returns 32 zero bytes for business-logic testing."""
|
|
127
|
+
return b'\x00' * 32
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# -- SHA-256 Compression (real implementation) --------------------------------
|
|
131
|
+
|
|
132
|
+
_SHA256_K = (
|
|
133
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
134
|
+
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
135
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
136
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
137
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
|
138
|
+
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
139
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
140
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
141
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
142
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
143
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
144
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
145
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
|
146
|
+
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
147
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
148
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _rotr(x: int, n: int) -> int:
|
|
153
|
+
"""Right-rotate a 32-bit unsigned integer by n bits."""
|
|
154
|
+
return ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def sha256_compress(state: bytes, block: bytes) -> bytes:
|
|
158
|
+
"""SHA-256 single-block compression function (FIPS 180-4 Section 6.2.2).
|
|
159
|
+
|
|
160
|
+
Performs one round of SHA-256 block compression: message schedule
|
|
161
|
+
expansion (W[0..63]), then 64 compression rounds with Sigma/Ch/Maj
|
|
162
|
+
functions and the K constants, followed by addition back to the
|
|
163
|
+
initial state.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
state: 32-byte intermediate hash state (8 big-endian uint32 words).
|
|
167
|
+
Use the SHA-256 IV for the first block.
|
|
168
|
+
block: 64-byte message block (512 bits).
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
32-byte updated hash state (big-endian).
|
|
172
|
+
"""
|
|
173
|
+
assert len(state) == 32, f"state must be 32 bytes, got {len(state)}"
|
|
174
|
+
assert len(block) == 64, f"block must be 64 bytes, got {len(block)}"
|
|
175
|
+
|
|
176
|
+
# Parse state as 8 big-endian uint32
|
|
177
|
+
H = list(struct.unpack('>8I', state))
|
|
178
|
+
|
|
179
|
+
# Parse block as 16 big-endian uint32 and expand to 64 words
|
|
180
|
+
W = list(struct.unpack('>16I', block))
|
|
181
|
+
for t in range(16, 64):
|
|
182
|
+
s0 = (_rotr(W[t - 15], 7) ^ _rotr(W[t - 15], 18) ^ (W[t - 15] >> 3)) & 0xFFFFFFFF
|
|
183
|
+
s1 = (_rotr(W[t - 2], 17) ^ _rotr(W[t - 2], 19) ^ (W[t - 2] >> 10)) & 0xFFFFFFFF
|
|
184
|
+
W.append((s1 + W[t - 7] + s0 + W[t - 16]) & 0xFFFFFFFF)
|
|
185
|
+
|
|
186
|
+
# Initialize working variables
|
|
187
|
+
a, b, c, d, e, f, g, h = H
|
|
188
|
+
|
|
189
|
+
# 64 compression rounds
|
|
190
|
+
for t in range(64):
|
|
191
|
+
S1 = (_rotr(e, 6) ^ _rotr(e, 11) ^ _rotr(e, 25)) & 0xFFFFFFFF
|
|
192
|
+
ch = ((e & f) ^ (~e & g)) & 0xFFFFFFFF
|
|
193
|
+
temp1 = (h + S1 + ch + _SHA256_K[t] + W[t]) & 0xFFFFFFFF
|
|
194
|
+
S0 = (_rotr(a, 2) ^ _rotr(a, 13) ^ _rotr(a, 22)) & 0xFFFFFFFF
|
|
195
|
+
maj = ((a & b) ^ (a & c) ^ (b & c)) & 0xFFFFFFFF
|
|
196
|
+
temp2 = (S0 + maj) & 0xFFFFFFFF
|
|
197
|
+
|
|
198
|
+
h = g
|
|
199
|
+
g = f
|
|
200
|
+
f = e
|
|
201
|
+
e = (d + temp1) & 0xFFFFFFFF
|
|
202
|
+
d = c
|
|
203
|
+
c = b
|
|
204
|
+
b = a
|
|
205
|
+
a = (temp1 + temp2) & 0xFFFFFFFF
|
|
206
|
+
|
|
207
|
+
# Add compressed chunk to hash state
|
|
208
|
+
result = tuple((H[i] + v) & 0xFFFFFFFF for i, v in enumerate((a, b, c, d, e, f, g, h)))
|
|
209
|
+
return struct.pack('>8I', *result)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def sha256_finalize(state: bytes, remaining: bytes, msg_bit_len: int) -> bytes:
|
|
213
|
+
"""SHA-256 finalization with FIPS 180-4 padding.
|
|
214
|
+
|
|
215
|
+
Applies SHA-256 padding (append 0x80 byte, zero-pad, append 8-byte
|
|
216
|
+
big-endian bit length) and runs the final 1-2 compression rounds:
|
|
217
|
+
|
|
218
|
+
- Single-block path (remaining <= 55 bytes): pads to one 64-byte
|
|
219
|
+
block and compresses once.
|
|
220
|
+
- Two-block path (56-119 bytes): pads to two 64-byte blocks and
|
|
221
|
+
compresses twice.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
state: 32-byte intermediate hash state. Use SHA-256 IV when
|
|
225
|
+
finalizing a message that fits in a single compress+finalize
|
|
226
|
+
call, or the output of a prior sha256_compress for multi-block.
|
|
227
|
+
remaining: Unprocessed trailing message bytes (0-119 bytes).
|
|
228
|
+
msg_bit_len: Total message length in bits across all blocks
|
|
229
|
+
(used in the 64-bit length suffix of SHA-256 padding).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Final 32-byte SHA-256 digest.
|
|
233
|
+
"""
|
|
234
|
+
# Append the 0x80 byte
|
|
235
|
+
padded = remaining + b'\x80'
|
|
236
|
+
|
|
237
|
+
if len(padded) + 8 <= 64:
|
|
238
|
+
# Fits in one block: pad to 56 bytes, then append 8-byte BE bit length
|
|
239
|
+
padded = padded.ljust(56, b'\x00')
|
|
240
|
+
padded += struct.pack('>Q', msg_bit_len)
|
|
241
|
+
return sha256_compress(state, padded)
|
|
242
|
+
else:
|
|
243
|
+
# Need two blocks: pad to 120 bytes, then append 8-byte BE bit length
|
|
244
|
+
padded = padded.ljust(120, b'\x00')
|
|
245
|
+
padded += struct.pack('>Q', msg_bit_len)
|
|
246
|
+
state = sha256_compress(state, padded[:64])
|
|
247
|
+
return sha256_compress(state, padded[64:])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# -- Real Hash Functions -----------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def hash160(data) -> bytes:
|
|
253
|
+
"""RIPEMD160(SHA256(data))"""
|
|
254
|
+
data = _as_bytes(data)
|
|
255
|
+
return hashlib.new('ripemd160', hashlib.sha256(data).digest()).digest()
|
|
256
|
+
|
|
257
|
+
def hash256(data) -> bytes:
|
|
258
|
+
"""SHA256(SHA256(data))"""
|
|
259
|
+
data = _as_bytes(data)
|
|
260
|
+
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
|
|
261
|
+
|
|
262
|
+
def sha256(data) -> bytes:
|
|
263
|
+
data = _as_bytes(data)
|
|
264
|
+
return hashlib.sha256(data).digest()
|
|
265
|
+
|
|
266
|
+
def ripemd160(data) -> bytes:
|
|
267
|
+
data = _as_bytes(data)
|
|
268
|
+
return hashlib.new('ripemd160', data).digest()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# -- Mock Preimage Extraction ------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def extract_locktime(preimage: bytes) -> int:
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
def extract_output_hash(preimage) -> bytes:
|
|
277
|
+
"""Returns the first 32 bytes of the preimage in test mode.
|
|
278
|
+
Tests set tx_preimage = hash256(expected_output_bytes) so the assertion
|
|
279
|
+
hash256(outputs) == extract_output_hash(tx_preimage) passes.
|
|
280
|
+
Falls back to 32 zero bytes when the preimage is shorter than 32 bytes."""
|
|
281
|
+
preimage = _as_bytes(preimage)
|
|
282
|
+
if len(preimage) >= 32:
|
|
283
|
+
return preimage[:32]
|
|
284
|
+
return b'\x00' * 32
|
|
285
|
+
|
|
286
|
+
def extract_amount(preimage: bytes) -> int:
|
|
287
|
+
return 10000
|
|
288
|
+
|
|
289
|
+
def extract_version(preimage: bytes) -> int:
|
|
290
|
+
return 1
|
|
291
|
+
|
|
292
|
+
def extract_sequence(preimage: bytes) -> int:
|
|
293
|
+
return 0xFFFFFFFF
|
|
294
|
+
|
|
295
|
+
def extract_hash_prevouts(preimage: bytes) -> bytes:
|
|
296
|
+
"""Returns hash256(72 zero bytes) in test mode.
|
|
297
|
+
|
|
298
|
+
This is consistent with passing all_prevouts = 72 zero bytes in tests,
|
|
299
|
+
since extract_outpoint also returns 36 zero bytes.
|
|
300
|
+
"""
|
|
301
|
+
return hash256(b'\x00' * 72)
|
|
302
|
+
|
|
303
|
+
def extract_outpoint(preimage: bytes) -> bytes:
|
|
304
|
+
return b'\x00' * 36
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# -- Math Utilities ----------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def safediv(a: int, b: int) -> int:
|
|
310
|
+
if b == 0:
|
|
311
|
+
return 0
|
|
312
|
+
# Python integer division truncates toward negative infinity,
|
|
313
|
+
# but Bitcoin Script truncates toward zero. Match that behavior.
|
|
314
|
+
if (a < 0) != (b < 0) and a % b != 0:
|
|
315
|
+
return -(abs(a) // abs(b))
|
|
316
|
+
return a // b
|
|
317
|
+
|
|
318
|
+
def safemod(a: int, b: int) -> int:
|
|
319
|
+
if b == 0:
|
|
320
|
+
return 0
|
|
321
|
+
r = a % b
|
|
322
|
+
# Ensure sign matches dividend (Bitcoin Script behavior)
|
|
323
|
+
if r != 0 and (a < 0) != (r < 0):
|
|
324
|
+
r -= b
|
|
325
|
+
return r
|
|
326
|
+
|
|
327
|
+
def clamp(value: int, lo: int, hi: int) -> int:
|
|
328
|
+
if value < lo:
|
|
329
|
+
return lo
|
|
330
|
+
if value > hi:
|
|
331
|
+
return hi
|
|
332
|
+
return value
|
|
333
|
+
|
|
334
|
+
def sign(n: int) -> int:
|
|
335
|
+
if n > 0:
|
|
336
|
+
return 1
|
|
337
|
+
if n < 0:
|
|
338
|
+
return -1
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
def pow_(base: int, exp: int) -> int:
|
|
342
|
+
return base ** exp
|
|
343
|
+
|
|
344
|
+
def mul_div(a: int, b: int, c: int) -> int:
|
|
345
|
+
return (a * b) // c
|
|
346
|
+
|
|
347
|
+
def percent_of(amount: int, bps: int) -> int:
|
|
348
|
+
return (amount * bps) // 10000
|
|
349
|
+
|
|
350
|
+
def sqrt(n: int) -> int:
|
|
351
|
+
"""Integer square root using Newton's method."""
|
|
352
|
+
if n < 0:
|
|
353
|
+
raise ValueError("sqrt of negative number")
|
|
354
|
+
if n == 0:
|
|
355
|
+
return 0
|
|
356
|
+
x = n
|
|
357
|
+
y = (x + 1) // 2
|
|
358
|
+
while y < x:
|
|
359
|
+
x = y
|
|
360
|
+
y = (x + n // x) // 2
|
|
361
|
+
return x
|
|
362
|
+
|
|
363
|
+
def gcd(a: int, b: int) -> int:
|
|
364
|
+
a, b = abs(a), abs(b)
|
|
365
|
+
while b:
|
|
366
|
+
a, b = b, a % b
|
|
367
|
+
return a
|
|
368
|
+
|
|
369
|
+
def divmod_(a: int, b: int) -> int:
|
|
370
|
+
"""Returns quotient only (matching Runar's divmod which returns quotient)."""
|
|
371
|
+
return a // b
|
|
372
|
+
|
|
373
|
+
def log2(n: int) -> int:
|
|
374
|
+
if n <= 0:
|
|
375
|
+
return 0
|
|
376
|
+
return n.bit_length() - 1
|
|
377
|
+
|
|
378
|
+
def bool_cast(n: int) -> bool:
|
|
379
|
+
return n != 0
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# -- Binary Utilities --------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def num2bin(v: int, length: int) -> bytes:
|
|
385
|
+
"""Convert integer to little-endian sign-magnitude byte string."""
|
|
386
|
+
if v == 0:
|
|
387
|
+
return b'\x00' * length
|
|
388
|
+
negative = v < 0
|
|
389
|
+
val = abs(v)
|
|
390
|
+
result = []
|
|
391
|
+
while val > 0:
|
|
392
|
+
result.append(val & 0xFF)
|
|
393
|
+
val >>= 8
|
|
394
|
+
# Sign bit
|
|
395
|
+
if result[-1] & 0x80:
|
|
396
|
+
result.append(0x80 if negative else 0x00)
|
|
397
|
+
elif negative:
|
|
398
|
+
result[-1] |= 0x80
|
|
399
|
+
# Pad to requested length, keeping sign bit on the last byte
|
|
400
|
+
if len(result) < length:
|
|
401
|
+
sign_byte = result[-1] & 0x80
|
|
402
|
+
result[-1] &= 0x7F # clear sign from current last byte
|
|
403
|
+
while len(result) < length:
|
|
404
|
+
result.append(0)
|
|
405
|
+
result[-1] |= sign_byte # set sign on actual last byte
|
|
406
|
+
return bytes(result[:length])
|
|
407
|
+
|
|
408
|
+
def bin2num(data: bytes) -> int:
|
|
409
|
+
"""Convert a byte string (Bitcoin Script LE signed-magnitude) to an integer.
|
|
410
|
+
Inverse of num2bin."""
|
|
411
|
+
if len(data) == 0:
|
|
412
|
+
return 0
|
|
413
|
+
last = data[-1]
|
|
414
|
+
negative = (last & 0x80) != 0
|
|
415
|
+
result = last & 0x7F
|
|
416
|
+
for i in range(len(data) - 2, -1, -1):
|
|
417
|
+
result = (result << 8) | data[i]
|
|
418
|
+
return -result if negative else result
|
|
419
|
+
|
|
420
|
+
def cat(a, b) -> bytes:
|
|
421
|
+
return _as_bytes(a) + _as_bytes(b)
|
|
422
|
+
|
|
423
|
+
def substr(data, start: int, length: int) -> bytes:
|
|
424
|
+
return _as_bytes(data)[start:start + length]
|
|
425
|
+
|
|
426
|
+
def reverse_bytes(data) -> bytes:
|
|
427
|
+
return _as_bytes(data)[::-1]
|
|
428
|
+
|
|
429
|
+
def len_(data) -> int:
|
|
430
|
+
return len(_as_bytes(data))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# -- Test Helpers ------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
def mock_sig() -> bytes:
|
|
436
|
+
"""Return ALICE's real ECDSA test signature (DER-encoded).
|
|
437
|
+
|
|
438
|
+
This is a valid signature over TEST_MESSAGE that will pass check_sig()
|
|
439
|
+
verification when paired with mock_pub_key().
|
|
440
|
+
"""
|
|
441
|
+
from runar.test_keys import ALICE
|
|
442
|
+
return ALICE.test_sig
|
|
443
|
+
|
|
444
|
+
def mock_pub_key() -> bytes:
|
|
445
|
+
"""Return ALICE's real compressed public key (33 bytes).
|
|
446
|
+
|
|
447
|
+
This is a valid secp256k1 public key that will pass check_sig()
|
|
448
|
+
verification when paired with mock_sig().
|
|
449
|
+
"""
|
|
450
|
+
from runar.test_keys import ALICE
|
|
451
|
+
return ALICE.pub_key
|
|
452
|
+
|
|
453
|
+
def mock_preimage() -> bytes:
|
|
454
|
+
return b'\x00' * 181
|
runar/compile_check.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Runar compile_check — validates Python contracts through the Runar frontend.
|
|
2
|
+
|
|
3
|
+
This is a placeholder that will use the Python compiler once it's implemented.
|
|
4
|
+
For now, it validates that the source file exists and has basic Python contract
|
|
5
|
+
structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compile_check(source_or_path: str, file_name: str | None = None) -> None:
|
|
12
|
+
"""Run Runar frontend (parse -> validate -> typecheck) on a contract source.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
source_or_path: Either a file path to a .runar.py file, or the source code string.
|
|
16
|
+
file_name: Optional file name for error messages when passing source code directly.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
RuntimeError: If the contract fails any frontend check.
|
|
20
|
+
"""
|
|
21
|
+
if '\n' not in source_or_path and os.path.isfile(source_or_path):
|
|
22
|
+
with open(source_or_path) as f:
|
|
23
|
+
source = f.read()
|
|
24
|
+
file_name = file_name or source_or_path
|
|
25
|
+
else:
|
|
26
|
+
source = source_or_path
|
|
27
|
+
file_name = file_name or 'contract.runar.py'
|
|
28
|
+
|
|
29
|
+
# Basic structural validation
|
|
30
|
+
if 'class ' not in source:
|
|
31
|
+
raise RuntimeError(f"No class declaration found in {file_name}")
|
|
32
|
+
|
|
33
|
+
if 'SmartContract' not in source and 'StatefulSmartContract' not in source:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"Contract class must extend SmartContract or StatefulSmartContract in {file_name}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# When the Python compiler is implemented, this will invoke:
|
|
39
|
+
# from runar_compiler.frontend.parser_dispatch import parse_source
|
|
40
|
+
# from runar_compiler.frontend.validator import validate
|
|
41
|
+
# from runar_compiler.frontend.typecheck import typecheck
|
|
42
|
+
# result = parse_source(source, file_name)
|
|
43
|
+
# if result.errors:
|
|
44
|
+
# raise RuntimeError(f"Parse errors: {'; '.join(result.errors)}")
|
|
45
|
+
# validate(result.contract)
|
|
46
|
+
# typecheck(result.contract)
|
runar/decorators.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Runar decorators for contract methods."""
|
|
2
|
+
|
|
3
|
+
from typing import TypeVar, Callable
|
|
4
|
+
|
|
5
|
+
F = TypeVar('F', bound=Callable)
|
|
6
|
+
|
|
7
|
+
def public(func: F) -> F:
|
|
8
|
+
"""Marks a method as a public spending entry point."""
|
|
9
|
+
func._runar_public = True # type: ignore
|
|
10
|
+
return func
|