I’ve been working on the ICP<>Sui integration the goal is to deliver and store data on Sui using an ICP canister written in Rust.
I’ve been developing a canister on the ICP that needs to sign and submit transactions to the Sui blockchain. While the canister successfully performs read operations (like getting balances, fetching objects, and dry-running transfers), I’m hitting a error with actual transaction submissions due to signature verification errors.
(
variant {
Err = "Transaction execution error: {\\"code\\":-32002,\\"message\\":\\"Invalid user signature: Required Signature from 0x9757c4d17ef230642cb3b723f6ba82882e219a1b9806f4fadc45eb2d54126b86 is absent [\\\\\\"0x5c085c98551acb429fb994b265e92cb2d0f4ee2f9f3728e376ee710dbc13cc5e\\\\\\"]\\"}"
},
)
i’ve tried multiple approaches:
- diff hashing algo (SHA, Blake2b, etc.)
- diff signature schemes (ECDSA/secp256k1, Ed25519, Schnorr)
- Creating Fresh Addresses
Despite all of this, the signed transaction continues to get rejected by Sui. Even they use the same signature type.
my full code
#[ic_cdk::update]
async fn store_data_on_sui(
timestamp: u64,
source: Vec<u8>,
data: Vec<u8>,
) -> Result<SuiTransactionResult, String> {
// config
let (pkg, stat, key, path) = STATE.with(|s| {
let st = s.borrow();
(
st.package_id.clone().ok_or("package unset".to_string()),
st.stats_object_id.clone().ok_or("stats unset".to_string()),
st.key_name.clone().ok_or("key unset".to_string()),
st.derivation_path.clone().ok_or("path unset".to_string()),
)
});
let pkg = pkg?;
let stat = stat?;
let key = key?;
let path = path?;
// Ed25519 public key from Schnorr API
let pubkey_res = schnorr_public_key(&SchnorrPublicKeyArgs {
canister_id: None,
derivation_path: path.clone(),
key_id: SchnorrKeyId {
algorithm: SchnorrAlgorithm::Ed25519,
name: key.clone(),
},
})
.await
.map_err(|e| format!("schnorr_public_key failed: {:?}", e))?;
let pubkey = pubkey_res.public_key;
// 32-byte Sui address (64 hex chars)
let mut hasher = Sha3_256::new();
hasher.update([0x00]); // Ed25519 scheme flag
hasher.update(&pubkey);
let hash = hasher.finalize();
let sui_address = format!("0x{}", hex::encode(&hash));
let move_args = json!([
stat, // Shared stats object ID (32-byte hex string)
timestamp.to_string(), // Plain u64 JSON number
format!("0x{}", hex::encode(&source)),
format!("0x{}", hex::encode(&data))
]);
//unsafe_moveCall JSON-RPC request
let build_tx = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "unsafe_moveCall",
"params": [
sui_address,
pkg,
"storage",
"store_data",
[],
move_args,
"0xd98c70cb706117642fcdb82ba56f93447451ac75ea541b2c36c1a6143edfd00b",
"10000000",
"Commit"
]
});
// Send request
let resp = http_post(SUI_DEVNET, &build_tx).await?;
let json_resp: Value =
serde_json::from_str(&resp).map_err(|e| format!("Invalid JSON builder response: {}", e))?;
ic_cdk::println!("Builder response: {}", resp);
if let Some(err) = json_resp.get("error") {
return Err(format!("Builder error: {}", err));
}
let tx_bytes_b64 = json_resp["result"]["txBytes"]
.as_str()
.ok_or("txBytes missing")?
.to_string();
ic_cdk::println!("TX BYTES_B64: {:?}", tx_bytes_b64);
//Decode and form intent message
let tx_bytes = base64::decode(&tx_bytes_b64).map_err(|e| format!("decode tx: {}", e))?;
let intent = [0, 0, 0];
let mut intent_msg = Vec::with_capacity(intent.len() + tx_bytes.len());
intent_msg.extend_from_slice(&intent);
intent_msg.extend_from_slice(&tx_bytes);
use blake2b_simd::Params;
let digest = Params::new().hash_length(32).hash(&intent_msg);
let digest_bytes = digest.as_bytes();
//sign intent message
let sig_res = sign_with_schnorr(&SignWithSchnorrArgs {
message: digest_bytes.to_vec(),
derivation_path: path.clone(),
key_id: SchnorrKeyId {
algorithm: SchnorrAlgorithm::Ed25519,
name: key.clone(),
},
aux: None,
})
.await
.map_err(|e| format!("sign_with_schnorr failed: {:?}", e))?;
let raw_sig = sig_res.signature;
//Build signature blob
let mut serialized_sig = Vec::new();
serialized_sig.push(0x00);
serialized_sig.extend_from_slice(&raw_sig);
serialized_sig.extend_from_slice(&pubkey);
let signature_b64 = base64::encode(&serialized_sig);
//execute transaction
let exec_tx = json!({
"jsonrpc":"2.0","id":1,"method":"sui_executeTransactionBlock",
"params":[
tx_bytes_b64,
[ signature_b64 ],
{
"showInput": true,
"showRawInput": true,
"showEffects": true,
"showEvents": true,
"showObjectChanges": true,
"showBalanceChanges": true,
"showRawEffects": false
},
"WaitForLocalExecution"
]
});
let exec_resp = http_post(SUI_DEVNET, &exec_tx).await?;
let exec_json: Value = serde_json::from_str(&exec_resp)
.map_err(|e| format!("Invalid JSON exec response: {}", e))?;
if let Some(err) = exec_json.get("error") {
return Err(format!("Execution error: {}", err));
}
let digest = exec_json["result"]["digest"]
.as_str()
.unwrap_or("")
.to_string();
let status = exec_json["result"]["effects"]["status"]["status"]
.as_str()
.unwrap_or("UNKNOWN")
.to_string();
let error_message = if status != "SUCCESS" {
Some(exec_json["result"]["effects"]["status"]["error"].to_string())
} else {
None
};
Ok(SuiTransactionResult {
digest,
status,
error_message,
})
}
Are there known compatibility issues or specific requirements when using IC’s or sui cryptographic functions?
Any insights, suggestions, or solutions would be greatly appreciated.
Thank you!