neo_solidity/cli/bytecode/
bytecode_core.rs

1#[derive(Clone, Debug)]
2struct CallPatch {
3    position: usize,
4    target: String,
5}
6
7#[derive(Clone, Debug)]
8pub(crate) struct BytecodeBuildOutput {
9    pub script: Vec<u8>,
10    pub tokens: Vec<neo_solidity::neo::MethodToken>,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
14struct MethodTokenKey {
15    hash: [u8; 20],
16    method: String,
17    parameters_count: u16,
18    has_return_value: bool,
19    call_flags: u8,
20}
21
22#[derive(Clone, Debug)]
23struct MethodTokenPatch {
24    position: usize,
25    token: MethodTokenKey,
26}
27
28/// Neo N3 uses CallFlags bitmask values.
29const CALLFLAGS_ALL: u8 = 0x0F;
30/// Read-only call flags (ReadStates | AllowCall).
31const CALLFLAGS_READ_ONLY: u8 = 0x05;
32
33/// Native contract script hashes as they must be pushed onto the NeoVM stack
34/// (UInt160 little-endian byte order).
35const NATIVE_NEO_HASH_LE: [u8; 20] = *b"\xf5\x63\xea\x40\xbc\x28\x3d\x4d\x0e\x05\xc4\x8e\xa3\x05\xb3\xf2\xa0\x73\x40\xef";
36const NATIVE_GAS_HASH_LE: [u8; 20] = *b"\xcf\x76\xe2\x8b\xd0\x06\x2c\x4a\x47\x8e\xe3\x55\x61\x01\x13\x19\xf3\xcf\xa4\xd2";
37const NATIVE_CONTRACT_MANAGEMENT_HASH_LE: [u8; 20] =
38    *b"\xfd\xa3\xfa\x43\x46\xea\x53\x2a\x25\x8f\xc4\x97\xdd\xad\xdb\x64\x37\xc9\xfd\xff";
39const NATIVE_POLICY_HASH_LE: [u8; 20] =
40    *b"\x7b\xc6\x81\xc0\xa1\xf7\x1d\x54\x34\x57\xb6\x8b\xba\x8d\x5f\x9f\xdd\x4e\x5e\xcc";
41const NATIVE_ORACLE_HASH_LE: [u8; 20] =
42    *b"\x58\x87\x17\x11\x7e\x0a\xa8\x10\x72\xaf\xab\x71\xd2\xdd\x89\xfe\x7c\x4b\x92\xfe";
43const NATIVE_ROLE_MANAGEMENT_HASH_LE: [u8; 20] =
44    *b"\xe2\x95\xe3\x91\x54\x4c\x17\x8a\xd9\x4f\x03\xec\x4d\xcd\xff\x78\x53\x4e\xcf\x49";
45const NATIVE_NOTARY_HASH_LE: [u8; 20] =
46    *b"\x3b\xec\x35\x31\x11\x9b\xba\xd7\x6d\xd0\x44\x92\x0b\x0d\xe6\xc3\x19\x4f\xe1\xc1";
47const NATIVE_TREASURY_HASH_LE: [u8; 20] =
48    *b"\xc1\x3a\x56\xc9\x83\x53\xa7\xea\x6a\x32\x4d\x9a\x83\x5d\x1b\x5b\xf2\x26\x63\x15";
49const NATIVE_LEDGER_HASH_LE: [u8; 20] =
50    *b"\xbe\xf2\x04\x31\x40\x36\x2a\x77\xc1\x50\x99\xc7\xe6\x4c\x12\xf7\x00\xb6\x65\xda";
51const NATIVE_CRYPTOLIB_HASH_LE: [u8; 20] =
52    *b"\x1b\xf5\x75\xab\x11\x89\x68\x84\x13\x61\x0a\x35\xa1\x28\x86\xcd\xe0\xb6\x6c\x72";
53const NATIVE_STDLIB_HASH_LE: [u8; 20] =
54    *b"\xc0\xef\x39\xce\xe0\xe4\xe9\x25\xc6\xc2\xa0\x6a\x79\xe1\x44\x0d\xd8\x6f\xce\xac";
55
56pub(crate) fn native_contract_hash(contract: ir::NativeContract) -> [u8; 20] {
57    match contract {
58        ir::NativeContract::Neo => NATIVE_NEO_HASH_LE,
59        ir::NativeContract::Gas => NATIVE_GAS_HASH_LE,
60        ir::NativeContract::ContractManagement => NATIVE_CONTRACT_MANAGEMENT_HASH_LE,
61        ir::NativeContract::Policy => NATIVE_POLICY_HASH_LE,
62        ir::NativeContract::Oracle => NATIVE_ORACLE_HASH_LE,
63        ir::NativeContract::RoleManagement => NATIVE_ROLE_MANAGEMENT_HASH_LE,
64        ir::NativeContract::Notary => NATIVE_NOTARY_HASH_LE,
65        ir::NativeContract::Treasury => NATIVE_TREASURY_HASH_LE,
66        ir::NativeContract::Ledger => NATIVE_LEDGER_HASH_LE,
67        ir::NativeContract::CryptoLib => NATIVE_CRYPTOLIB_HASH_LE,
68        ir::NativeContract::StdLib => NATIVE_STDLIB_HASH_LE,
69    }
70}
71
72pub(crate) fn generate_contract_bytecode(
73    metadata: &mut ContractMetadata,
74    ir_module: &ir::Module,
75    verbose: bool,
76    optimizer_level: u8,
77    use_callt: bool,
78) -> Result<BytecodeBuildOutput, String> {
79    let function_map: HashMap<_, _> = ir_module
80        .functions
81        .iter()
82        .map(|function| (function.name.as_str(), function))
83        .collect();
84
85    if verbose {
86        println!("  • Using optimizer level {}", optimizer_level.min(3));
87    }
88
89    let mut bytecode = Vec::new();
90    let mut call_fixups: Vec<CallPatch> = Vec::new();
91    let mut token_fixups: Vec<MethodTokenPatch> = Vec::new();
92
93    for method in metadata.methods.iter_mut() {
94        // Constructors without bodies are not emitted as standalone NeoVM methods.
95        // They are only relevant when called from an injected `_deploy`.
96        if matches!(method.kind, FunctionKind::Constructor) && method.body.is_none() {
97            continue;
98        }
99
100        let method_name = method.neo_name.clone();
101        let ir_function = function_map.get(method_name.as_str()).copied().ok_or_else(|| {
102            format!("internal compiler error: missing IR for method '{method_name}'")
103        })?;
104
105        let instruction_count: usize = ir_function
106            .basic_blocks
107            .iter()
108            .map(|block| block.instructions.len())
109            .sum();
110
111        let offset = bytecode.len() as u32;
112        method.offset = offset;
113
114        if verbose {
115            println!(
116                "  • Emitting method '{}' at offset {} ({} IR instruction(s))",
117                method_name, offset, instruction_count
118            );
119        }
120
121        let (function_bytes, patches, token_patches) =
122            emit_ir_function(ir_function, ir_module, method, use_callt);
123        let base_position = bytecode.len();
124        bytecode.extend_from_slice(&function_bytes);
125
126        for patch in patches {
127            call_fixups.push(CallPatch {
128                position: base_position + patch.position,
129                target: patch.target,
130            });
131        }
132
133        for patch in token_patches {
134            token_fixups.push(MethodTokenPatch {
135                position: base_position + patch.position,
136                token: patch.token,
137            });
138        }
139    }
140
141    let offset_map: HashMap<String, u32> = metadata
142        .methods
143        .iter()
144        .map(|method| (method.neo_name.clone(), method.offset))
145        .collect();
146
147    for fixup in call_fixups {
148        if let Some(target_offset) = offset_map.get(&fixup.target) {
149            // CALL_L uses a 4-byte signed offset from the beginning of the CALL_L opcode.
150            let opcode_pos = fixup.position.saturating_sub(1) as i32;
151            let relative = (*target_offset as i32)
152                .checked_sub(opcode_pos)
153                .unwrap_or(0);
154            let bytes = relative.to_le_bytes();
155            if fixup.position + 4 <= bytecode.len() {
156                bytecode[fixup.position..fixup.position + 4].copy_from_slice(&bytes);
157            }
158        } else {
159            eprintln!(
160                "warning: unresolved call target '{}' (offset unavailable)",
161                fixup.target
162            );
163        }
164    }
165
166    if bytecode.is_empty() {
167        bytecode.push(0x40); // RET
168    }
169
170    let tokens = if use_callt {
171        apply_method_tokens(bytecode.as_mut_slice(), &token_fixups)?
172    } else {
173        Vec::new()
174    };
175
176    Ok(BytecodeBuildOutput {
177        script: bytecode,
178        tokens,
179    })
180}
181
182fn apply_method_tokens(
183    bytecode: &mut [u8],
184    patches: &[MethodTokenPatch],
185) -> Result<Vec<neo_solidity::neo::MethodToken>, String> {
186    use std::collections::{BTreeMap, BTreeSet};
187
188    if patches.is_empty() {
189        return Ok(Vec::new());
190    }
191
192    let unique: BTreeSet<MethodTokenKey> = patches.iter().map(|p| p.token.clone()).collect();
193    if unique.len() > neo_solidity::neo::MAX_METHOD_TOKENS {
194        return Err(format!(
195            "CALLT requires <= {} method token(s); got {}",
196            neo_solidity::neo::MAX_METHOD_TOKENS,
197            unique.len()
198        ));
199    }
200
201    let mut token_map: BTreeMap<MethodTokenKey, u16> = BTreeMap::new();
202    let mut tokens: Vec<neo_solidity::neo::MethodToken> = Vec::with_capacity(unique.len());
203
204    for (index, key) in unique.into_iter().enumerate() {
205        let token_index = u16::try_from(index).unwrap_or(u16::MAX);
206        token_map.insert(key.clone(), token_index);
207        tokens.push(neo_solidity::neo::MethodToken::new(
208            key.hash,
209            &key.method,
210            key.parameters_count,
211            key.has_return_value,
212            key.call_flags,
213        ));
214    }
215
216    for patch in patches {
217        let Some(index) = token_map.get(&patch.token) else {
218            continue;
219        };
220        let start = patch.position;
221        let end = start + 2;
222        if end <= bytecode.len() {
223            bytecode[start..end].copy_from_slice(&index.to_le_bytes());
224        }
225    }
226
227    Ok(tokens)
228}