Integration

Steps for integration

  • Create Sender and Recipient smart contracts adhering to the structure shown below

  • Set mailbox and CCIP read ISM to the ISM address in references in INCO recipient during deployment

  • Now deploy sender contract to any chain of choice and configure the mailbox to the mailbox of that chain and the recipient to the contract you just deployed on INCO

  • Now send the necessary Ciphertext to the read server using the URL and request format below

  • Next send the same calldata to the function which commits Ciphertext hash to inco.

  • Now the relayer will query the ciphertext corresponding to the hash during relay time and finally process both ciphertext and hash by calling the handlewithCiphertext function on the recipient.

Find the addresses for cross chain mailboxes here

Uploading ciphertext to CCIP-read server

The ciphertext needed to be processed on Inco is uploaded to a server offchain using the following request format.

Make sure the Ciphertext is formatted as show in the section below this

curl -X POST https://hyperlane-ccip.vercel.app/token \
     -H "Content-Type: application/json" \
     -d '{"ciphertext": "0x000"}'

The post request returns the Hash of the Ciphertext which has to be dispatched from your contract on any EVM chain to the contract on INCO.

returns hash as response

Response :
{
    hash : "0x6c3bf3dccc39df78b946f2d49077e90fc88a90c7c3f06ab17244a9850d3dc85b"
}

now the hash and corresponding Ciphertext is available in the CCIP read server and can be retrieved by relayer.

Formatting Ciphertext before uploading to the Server.

The fhevmjs library returns the ciphertext in the form of uint8 array. However we will be converting it to bytes and uploading it to the server. use the following format technique to convert the ciphertext to bytes and remove additional padding provided by ethers.

const Ciphertext=fhevmInstance.alice.encrypt8(1);
const bytes_ciphertext=AbiCoder.defaultAbiCoder().encode(["bytes"], [Ciphertext]);
let padded_ciphertext:string="0x" + bytes_ciphertext.slice(130, 33146);
//commit to CCIP-server 
//Dispatch transaction to origin chain

This formatting is important and allows correct dispatch and verification of ciphertext.

Smart Contract Integration

We will have to configure our smart contract logic on both origin chain and Inco to make sure this setup works. The guide and template code for integration is shown below

Origin Chain Contract

The integration to smart contracts on origin chain will be same as dispatch function used in Hyperlane. The origin chain can hold the hash of the ciphertext. while the metadata is made available on Inco for the corresponding hash.

We can also retrieve the hash of the ciphertext from the function for verification purpose. You can refer the following template code.

Sender.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {IMailbox} from "@hyperlane-xyz/core/contracts/interfaces/IMailbox.sol";
import {IPostDispatchHook} from "@hyperlane-xyz/core/contracts/interfaces/hooks/IPostDispatchHook.sol";
import {IInterchainSecurityModule} from "@hyperlane-xyz/core/contracts/interfaces/IInterchainSecurityModule.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";


contract CipherTextCommitment is OwnableUpgradeable{
     IPostDispatchHook public hook;
      address mailbox;
      address recipient;
      mapping (bytes32 =>bytes32) hash;
      uint32 DomainID=9090;
      function setHook(address _hook) public onlyOwner {
        hook = IPostDispatchHook(_hook);
    }
    event hashedCiphertext(bytes32 _hash);
 constructor(address _mailbox,address _recipient) {
        mailbox = _mailbox;
        recipient=_recipient;
    }
    modifier onlyMailbox() {
        require(
            msg.sender == mailbox,
            "Only mailbox can call this function !!!"
        );
        _;
    }
//template code 
  function CommitCiphertextHash(bytes calldata Ciphertext) payable external returns (bytes32) {
    //necessary snippet
    bytes32 _hash = keccak256(Ciphertext);
    hash[_hash] = _hash;

    //necessary snippets
    uint256 quote = IMailbox(mailbox).quoteDispatch(DomainID, addressToBytes32(recipient), abi.encode(_hash));
    IMailbox(mailbox).dispatch{value: quote}(DomainID, addressToBytes32(recipient), abi.encode(_hash));
    emit hashedCiphertext(_hash);
    return _hash;
}

    
      function addressToBytes32(address _addr) internal pure returns (bytes32) {
        return bytes32(uint256(uint160(_addr)));
    }
}

Additional metadata can be sent alongside hash, but hash must be the first variable while encoding according to ABI followed by arbitrary data of your choice.

Inco gentry smart contract

The smart contract on Inco has slight modifications needed to process our message

  1. We will have to define the ISM to be CCIP-read ISM by specifying the address of ISM in the reference section by calling the setInterchainSecurityModule in the contract.

  2. Additional handler function called handleWithCiphertext is called by ISM to pass the Ciphertext

Use the following template code

Recipient.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {IMailbox} from "@hyperlane-xyz/core/contracts/interfaces/IMailbox.sol";
import {IPostDispatchHook} from "@hyperlane-xyz/core/contracts/interfaces/hooks/IPostDispatchHook.sol";
import {IInterchainSecurityModule} from "@hyperlane-xyz/core/contracts/interfaces/IInterchainSecurityModule.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract CipherTextProcessor is OwnableUpgradeable{
    IInterchainSecurityModule public interchainSecurityModule;
        function setInterchainSecurityModule(address _module) public {
        interchainSecurityModule = IInterchainSecurityModule(_module);
    }
    event handled (bytes32 hash);
    address mailbox; // address of mailbox contract
    address public ISM;

    constructor(address _mailbox, address _interchainSecurityModule) {
        mailbox = _mailbox;
        ISM=_interchainSecurityModule;
        //Sets CCIP read ISM as ISM to be used
        setInterchainSecurityModule(_interchainSecurityModule);
        
    }

    // Modifier so that only mailbox can call particular functions
    modifier onlyMailbox() {
        require(
            msg.sender == mailbox,
            "Only mailbox can call this function !!!"
        );
        _;
    }
        modifier onlyISM() {
        require(
            msg.sender == ISM,
            "Only mailbox can call this function !!!"
        );
        _;
    }
 //no-op function but called by mailbox pos verification so keep
  function handle( uint32 _origin,
        bytes32 _sender,
        bytes memory _body)external onlyMailbox {
           (bytes32 committedHash)=  abi.decode(_body, (bytes32));
           emit handled(committedHash);

  }
  //*************************************************************************
  // Necessary function to be implemented with no changes as it is  called by ISM with metadata
  function handleWithCiphertext( uint32 _origin,
        bytes32 _sender,
        bytes memory _message)external onlyISM {
  (bytes memory message,bytes memory ciphertext)=abi.decode(_message,(bytes , bytes));
   (bytes32 committedHash)=  abi.decode(message, (bytes32));
   //rest of function logic 
        }
  //**************************************************************************

}

This will allow the ISM to write the ciphertext and hash on your recipient.

In case the application requires using both Ciphertext services and general message passing, refer the Routing ISM section.

Last updated