ICP canister fails to sign Sui blockchain transactions

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!

2 Likes

Not well versed with SUI but dont think it support schnorr… correct me if I am wrong… Try without schnorr.

Already tried using ECDSA secp256k1 but getting the same error as with the schnorr one.