use neo_devpack_solidity::runtime::execution::ExecutionContext;
use neo_devpack_solidity::runtime::RuntimeConfig;
use secp256k1::{Message, Secp256k1, SecretKey};
use sha3::{Digest, Keccak256};
fn syscall_id(name: &str) -> [u8; 4] {
let digest = sha2::Sha256::digest(name.as_bytes());
[digest[0], digest[1], digest[2], digest[3]]
}
fn push_data(script: &mut Vec<u8>, data: &[u8]) {
assert!(
data.len() <= u8::MAX as usize,
"push_data only supports PUSHDATA1 lengths"
);
script.push(0x0C); script.push(data.len() as u8);
script.extend_from_slice(data);
}
const CRYPTOLIB_HASH_LE: [u8; 20] = [
0x1B, 0xF5, 0x75, 0xAB, 0x11, 0x89, 0x68, 0x84, 0x13, 0x61, 0x0A, 0x35, 0xA1, 0x28, 0x86, 0xCD,
0xE0, 0xB6, 0x6C, 0x72,
];
const STDLIB_HASH_LE: [u8; 20] = [
0xC0, 0xEF, 0x39, 0xCE, 0xE0, 0xE4, 0xE9, 0x25, 0xC6, 0xC2, 0xA0, 0x6A, 0x79, 0xE1, 0x44, 0x0D,
0xD8, 0x6F, 0xCE, 0xAC,
];
#[test]
fn keccak256_syscall_hashes_input() {
let call_id = syscall_id("System.Contract.Call");
let mut code = Vec::new();
push_data(&mut code, b"abc");
code.push(0x11); code.push(0xC0); code.push(0x1F); push_data(&mut code, b"keccak256");
push_data(&mut code, &CRYPTOLIB_HASH_LE);
code.push(0x41); code.extend_from_slice(&call_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let ret = ctx.return_data();
let expected = Keccak256::digest(b"abc");
assert_eq!(ret, expected.to_vec());
}
#[test]
fn platform_syscall_returns_neo() {
let mut code = vec![0x41];
code.extend_from_slice(&[178, 121, 252, 246]);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(ctx.return_data(), b"NEO");
}
#[test]
fn deserialize_syscall_is_identity_for_bytes() {
let call_id = syscall_id("System.Contract.Call");
let mut code = vec![
0x57, 0x01, 0x00, ];
push_data(&mut code, b"hi");
code.push(0x11); code.push(0xC0); code.push(0x1F); push_data(&mut code, b"serialize");
push_data(&mut code, &STDLIB_HASH_LE);
code.push(0x41); code.extend_from_slice(&call_id);
code.push(0x70);
code.push(0x68); code.push(0x11); code.push(0xC0); code.push(0x1F); push_data(&mut code, b"deserialize");
push_data(&mut code, &STDLIB_HASH_LE);
code.push(0x41); code.extend_from_slice(&call_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(ctx.return_data(), b"hi");
}
#[test]
fn get_random_returns_deterministic_hash() {
let mut code = vec![0x41];
code.extend_from_slice(&[107, 222, 169, 40]);
code.push(0x40);
let config = RuntimeConfig::default();
let mut ctx = ExecutionContext::new(&config).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let result = ctx.return_data();
assert_eq!(result.len(), 32, "GetRandom should return 32 bytes");
let mut ctx2 = ExecutionContext::new(&config).expect("context init");
ctx2.initialize(&code, &[]).expect("init");
while !ctx2.step().expect("step").halted {}
assert_eq!(
ctx2.return_data(),
result,
"GetRandom should be deterministic"
);
}
#[test]
fn unsupported_syscall_errors() {
let code = [0x41, 0x00, 0x00, 0x00, 0x00];
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
let err = ctx.step().err();
assert!(err.is_some(), "unsupported syscall should return error");
}
#[test]
fn checksig_returns_true() {
let secp = Secp256k1::signing_only();
let sk = SecretKey::from_slice(&[1u8; 32]).expect("sk");
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
let msg = Message::from_slice(&sha2::Sha256::digest([])).expect("msg");
let sig = secp.sign_ecdsa(&msg, &sk);
let sig_bytes = sig.serialize_compact();
let pub_bytes = pk.serialize();
let mut code = vec![0x0C, pub_bytes.len() as u8];
code.extend_from_slice(&pub_bytes);
code.push(0x0C);
code.push(sig_bytes.len() as u8);
code.extend_from_slice(&sig_bytes);
code.extend_from_slice(&[0x41, 86, 231, 179, 39, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let result = ctx.return_data();
assert!(
result == vec![0] || result == vec![1],
"CheckSig should return a boolean"
);
}
#[test]
fn checksig_with_injected_signing_hash_verifies_real_signature() {
let secp = Secp256k1::signing_only();
let sk = SecretKey::from_slice(&[7u8; 32]).expect("sk");
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
let signing_hash: [u8; 32] = {
let mut h = [0u8; 32];
let digest = sha2::Sha256::digest(b"s3-fix-injected-signing-hash");
h.copy_from_slice(&digest);
h
};
let msg = Message::from_slice(&signing_hash).expect("msg");
let sig = secp.sign_ecdsa(&msg, &sk);
let sig_bytes = sig.serialize_compact();
let pub_bytes = pk.serialize();
let mut code = vec![0x0C, pub_bytes.len() as u8];
code.extend_from_slice(&pub_bytes);
code.push(0x0C);
code.push(sig_bytes.len() as u8);
code.extend_from_slice(&sig_bytes);
code.extend_from_slice(&[0x41, 86, 231, 179, 39, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.override_signing_hash(signing_hash);
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(
ctx.return_data(),
vec![1],
"CheckSig with injected signing hash must verify a real signature"
);
}
#[test]
fn checksig_with_injected_signing_hash_rejects_wrong_signature() {
let secp = Secp256k1::signing_only();
let sk = SecretKey::from_slice(&[7u8; 32]).expect("sk");
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
let injected: [u8; 32] = {
let mut h = [0u8; 32];
h.copy_from_slice(&sha2::Sha256::digest(b"injected-hash"));
h
};
let signed_other: [u8; 32] = {
let mut h = [0u8; 32];
h.copy_from_slice(&sha2::Sha256::digest(b"a-different-hash"));
h
};
let msg = Message::from_slice(&signed_other).expect("msg");
let sig = secp.sign_ecdsa(&msg, &sk);
let sig_bytes = sig.serialize_compact();
let pub_bytes = pk.serialize();
let mut code = vec![0x0C, pub_bytes.len() as u8];
code.extend_from_slice(&pub_bytes);
code.push(0x0C);
code.push(sig_bytes.len() as u8);
code.extend_from_slice(&sig_bytes);
code.extend_from_slice(&[0x41, 86, 231, 179, 39, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.override_signing_hash(injected);
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(
ctx.return_data(),
vec![0],
"CheckSig must reject a signature over a different hash"
);
}
#[test]
fn override_signing_hash_is_drained_after_one_execution() {
let secp = Secp256k1::signing_only();
let sk = SecretKey::from_slice(&[7u8; 32]).expect("sk");
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
let injected: [u8; 32] = {
let mut h = [0u8; 32];
h.copy_from_slice(&sha2::Sha256::digest(b"one-shot"));
h
};
let msg = Message::from_slice(&injected).expect("msg");
let sig = secp.sign_ecdsa(&msg, &sk);
let sig_bytes = sig.serialize_compact();
let pub_bytes = pk.serialize();
let mut code = vec![0x0C, pub_bytes.len() as u8];
code.extend_from_slice(&pub_bytes);
code.push(0x0C);
code.push(sig_bytes.len() as u8);
code.extend_from_slice(&sig_bytes);
code.extend_from_slice(&[0x41, 86, 231, 179, 39, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.override_signing_hash(injected);
assert_eq!(ctx.pending_signing_hash(), Some(injected));
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(
ctx.return_data(),
vec![1],
"first run uses the injected hash"
);
assert_eq!(
ctx.pending_signing_hash(),
None,
"override must be drained after one execution"
);
}
fn expected_multisig_hash160(m: u64, pubkeys: &[Vec<u8>]) -> [u8; 20] {
let mut script: Vec<u8> = Vec::new();
append_push_int(&mut script, m);
for pk in pubkeys {
push_data(&mut script, pk);
}
append_push_int(&mut script, pubkeys.len() as u64);
script.push(0x41); script.extend_from_slice(&syscall_id("System.Crypto.CheckMultisig"));
let sha = sha2::Sha256::digest(&script);
use ripemd::Ripemd160;
let h = Ripemd160::digest(sha);
let mut out = [0u8; 20];
out.copy_from_slice(&h);
out
}
fn append_push_int(script: &mut Vec<u8>, value: u64) {
if value <= 0xFF {
script.push(0x00); script.push(value as u8);
} else if value <= 0xFFFF {
script.push(0x01); script.extend_from_slice(&(value as u16).to_le_bytes());
} else if value <= 0xFFFF_FFFF {
script.push(0x02); script.extend_from_slice(&(value as u32).to_le_bytes());
} else {
script.push(0x03); script.extend_from_slice(&value.to_le_bytes());
}
}
#[test]
fn create_multisig_account_matches_verification_script_hash() {
let secp = Secp256k1::signing_only();
let pk1 =
secp256k1::PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[1u8; 32]).unwrap())
.serialize()
.to_vec();
let pk2 =
secp256k1::PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[2u8; 32]).unwrap())
.serialize()
.to_vec();
let m = 1u64;
let mut code: Vec<u8> = Vec::new();
code.push(0x11); push_data(&mut code, &pk1);
push_data(&mut code, &pk2);
code.push(0x12); code.push(0xC0); code.push(0x41); let sid = syscall_id("System.Contract.CreateMultisigAccount");
code.extend_from_slice(&sid);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let expected = expected_multisig_hash160(m, &[pk2.clone(), pk1.clone()]);
assert_eq!(
ctx.return_data(),
expected.to_vec(),
"CreateMultisigAccount must equal RIPEMD160(SHA256(multi-sig verification script))"
);
}
#[test]
fn create_multisig_account_uses_ripemd160_not_sha256_trunc() {
let secp = Secp256k1::signing_only();
let pks: Vec<Vec<u8>> = (1u8..=2)
.map(|i| {
let sk = SecretKey::from_slice(&[i; 32]).expect("sk");
secp256k1::PublicKey::from_secret_key(&secp, &sk)
.serialize()
.to_vec()
})
.collect();
let m = 1u64;
let mut buggy_input = Vec::new();
buggy_input.extend_from_slice(&m.to_le_bytes());
for pk in &pks {
buggy_input.extend_from_slice(pk);
}
let buggy = {
let d = sha2::Sha256::digest(&buggy_input);
let mut out = [0u8; 20];
out.copy_from_slice(&d[..20]);
out
};
let correct = expected_multisig_hash160(m, &pks);
assert_ne!(
buggy, correct,
"correct multisig hash must differ from the SHA256-truncation stub"
);
}
#[test]
fn checkmultisig_returns_boolean_result() {
let secp = Secp256k1::signing_only();
let sk = SecretKey::from_slice(&[1u8; 32]).expect("sk");
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
let msg = Message::from_slice(&sha2::Sha256::digest([])).expect("msg");
let sig = secp.sign_ecdsa(&msg, &sk);
let sig_bytes = sig.serialize_compact();
let pub_bytes = pk.serialize();
let mut code = vec![0x0C, pub_bytes.len() as u8];
code.extend_from_slice(&pub_bytes);
code.push(0x0C);
code.push(sig_bytes.len() as u8);
code.extend_from_slice(&sig_bytes);
code.extend_from_slice(&[0x41, 158, 208, 220, 58, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let result = ctx.return_data();
assert!(
result == vec![0] || result == vec![1],
"CheckMultisig should return a boolean"
);
}
#[test]
fn checkwitness_returns_true() {
let mut code = vec![0x0C, 0x14];
code.extend_from_slice(&[0u8; 20]);
code.extend_from_slice(&[0x41, 248, 39, 236, 140, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.force_default_account_explicit_for_tests();
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(ctx.return_data(), vec![1]);
}
#[test]
fn checkwitness_accepts_array_of_witnesses() {
let mut code = vec![
0x0C, 0x01, 0x01, 0x0C, 0x14,
];
code.extend_from_slice(&[0u8; 20]); code.push(0x12); code.push(0xC0); code.extend_from_slice(&[0x41, 248, 39, 236, 140, 0x40]);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.force_default_account_explicit_for_tests();
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(ctx.return_data(), vec![1]);
}
#[test]
fn get_network_returns_zero() {
let mut code = vec![0x41];
code.extend_from_slice(&[197, 251, 160, 224]);
code.push(0x40);
let config = RuntimeConfig {
network_magic: 0x12345678,
..Default::default()
};
let mut ctx = ExecutionContext::new(&config).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(ctx.return_data(), 0x12345678u64.to_le_bytes());
}
#[test]
fn get_gas_left_reports_remaining() {
let mut code = vec![0x41];
code.extend_from_slice(&[20, 136, 216, 206]);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let remaining = ctx.return_data();
assert!(
remaining.iter().any(|&b| b != 0),
"gas left should be non-zero"
);
}
mod s6_call_flags {
pub const READ_STATES: u8 = 0b0001;
pub const WRITE_STATES: u8 = 0b0010;
pub const ALL: u8 = 0b1111;
}
#[test]
fn s6_getcallflags_defaults_to_all_for_top_level_execution() {
let mut code = vec![0x41];
code.extend_from_slice(&syscall_id("System.Contract.GetCallFlags"));
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
let rd = ctx.return_data();
assert!(
!rd.is_empty() && rd[0] == s6_call_flags::ALL,
"top-level GetCallFlags must be 0x0F (All); got {rd:?}"
);
}
#[test]
fn s6_storage_put_faults_in_readonly_context() {
let get_ctx_id = syscall_id("System.Storage.GetContext");
let put_id = syscall_id("System.Storage.Put");
let mut code = Vec::new();
code.extend_from_slice(&[0x0C, 0x01, 0xAA]);
code.extend_from_slice(&[0x0C, 0x01, 0xBB]);
code.push(0x41);
code.extend_from_slice(&get_ctx_id);
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
ctx.override_call_flags(s6_call_flags::READ_STATES);
ctx.initialize(&code, &[]).expect("init");
let outcome = step_until_halt(&mut ctx);
assert!(
outcome.is_err(),
"Storage.Put in a read-only context (no WriteStates flag) must FAULT; \
instead it succeeded. S6 regression."
);
}
#[test]
fn s6_storage_put_succeeds_with_writestates_flag() {
let get_ctx_id = syscall_id("System.Storage.GetContext");
let put_id = syscall_id("System.Storage.Put");
let mut code = Vec::new();
code.extend_from_slice(&[0x0C, 0x01, 0xAA]);
code.extend_from_slice(&[0x0C, 0x01, 0xBB]);
code.push(0x41);
code.extend_from_slice(&get_ctx_id);
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
ctx.override_call_flags(s6_call_flags::READ_STATES | s6_call_flags::WRITE_STATES);
ctx.initialize(&code, &[]).expect("init");
let outcome = step_until_halt(&mut ctx);
assert!(
outcome.is_ok(),
"Storage.Put with WriteStates flag must succeed; got {:?}",
outcome.err()
);
}
fn step_until_halt(ctx: &mut ExecutionContext) -> Result<(), String> {
loop {
match ctx.step() {
Ok(h) => {
if h.halted {
return Ok(());
}
}
Err(e) => return Err(format!("{e:?}")),
}
}
}