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.
constencryptAES256= (userAddress, ipfsCid, arweaveId) => {// Grab the secret keyconstkey=process.env.NEXT_PUBLIC_AES_ENCRYPTION_KEY;// Join the user address with the hashesconstdata= userAddress + ipfsCid + arweaveId;// Generate a random iv in hex formatconstiv=CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex);// Encrypt in AES 256 CBCconstencryptedData=CryptoJS.AES.encrypt(data, key, { iv: iv, mode:CryptoJS.mode.CBC, padding:CryptoJS.pad.Pkcs7, });// Turn the encrypted data into a hex stringconstencryptedHex=Buffer.from(encryptedData.toString(),'base64').toString('hex', );// Prepend it with the random iv for use in decryptionreturn 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).
Consider the following code, from the createRequest function in the external adapter ; it is documented so the process is more transparent to follow:
index.js
// 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 optionalconstcustomParams= { promiseAddress:true, userAddress:true, ipfsCid:true, arweaveId:true, encryptedProof:true,};constcreateRequest= (input, callback) => {// The Chainlink Validator helps validate the request dataconstvalidator=newValidator(callback, input, customParams);constjobRunID=validator.validated.id;// The contract address of the promise createdconstpromiseAddress=validator.validated.data.promiseAddress ||'0x0000000000000000000000000000000000000000';// The address of the creator of the promiseconstuserAddress=validator.validated.data.userAddress ||'0x0000000000000000000000000000000000000000';// The IPFS CIDconstipfsCid=validator.validated.data.ipfsCid ||'';// The Arweave ID - if not uploaded to Arweave, it was// supplied with an empty stringconstarweaveId=validator.validated.data.arweaveId ||'';// The hex proofconstencryptedProof=validator.validated.data.encryptedProof ||'';try {// Grab the secret hex keyconstkey=process.env.AES_ENCRYPTION_KEY;// Grab the iv and encrypted data from the encrypted proof// The iv generated was placed as the first 16 bytesconstiv=encryptedProof.slice(0,32);constencryptedData=encryptedProof.slice(32);// Get back the encrypted hex string in base64constencryptedBase64=Buffer.from(encryptedData,'hex').toString('base64', );// 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 1let decryptedString;try {// Decrypt itconstdecryptedData=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 =''; }constexpectedString= (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 3conststorageStatus= decryptedString === expectedString ? (arweaveId ?3:2) :1;// Prepare the responseconstresponse= { 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, }, jobRunID, status:200, };callback(response.status,Requester.success(jobRunID, response)); } catch (err) {console.log(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.