IPFS & Arweave verification

How can we guarantee the integrity of the content attached to a promise?
As described previously, there are two ways of creating a promise: from the App, or from the contract. The former enables the App to generate an encrypted proof, containing both the IPFS CID and the Arweave ID, along with the Ethereum address of the user.
This is meaningful considering that, if the promise was issued from the application, we can vouch for the integrity of the content, as we are confident it has indeed been:
  • caught by our IPFS node, meaning its distribution is being covered by Web3 Storage through the Filecoin network ;
  • sent properly to the Arweave blockchain.
During the testnet phase, we are using a Bundlr devnet node, rather than the mainnet ones.
This means that files are actually never sent to Arweave, and are instead deleted after a week.
Regardless of whether the promise was created from the application or from the contract, users can still contribute to indexing the IPFS directory of a promise (Indexing an IPFS directory). This way, they can ensure that it will be permanently available.

How is the proof encrypted?

Right after sending files to IPFS, and optionally to Arweave, the application takes the returned hashes, along with the user address, and proceeds to encrypting them.
const encryptedProof = encryptAES256(userAddress, ipfsCid, arweaveId);
This encryptAES256 function is entrusted with carrying out this AES 256 encryption, given a secret key shared with the External Adapter. Consider the following, available in the context of the application, in the repository.
const encryptAES256 = (userAddress, ipfsCid, arweaveId) => {
// Grab the secret key
const key = process.env.NEXT_PUBLIC_AES_ENCRYPTION_KEY;
// Join the user address with the hashes
const data = userAddress + ipfsCid + arweaveId;
// Generate a random iv in hex format
const iv = CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex);
// Encrypt in AES 256 CBC
const encryptedData = CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
// Turn the encrypted data into a hex string
const encryptedHex = Buffer.from(encryptedData.toString(), 'base64').toString(
// Prepend it with the random iv for use in decryption
return iv + encryptedHex;
The returned string is then supplied as a parameter to the createPromiseContract function, which makes a request to the external adapter to verify it.

How does the EA verify the proof?

The External Adapter is written as a serverless function. Each time it is triggered with a request, the API server grabs the input parameters, performs the custom computation, and sends back its result (or an error, if anything happens in between).
The process following the request is described in The verification process.
Consider the following code, from the createRequest function in the external adapter ; it is documented so the process is more transparent to follow:
// Custom parameters will be used by the External Adapter
// true: the parameter is required, if not provided it will
// throw an error
// false: the parameter is optional
const customParams = {
promiseAddress: true,
userAddress: true,
ipfsCid: true,
arweaveId: true,
encryptedProof: true,
const createRequest = (input, callback) => {
// The Chainlink Validator helps validate the request data
const validator = new Validator(callback, input, customParams);
const jobRunID =;
// The contract address of the promise created
const promiseAddress = ||
// The address of the creator of the promise
const userAddress = ||
const ipfsCid = || '';
// The Arweave ID - if not uploaded to Arweave, it was
// supplied with an empty string
const arweaveId = || '';
// The hex proof
const encryptedProof = || '';
try {
// Grab the secret hex key
const key = process.env.AES_ENCRYPTION_KEY;
// Grab the iv and encrypted data from the encrypted proof
// The iv generated was placed as the first 16 bytes
const iv = encryptedProof.slice(0, 32);
const encryptedData = encryptedProof.slice(32);
// Get back the encrypted hex string in base64
const encryptedBase64 = Buffer.from(encryptedData, 'hex').toString(
// We're using a nested try/catch here because otherwise the decrypt function
// does sometimes throw an error when the key is wrong
// In this case, we want to return a 200 response with a status of 1
let decryptedString;
try {
// Decrypt it
const decryptedData = CryptoJS.AES.decrypt(encryptedBase64, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
// We must transform to lowercase because the addresses can mismatch if not
decryptedString = decryptedData.toString(CryptoJS.enc.Utf8).toLowerCase();
} catch (err) {
decryptedString = '';
const expectedString = (userAddress + ipfsCid + arweaveId).toLowerCase();
// If the strings don't match, return 1
// If they do and arweaveId is empty, return 2 (not been uploaded to Arweave)
// If they do and an arweaveId is provided, return 3
const storageStatus =
decryptedString === expectedString ? (arweaveId ? 3 : 2) : 1;
// Prepare the response
const response = {
data: {
// Either 1, 2 or 3
result: storageStatus,
// The address of the promise, so when fulfilling
// the request, the PromiseFactory can call it to
// change its storage status
promiseAddress: promiseAddress,
status: 200,
callback(response.status, Requester.success(jobRunID, response));
} catch (err) {
callback(500, Requester.errored(jobRunID, err));
Once the result is returned to the VerifyStorage contract, it can fulfill the request and call the PromiseFactory to update the storage status for this promise.