1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
use super::*;
impl ExecutionContext {
pub(crate) fn handle_contract_call(&mut self) -> Result<(), RuntimeError> {
// Neo N3 syscall convention: the first argument is at the top of the stack.
// For System.Contract.Call(hash, method, flags, args), the evaluation stack
// order is: [args, flags, method, hash].
let contract_item = self.pop_stack()?;
let method_item = self.pop_stack()?;
let _flags = self.pop_stack()?; // call flags ignored in emulator
let params = self.pop_stack()?;
let method = String::from_utf8(Self::stack_item_to_bytes(method_item)).unwrap_or_default();
let contract_bytes = Self::stack_item_to_bytes(contract_item);
let mut hash = [0u8; 20];
for (i, b) in contract_bytes.iter().take(20).enumerate() {
hash[i] = *b;
}
// Task #70: route `this.someFn()` self external calls to the
// compiled method offset from the manifest, so `invoke_native_contract`
// doesn't return `Null` (silently 0/false for typed returns).
//
// Task #83 (batch #33 K1): also route the 20-byte zero placeholder
// that `new Contract()` pushes for factory-deploy receivers — the
// analyse phase merges the sibling's public methods into the host
// (see `analyse_all_sources`), so `self_method_offsets` already
// carries an entry for the target method name.
let is_zero_placeholder = hash.iter().all(|b| *b == 0);
if hash[..] == self.default_account_bytes[..] || is_zero_placeholder {
// Task #126 — if the explicitly-named method isn't in the merged
// self-offsets table (e.g. `Target(t).nonExistentMethod()` where
// `TargetImpl` declares only `fallback()`), route to the
// `fallback` entry instead. This mirrors Solidity's ABI
// dispatcher: an unknown selector on a contract with a
// `fallback()` falls through to that fallback. Without this
// redirect, the unknown-method path would drop into
// `invoke_native_contract`, which returns `Null` for the zero-
// placeholder hash — silently absorbing the call and letting
// the caller's try-arm fire with a bogus zero/empty result
// instead of propagating the fallback's revert envelope.
let resolved_offset = self
.self_method_offsets
.get(&method)
.copied()
.or_else(|| self.self_method_offsets.get("fallback").copied());
if let Some(target_offset) = resolved_offset {
let args_vec = match ¶ms {
StackItem::Array(items) => items.borrow().clone(),
_ => Vec::new(),
};
// The SYSCALL opcode handler normally post-increments the
// instruction pointer by 5 (see instruction/syscall.rs). The
// return address must therefore be `instruction_pointer + 5`
// (i.e. past the SYSCALL). We set IP directly to the target
// method's compiled offset and raise the `suppress_ip_advance`
// flag so the SYSCALL dispatcher skips its post-increment.
let return_address = self.instruction_pointer.wrapping_add(5);
// Task #123 — compute the synthetic "virtual caller" script
// hash BEFORE pushing the frame (needs `&self`). The bundle's
// `default_account_bytes` already represents the currently
// executing contract (Middleware, in the canonical User →
// Middleware → Caller scenario). We derive a distinct
// caller-side identity from it so the nested callee's
// `CallingScriptHash` reads back a value that differs from
// both `EntryScriptHash` (which stays pinned to the bundle
// hash) and `Transaction.Sender` (which backs `tx.origin`).
//
// Without this override, every self-offsets dispatch collapsed
// `CallingScriptHash == EntryScriptHash`, short-circuiting the
// `msg.sender` lowering in
// `src/cli/bytecode/bytecode_helpers/array_runtime.rs`
// (`RuntimeValue::MsgSender`) to `Transaction.Sender` — i.e.
// `tx.origin` leaked into `msg.sender` on every nested call,
// defeating the whole "only the direct caller can authorise
// X" guard that the EVM convention relies on.
let synthetic_caller =
Self::derive_self_offsets_caller_hash(&self.default_account_bytes);
// Task #197 — push the call frame BEFORE unpacking args onto
// the evaluation stack. The frame's `stack_base` snapshots
// `self.stack.len()` at the moment of creation, and
// `return_from_function` later uses
// `self.stack.len() > frame.stack_base` to decide whether the
// callee produced a return value. If the args are pushed
// first, the stack_base is inflated by `args.len()`, and the
// callee's INITSLOT pops them back down — so a single-return
// callee leaves `stack.len() == stack_base` on the caller's
// side, the check reads as "no return", and the Task #160
// `syscall_result_expected` path synthesises a spurious Null
// that clobbers the real return value. Moving the frame
// push ahead of the arg push keeps stack_base at the
// pre-call baseline, so a callee producing exactly one
// result lands at `stack_base + 1` and the check fires
// correctly.
self.push_call_frame(return_address)?;
// S7 fix — capture the storage-overlay snapshot BEFORE the
// mutable borrow of call_stack below (snapshot_storage_overlay
// borrows self too, which would conflict with last_mut).
let storage_snapshot = Some(self.snapshot_storage_overlay());
if let Some(frame) = self.call_stack.last_mut() {
frame.msg_sender_override = Some(synthetic_caller);
// Task #160 — mark the frame so `return_from_function`
// synthesises a `StackItem::Null` result for void callees.
// The caller site emitted by `try_catch.rs` (and the generic
// `lower_expression_statement` path) assumes every external
// call leaves exactly one syscall result on the evaluation
// stack — matching what `invoke_native_contract` does on
// the non-zero-placeholder branch below. Without this flag,
// a void target's `ReturnVoid` (`0x40`) pushes nothing, and
// the caller's implicit `DROP` faults into the catch arm
// — turning a happy-path `try Target(t).voidFn() {}` into
// an unintended catch firing.
frame.syscall_result_expected = true;
// S7 fix — attach the snapshot captured above.
frame.storage_snapshot = storage_snapshot;
}
// Unpack the params array and push args in reverse so that the
// target method's INITSLOT pops them into arg slots in order.
for arg in args_vec.iter().rev() {
self.push_stack(arg.clone())?;
}
self.instruction_pointer = target_offset;
self.syscall_suppress_ip_advance = true;
// Do NOT push a result here: the target method's RET will
// leave its return value on the evaluation stack, which is
// exactly what the caller (the SYSCALL site) expects.
return Ok(());
}
}
let result = self.invoke_native_contract(&hash, &method, params);
self.push_stack(result)?;
Ok(())
}
/// Task #123 — deterministic derivation of the "virtual caller" script
/// hash for a self-offsets dispatch frame.
///
/// Neo's native `CallingScriptHash` rolls over whenever the VM crosses a
/// real contract boundary. The embedded runtime's self-offsets routing
/// (see `handle_contract_call` above and the Task #83 sibling-merge pass
/// in `src/solidity/solidity_analyse.rs::analyse_all_sources`) executes a
/// simulated boundary *inside* the same compiled bundle, so there is no
/// real script-hash rotation to observe. This helper synthesises a
/// deterministic callee-facing identity by hashing the bundle's executing
/// script hash with a fixed domain tag, guaranteeing it differs from both
/// `default_account_bytes` (the entry/executing hash) and any plausible
/// `Transaction.Sender` bytes that the host might inject.
///
/// Determinism matters: the same bundle invoked with the same dispatch
/// path always produces the same 20-byte value, so tests that assert on
/// `msg.sender` byte content stay stable across runs.
fn derive_self_offsets_caller_hash(executing: &[u8]) -> Vec<u8> {
use ripemd::Ripemd160;
use sha2::{Digest, Sha256};
// Domain tag keeps the derivation distinct from any other `Hash160`
// preimage the compiler emits (address derivation, bytecode hashing,
// etc.) so accidental collisions are not possible.
const DOMAIN: &[u8] = b"neo-devpack-solidity/self-offsets/msg-sender/v1";
let mut preimage = Vec::with_capacity(DOMAIN.len() + executing.len());
preimage.extend_from_slice(DOMAIN);
preimage.extend_from_slice(executing);
let sha = Sha256::digest(&preimage);
let hash: [u8; 20] = Ripemd160::digest(sha).into();
hash.to_vec()
}
/// Task #123 — resolve the active `msg.sender` (`CallingScriptHash`) for
/// the current frame.
///
/// Walks the call stack from the most recent frame downwards, returning
/// the first `msg_sender_override` that is populated. When no frame
/// carries an override (entry-depth execution, or a plain `CALL` frame
/// from the Solidity internal-call lowering), this returns `None` and the
/// `System.Runtime.GetCallingScriptHash` handler falls through to its
/// pre-Task-123 `caller_account`-first behaviour.
pub(crate) fn active_msg_sender_override(&self) -> Option<Vec<u8>> {
for frame in self.call_stack.iter().rev() {
if let Some(bytes) = frame.msg_sender_override.as_ref() {
return Some(bytes.clone());
}
}
None
}
}