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 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