#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CallPatchKind {
CallRelative,
AbsoluteOffset,
}
#[derive(Clone, Debug)]
struct CallPatch {
position: usize,
target: String,
kind: CallPatchKind,
}
#[derive(Clone, Debug)]
pub(crate) struct BytecodeBuildOutput {
pub script: Vec<u8>,
pub tokens: Vec<neo_devpack_solidity::neo::MethodToken>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct MethodTokenKey {
hash: [u8; 20],
method: String,
parameters_count: u16,
has_return_value: bool,
call_flags: u8,
}
#[derive(Clone, Debug)]
struct MethodTokenPatch {
position: usize,
token: MethodTokenKey,
}
const CALLFLAGS_ALL: u8 = 0x0F;
const CALLFLAGS_READ_ONLY: u8 = 0x05;
const 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";
const 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";
const NATIVE_CONTRACT_MANAGEMENT_HASH_LE: [u8; 20] =
*b"\xfd\xa3\xfa\x43\x46\xea\x53\x2a\x25\x8f\xc4\x97\xdd\xad\xdb\x64\x37\xc9\xfd\xff";
const NATIVE_POLICY_HASH_LE: [u8; 20] =
*b"\x7b\xc6\x81\xc0\xa1\xf7\x1d\x54\x34\x57\xb6\x8b\xba\x8d\x5f\x9f\xdd\x4e\x5e\xcc";
const NATIVE_ORACLE_HASH_LE: [u8; 20] =
*b"\x58\x87\x17\x11\x7e\x0a\xa8\x10\x72\xaf\xab\x71\xd2\xdd\x89\xfe\x7c\x4b\x92\xfe";
const NATIVE_ROLE_MANAGEMENT_HASH_LE: [u8; 20] =
*b"\xe2\x95\xe3\x91\x54\x4c\x17\x8a\xd9\x4f\x03\xec\x4d\xcd\xff\x78\x53\x4e\xcf\x49";
const NATIVE_NOTARY_HASH_LE: [u8; 20] =
*b"\x3b\xec\x35\x31\x11\x9b\xba\xd7\x6d\xd0\x44\x92\x0b\x0d\xe6\xc3\x19\x4f\xe1\xc1";
const NATIVE_TREASURY_HASH_LE: [u8; 20] =
*b"\xc1\x3a\x56\xc9\x83\x53\xa7\xea\x6a\x32\x4d\x9a\x83\x5d\x1b\x5b\xf2\x26\x63\x15";
const NATIVE_LEDGER_HASH_LE: [u8; 20] =
*b"\xbe\xf2\x04\x31\x40\x36\x2a\x77\xc1\x50\x99\xc7\xe6\x4c\x12\xf7\x00\xb6\x65\xda";
const NATIVE_CRYPTOLIB_HASH_LE: [u8; 20] =
*b"\x1b\xf5\x75\xab\x11\x89\x68\x84\x13\x61\x0a\x35\xa1\x28\x86\xcd\xe0\xb6\x6c\x72";
const NATIVE_STDLIB_HASH_LE: [u8; 20] =
*b"\xc0\xef\x39\xce\xe0\xe4\xe9\x25\xc6\xc2\xa0\x6a\x79\xe1\x44\x0d\xd8\x6f\xce\xac";
pub(crate) fn native_contract_hash(contract: ir::NativeContract) -> [u8; 20] {
match contract {
ir::NativeContract::Neo => NATIVE_NEO_HASH_LE,
ir::NativeContract::Gas => NATIVE_GAS_HASH_LE,
ir::NativeContract::ContractManagement => NATIVE_CONTRACT_MANAGEMENT_HASH_LE,
ir::NativeContract::Policy => NATIVE_POLICY_HASH_LE,
ir::NativeContract::Oracle => NATIVE_ORACLE_HASH_LE,
ir::NativeContract::RoleManagement => NATIVE_ROLE_MANAGEMENT_HASH_LE,
ir::NativeContract::Notary => NATIVE_NOTARY_HASH_LE,
ir::NativeContract::Treasury => NATIVE_TREASURY_HASH_LE,
ir::NativeContract::Ledger => NATIVE_LEDGER_HASH_LE,
ir::NativeContract::CryptoLib => NATIVE_CRYPTOLIB_HASH_LE,
ir::NativeContract::StdLib => NATIVE_STDLIB_HASH_LE,
}
}
pub(crate) fn generate_contract_bytecode(
metadata: &mut ContractMetadata,
ir_module: &ir::Module,
verbose: bool,
optimizer_level: u8,
use_callt: bool,
) -> Result<BytecodeBuildOutput, String> {
let function_map: HashMap<_, _> = ir_module
.functions
.iter()
.map(|function| (function.name.as_str(), function))
.collect();
if verbose {
println!(" • Using optimizer level {}", optimizer_level.min(3));
}
let mut bytecode = Vec::new();
let mut call_fixups: Vec<CallPatch> = Vec::new();
let mut token_fixups: Vec<MethodTokenPatch> = Vec::new();
metadata.methods.sort_by_key(|m| match m.visibility {
VisibilityKind::Public | VisibilityKind::External => 0,
VisibilityKind::Internal | VisibilityKind::Private => 1,
});
for method in metadata.methods.iter_mut() {
if matches!(method.kind, FunctionKind::Constructor) && method.body.is_none() {
continue;
}
let method_name = method.neo_name.clone();
let ir_function = function_map.get(method_name.as_str()).copied().ok_or_else(|| {
format!("internal compiler error: missing IR for method '{method_name}'")
})?;
if ir_function.local_count > u8::MAX as u16 {
return Err(format!(
"function '{method_name}' requires {} local slots, exceeding NeoVM's 255-slot limit; \
reduce local variables (e.g. split the function or reuse temporaries)",
ir_function.local_count
));
}
let instruction_count: usize = ir_function
.basic_blocks
.iter()
.map(|block| block.instructions.len())
.sum();
let offset = bytecode.len() as u32;
method.offset = offset;
if verbose {
println!(
" • Emitting method '{method_name}' at offset {offset} ({instruction_count} IR instruction(s))"
);
}
let (function_bytes, patches, token_patches) =
emit_ir_function(ir_function, ir_module, method, use_callt)?;
let base_position = bytecode.len();
bytecode.extend_from_slice(&function_bytes);
for patch in patches {
call_fixups.push(CallPatch {
position: base_position + patch.position,
target: patch.target,
kind: patch.kind,
});
}
for patch in token_patches {
token_fixups.push(MethodTokenPatch {
position: base_position + patch.position,
token: patch.token,
});
}
}
let offset_map: HashMap<String, u32> = metadata
.methods
.iter()
.map(|method| (method.neo_name.clone(), method.offset))
.collect();
for fixup in call_fixups {
if let Some(target_offset) = offset_map.get(&fixup.target) {
let bytes = match fixup.kind {
CallPatchKind::CallRelative => {
let opcode_pos = fixup.position.saturating_sub(1) as i32;
let relative = (*target_offset as i32)
.checked_sub(opcode_pos)
.unwrap_or(0);
relative.to_le_bytes()
}
CallPatchKind::AbsoluteOffset => {
let opcode_pos = fixup.position.saturating_sub(1) as i32;
let relative = (*target_offset as i32)
.checked_sub(opcode_pos)
.unwrap_or(0);
relative.to_le_bytes()
}
};
if fixup.position + 4 <= bytecode.len() {
bytecode[fixup.position..fixup.position + 4].copy_from_slice(&bytes);
}
} else {
return Err(format!(
"bytecode emission: unresolved call target '{}' (method offset unavailable — the callee may be external and unlinked, or the IR call graph is inconsistent)",
fixup.target
));
}
}
if bytecode.is_empty() {
bytecode.push(0x40); }
let tokens = if use_callt {
apply_method_tokens(bytecode.as_mut_slice(), &token_fixups)?
} else {
Vec::new()
};
Ok(BytecodeBuildOutput {
script: bytecode,
tokens,
})
}
fn apply_method_tokens(
bytecode: &mut [u8],
patches: &[MethodTokenPatch],
) -> Result<Vec<neo_devpack_solidity::neo::MethodToken>, String> {
use std::collections::{BTreeMap, BTreeSet};
if patches.is_empty() {
return Ok(Vec::new());
}
let unique: BTreeSet<MethodTokenKey> = patches.iter().map(|p| p.token.clone()).collect();
if unique.len() > neo_devpack_solidity::neo::MAX_METHOD_TOKENS {
return Err(format!(
"CALLT requires <= {} method token(s); got {}",
neo_devpack_solidity::neo::MAX_METHOD_TOKENS,
unique.len()
));
}
let mut token_map: BTreeMap<MethodTokenKey, u16> = BTreeMap::new();
let mut tokens: Vec<neo_devpack_solidity::neo::MethodToken> = Vec::with_capacity(unique.len());
for (index, key) in unique.into_iter().enumerate() {
let token_index = u16::try_from(index).map_err(|_| {
format!(
"bytecode emission: method-token index {index} exceeds u16 range (MAX_METHOD_TOKENS cap exceeded)"
)
})?;
token_map.insert(key.clone(), token_index);
tokens.push(neo_devpack_solidity::neo::MethodToken::new(
key.hash,
&key.method,
key.parameters_count,
key.has_return_value,
key.call_flags,
));
}
for patch in patches {
let Some(index) = token_map.get(&patch.token) else {
return Err(format!(
"bytecode emission: CALLT patch references an unregistered method token (hash={:?}, method='{}')",
patch.token.hash, patch.token.method
));
};
let start = patch.position;
let end = start + 2;
if end > bytecode.len() {
return Err(format!(
"bytecode emission: CALLT patch position {start} out of range (bytecode len {})",
bytecode.len()
));
}
bytecode[start..end].copy_from_slice(&index.to_le_bytes());
}
Ok(tokens)
}