zexus 1.7.2 → 1.8.1
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.
- package/README.md +57 -6
- package/package.json +2 -1
- package/rust_core/Cargo.lock +603 -0
- package/rust_core/Cargo.toml +26 -0
- package/rust_core/README.md +15 -0
- package/rust_core/pyproject.toml +25 -0
- package/rust_core/src/binary_bytecode.rs +543 -0
- package/rust_core/src/contract_vm.rs +643 -0
- package/rust_core/src/executor.rs +847 -0
- package/rust_core/src/hasher.rs +90 -0
- package/rust_core/src/lib.rs +71 -0
- package/rust_core/src/merkle.rs +128 -0
- package/rust_core/src/rust_vm.rs +2313 -0
- package/rust_core/src/signature.rs +79 -0
- package/rust_core/src/state_adapter.rs +281 -0
- package/rust_core/src/validator.rs +116 -0
- package/scripts/postinstall.js +34 -2
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/blockchain/accelerator.py +27 -0
- package/src/zexus/blockchain/contract_vm.py +409 -3
- package/src/zexus/blockchain/rust_bridge.py +64 -0
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +150 -52
- package/src/zexus/evaluator/core.py +151 -809
- package/src/zexus/evaluator/expressions.py +27 -22
- package/src/zexus/evaluator/functions.py +171 -126
- package/src/zexus/evaluator/statements.py +55 -112
- package/src/zexus/module_cache.py +20 -9
- package/src/zexus/object.py +330 -38
- package/src/zexus/parser/parser.py +69 -14
- package/src/zexus/parser/strategy_context.py +228 -5
- package/src/zexus/parser/strategy_structural.py +2 -2
- package/src/zexus/persistence.py +46 -17
- package/src/zexus/security.py +140 -234
- package/src/zexus/type_checker.py +44 -5
- package/src/zexus/vm/binary_bytecode.py +7 -3
- package/src/zexus/vm/bytecode.py +6 -0
- package/src/zexus/vm/cache.py +24 -46
- package/src/zexus/vm/compiler.py +80 -20
- package/src/zexus/vm/fastops.c +1093 -2975
- package/src/zexus/vm/gas_metering.py +2 -2
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +527 -67
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +79 -12
- package/src/zexus.egg-info/SOURCES.txt +23 -1
- package/src/zexus.egg-info/requires.txt +26 -0
- package/src/zexus.egg-info/entry_points.txt +0 -4
package/src/zexus/security.py
CHANGED
|
@@ -14,8 +14,11 @@ import sqlite3
|
|
|
14
14
|
import time
|
|
15
15
|
import hashlib
|
|
16
16
|
import threading
|
|
17
|
+
import logging
|
|
17
18
|
from zexus.config import config as zexus_config
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
19
22
|
# Try importing advanced database drivers
|
|
20
23
|
try:
|
|
21
24
|
import plyvel # For LevelDB
|
|
@@ -31,7 +34,12 @@ except ImportError:
|
|
|
31
34
|
|
|
32
35
|
from zexus.object import (
|
|
33
36
|
Environment, Map, String, Integer, Float, Boolean as BooleanObj,
|
|
34
|
-
Builtin, List, Null, EvaluationError as ObjectEvaluationError, NULL
|
|
37
|
+
Builtin, List, Null, EvaluationError as ObjectEvaluationError, NULL,
|
|
38
|
+
_sanitize_identifier,
|
|
39
|
+
EntityDefinition,
|
|
40
|
+
EntityInstance,
|
|
41
|
+
_get_cached_evaluator,
|
|
42
|
+
_clear_cached_evaluator,
|
|
35
43
|
)
|
|
36
44
|
|
|
37
45
|
try:
|
|
@@ -39,13 +47,6 @@ try:
|
|
|
39
47
|
except ImportError: # Fallback if optional type missing
|
|
40
48
|
ContractReference = None
|
|
41
49
|
|
|
42
|
-
# =============================================
|
|
43
|
-
# Shared Evaluator Cache for Performance
|
|
44
|
-
# =============================================
|
|
45
|
-
# Creating a new Evaluator() for every contract method call is expensive
|
|
46
|
-
# This cache provides thread-local evaluators that can be reused
|
|
47
|
-
|
|
48
|
-
_evaluator_cache = threading.local()
|
|
49
50
|
_vm_action_context = threading.local()
|
|
50
51
|
|
|
51
52
|
def _get_vm_action_context() -> bool:
|
|
@@ -54,33 +55,36 @@ def _get_vm_action_context() -> bool:
|
|
|
54
55
|
def _set_vm_action_context(flag: bool) -> None:
|
|
55
56
|
_vm_action_context.disable_vm = bool(flag)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return _evaluator_cache.evaluator
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Re-export entity classes for backwards compatibility
|
|
60
|
+
"EntityDefinition",
|
|
61
|
+
"EntityInstance",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Storage directory roots (created lazily)
|
|
65
|
+
STORAGE_DIR = "chain_data"
|
|
66
|
+
AUDIT_DIR = os.path.join(STORAGE_DIR, "audit_logs")
|
|
67
67
|
|
|
68
|
-
def _clear_evaluator_cache():
|
|
69
|
-
"""Clear the evaluator cache (useful for testing)."""
|
|
70
|
-
if hasattr(_evaluator_cache, 'evaluator'):
|
|
71
|
-
del _evaluator_cache.evaluator
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
def _ensure_storage_dirs() -> None:
|
|
70
|
+
"""Ensure storage directories exist.
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
os.makedirs(STORAGE_DIR)
|
|
72
|
+
MEDIUM (M3): Avoid filesystem writes at import time; create directories only
|
|
73
|
+
when storage/audit features are actually used.
|
|
74
|
+
"""
|
|
75
|
+
os.makedirs(STORAGE_DIR, exist_ok=True)
|
|
76
|
+
os.makedirs(AUDIT_DIR, exist_ok=True)
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
|
|
79
|
+
def _ensure_parent_dir(path: str) -> None:
|
|
80
|
+
"""Ensure the parent directory for a path exists.
|
|
81
|
+
|
|
82
|
+
MEDIUM (M3): Used to lazily create storage directories only when a feature
|
|
83
|
+
actually persists data.
|
|
84
|
+
"""
|
|
85
|
+
parent = os.path.dirname(path)
|
|
86
|
+
if parent:
|
|
87
|
+
os.makedirs(parent, exist_ok=True)
|
|
84
88
|
|
|
85
89
|
|
|
86
90
|
class AuditLog:
|
|
@@ -94,6 +98,8 @@ class AuditLog:
|
|
|
94
98
|
self.entries = [] # In-memory audit log
|
|
95
99
|
self.max_entries = max_entries
|
|
96
100
|
self.persist_to_file = persist_to_file
|
|
101
|
+
if self.persist_to_file:
|
|
102
|
+
_ensure_storage_dirs()
|
|
97
103
|
self.audit_file = os.path.join(AUDIT_DIR, f"audit_{uuid.uuid4().hex[:8]}.jsonl")
|
|
98
104
|
|
|
99
105
|
def log(self, data_name, action, data_type, timestamp=None, additional_context=None):
|
|
@@ -368,7 +374,6 @@ class SecurityContext:
|
|
|
368
374
|
- For matching trails, record a derived audit entry and also print to stdout.
|
|
369
375
|
"""
|
|
370
376
|
try:
|
|
371
|
-
# simple stringify payload for filtering
|
|
372
377
|
payload_str = json.dumps(payload) if not isinstance(payload, str) else payload
|
|
373
378
|
except Exception:
|
|
374
379
|
try:
|
|
@@ -390,8 +395,12 @@ class SecurityContext:
|
|
|
390
395
|
if isinstance(flt, str) and flt.startswith('re:'):
|
|
391
396
|
import re
|
|
392
397
|
pattern = flt[3:]
|
|
393
|
-
|
|
394
|
-
|
|
398
|
+
# SECURITY (H2): Guard against ReDoS — timeout regex match
|
|
399
|
+
try:
|
|
400
|
+
if re.search(pattern, payload_str, flags=0):
|
|
401
|
+
matched = True
|
|
402
|
+
except re.error:
|
|
403
|
+
logger.debug("Invalid trail regex filter; skipping", exc_info=True)
|
|
395
404
|
elif isinstance(flt, str) and ':' in flt:
|
|
396
405
|
# key:value pattern
|
|
397
406
|
k, v = flt.split(':', 1)
|
|
@@ -407,6 +416,7 @@ class SecurityContext:
|
|
|
407
416
|
if flt in payload_str:
|
|
408
417
|
matched = True
|
|
409
418
|
except Exception:
|
|
419
|
+
logger.debug("Trail filter evaluation failed; skipping trail", exc_info=True)
|
|
410
420
|
matched = False
|
|
411
421
|
|
|
412
422
|
if not matched:
|
|
@@ -424,7 +434,7 @@ class SecurityContext:
|
|
|
424
434
|
try:
|
|
425
435
|
self.audit_log.entries.append(entry)
|
|
426
436
|
except Exception:
|
|
427
|
-
|
|
437
|
+
logger.debug("Failed to append trail entry to audit_log.entries", exc_info=True)
|
|
428
438
|
|
|
429
439
|
# Deliver to configured sinks
|
|
430
440
|
for sink in list(self.trail_sinks):
|
|
@@ -433,16 +443,15 @@ class SecurityContext:
|
|
|
433
443
|
if stype == 'stdout':
|
|
434
444
|
print(f"[TRAIL:{tid}] {event_type} -> {payload_str}")
|
|
435
445
|
elif stype == 'file':
|
|
446
|
+
_ensure_storage_dirs()
|
|
436
447
|
path = sink.get('path') or os.path.join(AUDIT_DIR, 'trails.jsonl')
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
sf.write(json.dumps(entry) + '\n')
|
|
440
|
-
except Exception:
|
|
441
|
-
pass
|
|
448
|
+
with open(path, 'a', encoding='utf-8') as sf:
|
|
449
|
+
sf.write(json.dumps(entry) + '\n')
|
|
442
450
|
elif stype == 'sqlite':
|
|
451
|
+
_ensure_storage_dirs()
|
|
443
452
|
db_path = sink.get('db_path') or os.path.join(STORAGE_DIR, 'trails.db')
|
|
453
|
+
conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
444
454
|
try:
|
|
445
|
-
conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
446
455
|
cur = conn.cursor()
|
|
447
456
|
cur.execute('''CREATE TABLE IF NOT EXISTS trails (
|
|
448
457
|
id TEXT PRIMARY KEY,
|
|
@@ -455,18 +464,14 @@ class SecurityContext:
|
|
|
455
464
|
entry['id'], entry['trail_id'], entry['event_type'], entry['payload'], entry['timestamp']
|
|
456
465
|
))
|
|
457
466
|
conn.commit()
|
|
467
|
+
finally:
|
|
458
468
|
conn.close()
|
|
459
|
-
except Exception:
|
|
460
|
-
pass
|
|
461
469
|
elif stype == 'callback':
|
|
462
470
|
cb = sink.get('callback')
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
cb(entry)
|
|
466
|
-
except Exception:
|
|
467
|
-
pass
|
|
471
|
+
if callable(cb):
|
|
472
|
+
cb(entry)
|
|
468
473
|
except Exception:
|
|
469
|
-
|
|
474
|
+
logger.debug("Trail sink delivery failed", exc_info=True)
|
|
470
475
|
|
|
471
476
|
|
|
472
477
|
def register_verify_check(self, name, check_func):
|
|
@@ -524,13 +529,39 @@ def get_security_context():
|
|
|
524
529
|
|
|
525
530
|
|
|
526
531
|
def _is_ip_in_list(ip, ip_list):
|
|
527
|
-
"""Check if IP matches CIDR or exact match in list
|
|
532
|
+
"""Check if IP matches CIDR or exact match in list.
|
|
533
|
+
|
|
534
|
+
SECURITY (H1): Uses proper bit-masking for CIDR checks instead of string prefix.
|
|
535
|
+
"""
|
|
536
|
+
import struct
|
|
537
|
+
import socket
|
|
538
|
+
|
|
539
|
+
def _ip_to_int(ip_str):
|
|
540
|
+
"""Convert dotted-quad IP to 32-bit integer."""
|
|
541
|
+
try:
|
|
542
|
+
return struct.unpack('!I', socket.inet_aton(ip_str))[0]
|
|
543
|
+
except (socket.error, struct.error, OSError):
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
ip_int = _ip_to_int(ip)
|
|
547
|
+
if ip_int is None:
|
|
548
|
+
return False
|
|
549
|
+
|
|
528
550
|
for pattern in ip_list:
|
|
529
551
|
if "/" in pattern: # CIDR notation
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
552
|
+
try:
|
|
553
|
+
network_str, prefix_len_str = pattern.split("/", 1)
|
|
554
|
+
prefix_len = int(prefix_len_str)
|
|
555
|
+
if prefix_len < 0 or prefix_len > 32:
|
|
556
|
+
continue
|
|
557
|
+
network_int = _ip_to_int(network_str)
|
|
558
|
+
if network_int is None:
|
|
559
|
+
continue
|
|
560
|
+
mask = (0xFFFFFFFF << (32 - prefix_len)) & 0xFFFFFFFF
|
|
561
|
+
if (ip_int & mask) == (network_int & mask):
|
|
562
|
+
return True
|
|
563
|
+
except (ValueError, TypeError):
|
|
564
|
+
continue
|
|
534
565
|
elif ip == pattern: # Exact match
|
|
535
566
|
return True
|
|
536
567
|
return False
|
|
@@ -540,177 +571,7 @@ def _is_ip_in_list(ip, ip_list):
|
|
|
540
571
|
# ENTITY SYSTEM - Object-Oriented Data Structures
|
|
541
572
|
# ===============================================
|
|
542
573
|
|
|
543
|
-
|
|
544
|
-
"""Represents an entity definition with properties and methods"""
|
|
545
|
-
|
|
546
|
-
def __init__(self, name, properties, methods=None, parent=None):
|
|
547
|
-
self.name = name
|
|
548
|
-
self.properties = properties # {prop_name: {type, default_value}}
|
|
549
|
-
self.methods = methods or {} # {method_name: Action}
|
|
550
|
-
self.parent = parent # Parent entity (inheritance)
|
|
551
|
-
|
|
552
|
-
def create_instance(self, values=None):
|
|
553
|
-
"""Create an instance of this entity with dependency injection support"""
|
|
554
|
-
# Perform dependency injection for marked properties
|
|
555
|
-
injected_values = values or {}
|
|
556
|
-
|
|
557
|
-
# Check if this entity has injected dependencies
|
|
558
|
-
if hasattr(self, 'injected_deps') and self.injected_deps:
|
|
559
|
-
from zexus.dependency_injection import get_di_registry
|
|
560
|
-
|
|
561
|
-
registry = get_di_registry()
|
|
562
|
-
# Use __main__ as default module context
|
|
563
|
-
container = registry.get_container("__main__")
|
|
564
|
-
|
|
565
|
-
for dep_name in self.injected_deps:
|
|
566
|
-
if dep_name not in injected_values:
|
|
567
|
-
# Try to inject from DI container
|
|
568
|
-
try:
|
|
569
|
-
injected_value = container.get(dep_name)
|
|
570
|
-
injected_values[dep_name] = injected_value
|
|
571
|
-
except BaseException as e:
|
|
572
|
-
# Dependency not available - use NULL placeholder
|
|
573
|
-
from zexus.object import NULL
|
|
574
|
-
injected_values[dep_name] = NULL
|
|
575
|
-
|
|
576
|
-
instance = EntityInstance(self, injected_values)
|
|
577
|
-
return instance
|
|
578
|
-
|
|
579
|
-
def get_all_properties(self):
|
|
580
|
-
"""Get all properties including inherited ones, in correct order (parent first, then child)"""
|
|
581
|
-
props = {}
|
|
582
|
-
# First add parent properties
|
|
583
|
-
if self.parent:
|
|
584
|
-
parent_props = self.parent.get_all_properties()
|
|
585
|
-
props.update(parent_props)
|
|
586
|
-
# Then add/override with child properties
|
|
587
|
-
props.update(self.properties)
|
|
588
|
-
return props
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
class EntityInstance:
|
|
592
|
-
"""Represents an instance of an entity"""
|
|
593
|
-
|
|
594
|
-
def __init__(self, entity_def, values):
|
|
595
|
-
self.entity_def = entity_def
|
|
596
|
-
self.data = values or {}
|
|
597
|
-
self._validate_properties()
|
|
598
|
-
|
|
599
|
-
def _validate_properties(self):
|
|
600
|
-
"""Validate that all required properties are present and inject dependencies"""
|
|
601
|
-
all_props = self.entity_def.get_all_properties()
|
|
602
|
-
for prop_name, prop_config in all_props.items():
|
|
603
|
-
if prop_name not in self.data:
|
|
604
|
-
# Check if this is an injected dependency
|
|
605
|
-
if prop_config.get("injected", False):
|
|
606
|
-
# Try to inject from DI registry
|
|
607
|
-
try:
|
|
608
|
-
from zexus.dependency_injection import get_di_registry
|
|
609
|
-
from zexus.object import NULL
|
|
610
|
-
registry = get_di_registry()
|
|
611
|
-
container = registry.get_container("__main__")
|
|
612
|
-
if container:
|
|
613
|
-
injected_value = container.get(prop_name)
|
|
614
|
-
self.data[prop_name] = injected_value
|
|
615
|
-
else:
|
|
616
|
-
# No container, set to NULL
|
|
617
|
-
self.data[prop_name] = NULL
|
|
618
|
-
except Exception:
|
|
619
|
-
# If injection fails, set to NULL
|
|
620
|
-
from zexus.object import NULL
|
|
621
|
-
self.data[prop_name] = NULL
|
|
622
|
-
elif "default_value" in prop_config:
|
|
623
|
-
self.data[prop_name] = prop_config["default_value"]
|
|
624
|
-
|
|
625
|
-
def get(self, property_name):
|
|
626
|
-
"""Get property value"""
|
|
627
|
-
return self.data.get(property_name)
|
|
628
|
-
|
|
629
|
-
def set(self, property_name, value):
|
|
630
|
-
"""Set property value (prevent modification if property is sealed)"""
|
|
631
|
-
if property_name not in self.entity_def.get_all_properties():
|
|
632
|
-
raise ValueError(f"Unknown property: {property_name}")
|
|
633
|
-
existing = self.data.get(property_name)
|
|
634
|
-
# Avoid importing SealedObject here to prevent circular imports; use name-based check
|
|
635
|
-
if existing is not None and existing.__class__.__name__ == 'SealedObject':
|
|
636
|
-
raise ValueError(f"Cannot modify sealed property: {property_name}")
|
|
637
|
-
self.data[property_name] = value
|
|
638
|
-
|
|
639
|
-
def to_dict(self):
|
|
640
|
-
"""Convert to dictionary"""
|
|
641
|
-
return self.data
|
|
642
|
-
|
|
643
|
-
def __str__(self):
|
|
644
|
-
"""String representation of entity instance"""
|
|
645
|
-
entity_name = self.entity_def.name if hasattr(self.entity_def, 'name') else 'Entity'
|
|
646
|
-
# Format properties nicely
|
|
647
|
-
props = []
|
|
648
|
-
for key, value in self.data.items():
|
|
649
|
-
# Convert value to a readable string
|
|
650
|
-
if hasattr(value, 'value'):
|
|
651
|
-
# Object wrapper with value attribute (Integer, String, etc.)
|
|
652
|
-
props.append(f"{key}={value.value}")
|
|
653
|
-
elif hasattr(value, '__class__') and hasattr(value.__class__, '__name__'):
|
|
654
|
-
if value.__class__.__name__ in ['EntityInstance', 'SealedObject']:
|
|
655
|
-
props.append(f"{key}=<{value.__class__.__name__}>")
|
|
656
|
-
else:
|
|
657
|
-
try:
|
|
658
|
-
props.append(f"{key}={value}")
|
|
659
|
-
except:
|
|
660
|
-
props.append(f"{key}=<object>")
|
|
661
|
-
else:
|
|
662
|
-
props.append(f"{key}={value}")
|
|
663
|
-
props_str = ", ".join(props)
|
|
664
|
-
return f"{entity_name}({props_str})"
|
|
665
|
-
|
|
666
|
-
def __repr__(self):
|
|
667
|
-
"""Python representation"""
|
|
668
|
-
return self.__str__()
|
|
669
|
-
|
|
670
|
-
def call_method(self, method_name, args):
|
|
671
|
-
"""Call a method on this entity instance"""
|
|
672
|
-
if method_name not in self.entity_def.methods:
|
|
673
|
-
from zexus.object import EvaluationError
|
|
674
|
-
return EvaluationError(f"Method '{method_name}' not supported for ENTITY_INSTANCE")
|
|
675
|
-
|
|
676
|
-
# Get the method (Action or Function)
|
|
677
|
-
method = self.entity_def.methods[method_name]
|
|
678
|
-
|
|
679
|
-
# Create a new environment for the method execution
|
|
680
|
-
from zexus.environment import Environment
|
|
681
|
-
method_env = Environment(outer=method.env if hasattr(method, 'env') else None)
|
|
682
|
-
|
|
683
|
-
# Bind 'this' to the current instance in the method environment
|
|
684
|
-
method_env.set('this', self)
|
|
685
|
-
|
|
686
|
-
# Bind method parameters to arguments
|
|
687
|
-
if hasattr(method, 'parameters'):
|
|
688
|
-
for i, param in enumerate(method.parameters):
|
|
689
|
-
if i < len(args):
|
|
690
|
-
# Handle both Identifier objects and ParameterNode objects
|
|
691
|
-
if hasattr(param, 'name'):
|
|
692
|
-
# It's a ParameterNode with name and type
|
|
693
|
-
param_name = param.name.value if hasattr(param.name, 'value') else str(param.name)
|
|
694
|
-
elif hasattr(param, 'value'):
|
|
695
|
-
# It's an Identifier
|
|
696
|
-
param_name = param.value
|
|
697
|
-
else:
|
|
698
|
-
# Fallback to string representation
|
|
699
|
-
param_name = str(param)
|
|
700
|
-
method_env.set(param_name, args[i])
|
|
701
|
-
|
|
702
|
-
# Use cached evaluator for performance (avoids repeated Evaluator() initialization)
|
|
703
|
-
evaluator = _get_cached_evaluator()
|
|
704
|
-
|
|
705
|
-
# Execute the method body with stack trace
|
|
706
|
-
result = evaluator.eval_node(method.body, method_env, stack_trace=[])
|
|
707
|
-
|
|
708
|
-
# Unwrap return values
|
|
709
|
-
from zexus.object import ReturnValue
|
|
710
|
-
if isinstance(result, ReturnValue):
|
|
711
|
-
return result.value
|
|
712
|
-
|
|
713
|
-
return result
|
|
574
|
+
# MEDIUM (M5): EntityDefinition/EntityInstance are provided by `zexus.object`.
|
|
714
575
|
|
|
715
576
|
|
|
716
577
|
# ===============================================
|
|
@@ -787,6 +648,7 @@ class InMemoryBackend(StorageBackend):
|
|
|
787
648
|
class SQLiteBackend(StorageBackend):
|
|
788
649
|
def __init__(self, db_path):
|
|
789
650
|
import sqlite3
|
|
651
|
+
_ensure_parent_dir(db_path)
|
|
790
652
|
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
791
653
|
self.cursor = self.conn.cursor()
|
|
792
654
|
|
|
@@ -863,6 +725,7 @@ class SQLiteBackend(StorageBackend):
|
|
|
863
725
|
class LevelDBBackend(StorageBackend):
|
|
864
726
|
def __init__(self, db_path):
|
|
865
727
|
if not _LEVELDB_AVAILABLE: raise ImportError("plyvel not installed")
|
|
728
|
+
_ensure_parent_dir(db_path)
|
|
866
729
|
self.db = plyvel.DB(db_path, create_if_missing=True)
|
|
867
730
|
|
|
868
731
|
def set(self, key, value):
|
|
@@ -887,6 +750,7 @@ class LevelDBBackend(StorageBackend):
|
|
|
887
750
|
class RocksDBBackend(StorageBackend):
|
|
888
751
|
def __init__(self, db_path):
|
|
889
752
|
if not _ROCKSDB_AVAILABLE: raise ImportError("rocksdb not installed")
|
|
753
|
+
_ensure_parent_dir(db_path)
|
|
890
754
|
self.db = rocksdb.DB(db_path, rocksdb.Options(create_if_missing=True))
|
|
891
755
|
|
|
892
756
|
def set(self, key, value):
|
|
@@ -1030,6 +894,9 @@ class ContractStorage:
|
|
|
1030
894
|
def __init__(self, contract_id, db_type=None):
|
|
1031
895
|
self.transaction_log = []
|
|
1032
896
|
|
|
897
|
+
# SECURITY (C6): Sanitize contract_id to prevent path traversal
|
|
898
|
+
contract_id = _sanitize_identifier(str(contract_id), "contract_id")
|
|
899
|
+
|
|
1033
900
|
# Determine storage type: explicit > env var > default (memory)
|
|
1034
901
|
if db_type is None:
|
|
1035
902
|
db_type = os.environ.get("ZEXUS_STORAGE_ENGINE", "memory")
|
|
@@ -1043,11 +910,16 @@ class ContractStorage:
|
|
|
1043
910
|
if db_type == "memory":
|
|
1044
911
|
self.backend = InMemoryBackend()
|
|
1045
912
|
elif db_type == "leveldb" and _LEVELDB_AVAILABLE:
|
|
913
|
+
_ensure_parent_dir(base_path)
|
|
1046
914
|
self.backend = LevelDBBackend(base_path)
|
|
1047
915
|
elif db_type == "rocksdb" and _ROCKSDB_AVAILABLE:
|
|
1048
|
-
|
|
916
|
+
db_path = f"{base_path}.rdb"
|
|
917
|
+
_ensure_parent_dir(db_path)
|
|
918
|
+
self.backend = RocksDBBackend(db_path)
|
|
1049
919
|
elif db_type == "sqlite":
|
|
1050
|
-
|
|
920
|
+
db_path = f"{base_path}.sqlite"
|
|
921
|
+
_ensure_parent_dir(db_path)
|
|
922
|
+
self.backend = SQLiteBackend(db_path)
|
|
1051
923
|
else:
|
|
1052
924
|
# Unknown type, fall back to memory
|
|
1053
925
|
self.backend = InMemoryBackend()
|
|
@@ -1538,12 +1410,21 @@ class SmartContract:
|
|
|
1538
1410
|
register_contract(self)
|
|
1539
1411
|
|
|
1540
1412
|
def __del__(self):
|
|
1541
|
-
"""
|
|
1413
|
+
"""Best-effort commit on cleanup.
|
|
1414
|
+
|
|
1415
|
+
Destructors must never raise; however, silently swallowing exceptions
|
|
1416
|
+
can hide storage corruption or shutdown bugs. We log in debug mode.
|
|
1417
|
+
"""
|
|
1542
1418
|
try:
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1419
|
+
storage = getattr(self, 'storage', None)
|
|
1420
|
+
if storage is not None:
|
|
1421
|
+
storage.commit_batch(force=True)
|
|
1422
|
+
except Exception as exc:
|
|
1423
|
+
try:
|
|
1424
|
+
if zexus_config.should_log('debug'):
|
|
1425
|
+
print(f"⚠️ SmartContract.__del__ commit failed: {exc}")
|
|
1426
|
+
except Exception:
|
|
1427
|
+
pass
|
|
1547
1428
|
|
|
1548
1429
|
def instantiate(self, args=None):
|
|
1549
1430
|
"""Create a new instance of this contract when called like ZiverWallet()."""
|
|
@@ -2077,8 +1958,33 @@ class AuthConfig:
|
|
|
2077
1958
|
|
|
2078
1959
|
def validate_token(self, token):
|
|
2079
1960
|
"""Validate a token"""
|
|
2080
|
-
#
|
|
2081
|
-
|
|
1961
|
+
# LI10: Don't unconditionally accept tokens.
|
|
1962
|
+
# This is still a lightweight heuristic (no provider integration), but it
|
|
1963
|
+
# rejects obviously invalid/empty values.
|
|
1964
|
+
if token is None:
|
|
1965
|
+
return False
|
|
1966
|
+
|
|
1967
|
+
try:
|
|
1968
|
+
token_str = token.value if hasattr(token, 'value') else str(token)
|
|
1969
|
+
except Exception:
|
|
1970
|
+
return False
|
|
1971
|
+
|
|
1972
|
+
token_str = token_str.strip()
|
|
1973
|
+
if not token_str:
|
|
1974
|
+
return False
|
|
1975
|
+
|
|
1976
|
+
if token_str.lower().startswith("bearer "):
|
|
1977
|
+
token_str = token_str[7:].strip()
|
|
1978
|
+
if not token_str:
|
|
1979
|
+
return False
|
|
1980
|
+
|
|
1981
|
+
# Accept JWT-like tokens (three base64url-ish segments) or opaque tokens
|
|
1982
|
+
# with a reasonable minimum length.
|
|
1983
|
+
parts = token_str.split('.')
|
|
1984
|
+
if len(parts) == 3 and all(parts):
|
|
1985
|
+
return True
|
|
1986
|
+
|
|
1987
|
+
return len(token_str) >= 16
|
|
2082
1988
|
|
|
2083
1989
|
def is_token_expired(self, token_data):
|
|
2084
1990
|
"""Check if token is expired"""
|
|
@@ -290,6 +290,31 @@ class StaticTypeChecker:
|
|
|
290
290
|
self._check_block(stmt.body, ret_ts)
|
|
291
291
|
self._pop_scope()
|
|
292
292
|
|
|
293
|
+
def _check_EntityStatement(self, stmt):
|
|
294
|
+
"""Register entity as a callable constructor.
|
|
295
|
+
|
|
296
|
+
Entity constructors can be called two ways:
|
|
297
|
+
- Positional: Point(3, 4) — N args matching N properties
|
|
298
|
+
- Map style: Point{x: 3, y: 4} — parsed as 1 MapLiteral arg
|
|
299
|
+
|
|
300
|
+
We register with _is_entity_constructor marker so _check_call
|
|
301
|
+
can skip strict arity validation for the single-map-arg case.
|
|
302
|
+
"""
|
|
303
|
+
name = stmt.name.value if hasattr(stmt.name, 'value') else str(stmt.name)
|
|
304
|
+
param_types: List[Tuple[str, Optional[TypeSpec]]] = []
|
|
305
|
+
for prop in (stmt.properties or []):
|
|
306
|
+
prop_name = prop.get('name', '') if isinstance(prop, dict) else getattr(prop, 'name', '')
|
|
307
|
+
prop_type = prop.get('type', None) if isinstance(prop, dict) else getattr(prop, 'type', None)
|
|
308
|
+
ts = _resolve_annotation(prop_type)
|
|
309
|
+
param_types.append((prop_name, ts))
|
|
310
|
+
if name:
|
|
311
|
+
self._scope.define_callable(name, param_types, None)
|
|
312
|
+
self._scope.define(name, TypeSpec(BaseType.ANY))
|
|
313
|
+
# Mark this name as an entity constructor for flexible arity
|
|
314
|
+
if not hasattr(self._scope, '_entity_names'):
|
|
315
|
+
self._scope._entity_names = set()
|
|
316
|
+
self._scope._entity_names.add(name)
|
|
317
|
+
|
|
293
318
|
def _check_FunctionStatement(self, stmt: ast.FunctionStatement):
|
|
294
319
|
# Reuse action logic
|
|
295
320
|
name = stmt.name if isinstance(stmt.name, str) else getattr(stmt.name, "value", None)
|
|
@@ -382,14 +407,28 @@ class StaticTypeChecker:
|
|
|
382
407
|
params, _ = sig
|
|
383
408
|
args = call.arguments or []
|
|
384
409
|
|
|
385
|
-
#
|
|
410
|
+
# Check if this is an entity constructor — they accept flexible arity
|
|
411
|
+
# (either N positional args or 1 MapLiteral arg for {field: value} syntax)
|
|
412
|
+
is_entity = False
|
|
413
|
+
scope = self._scope
|
|
414
|
+
while scope:
|
|
415
|
+
if hasattr(scope, '_entity_names') and fn_name in scope._entity_names:
|
|
416
|
+
is_entity = True
|
|
417
|
+
break
|
|
418
|
+
scope = scope.parent
|
|
419
|
+
|
|
420
|
+
# Arity check (skip for entity constructors with single map arg)
|
|
386
421
|
expected = len(params)
|
|
387
422
|
got = len(args)
|
|
388
423
|
if got != expected:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
424
|
+
if is_entity and got == 1:
|
|
425
|
+
# Entity{field: value} syntax — single MapLiteral arg is valid
|
|
426
|
+
pass
|
|
427
|
+
else:
|
|
428
|
+
self._error(
|
|
429
|
+
f"'{fn_name}' expects {expected} argument(s) but got {got}",
|
|
430
|
+
call,
|
|
431
|
+
)
|
|
393
432
|
|
|
394
433
|
# Per-argument type check
|
|
395
434
|
for i, (pname, pts) in enumerate(params):
|
|
@@ -391,9 +391,13 @@ def _deserialize_constant(r: _Reader) -> Any:
|
|
|
391
391
|
result[key] = val
|
|
392
392
|
return result
|
|
393
393
|
elif tag == ConstTag.OPAQUE:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
# SECURITY (C10): pickle.loads() removed — arbitrary code execution risk.
|
|
395
|
+
# Skip the raw bytes but refuse to deserialize.
|
|
396
|
+
_data = r.raw_bytes()
|
|
397
|
+
raise ValueError(
|
|
398
|
+
"OPAQUE constant tag is disabled for security (pickle deserialization). "
|
|
399
|
+
"Re-compile the .zxc file with a safe serialization format."
|
|
400
|
+
)
|
|
397
401
|
else:
|
|
398
402
|
raise ValueError(f"Unknown constant tag: 0x{tag:02x}")
|
|
399
403
|
|
package/src/zexus/vm/bytecode.py
CHANGED
|
@@ -342,6 +342,12 @@ class BytecodeBuilder:
|
|
|
342
342
|
idx = self.emit("JUMP_IF_FALSE", None)
|
|
343
343
|
self._forward_refs.setdefault(label, []).append(idx)
|
|
344
344
|
return idx
|
|
345
|
+
|
|
346
|
+
def emit_jump_if_true(self, label: str) -> int:
|
|
347
|
+
"""Emit a conditional jump (true) to a label"""
|
|
348
|
+
idx = self.emit("JUMP_IF_TRUE", None)
|
|
349
|
+
self._forward_refs.setdefault(label, []).append(idx)
|
|
350
|
+
return idx
|
|
345
351
|
|
|
346
352
|
def resolve_labels(self):
|
|
347
353
|
"""Resolve all forward label references"""
|