diff --git a/README.md b/README.md index 24afddda..eb20d0c0 100644 --- a/README.md +++ b/README.md @@ -329,7 +329,13 @@ You can also filter these commitments by variable name. Using the example above, ``` as a GET request to `http://localhost:3000/getCommitmentsByVariableName`. -If the commitment database that stores commitment preimages is lost you can restore the DB (by decrypting the onchain encryptions of the preimages) by sending a POST request to `http://localhost:3000/backupDataRetriever`. +If the commitment database that stores commitment preimages is lost you can restore the DB (by decrypting the onchain encryptions of the preimages) by sending a POST request to `http://localhost:3000/backupDataRetriever`. You can also restore only the commitments with a specific variable name. For example, if you want to restore commitments representing variable `a`, send: +``` +{ + "name": "a" +} +``` +as a POST request to `http://localhost:3000/backupVariable`. #### Using secret states in the constructor diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index 178b8956..81cd1883 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -122,6 +122,18 @@ export async function getCommitmentsByState(name, mappingKey = null) { return commitments; } +// function to delete commitment with a specified stateName +export async function deleteCommitmentsByState(name, mappingKey = null) { + const connection = await mongo.connection(MONGO_URL); + const db = connection.db(COMMITMENTS_DB); + const query = { name: name }; + if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; + const deleteResult = await db + .collection(COMMITMENTS_COLLECTION) + .deleteMany(query); + return deleteResult; +} + // function to retrieve all known nullified commitments export async function getNullifiedCommitments() { const connection = await mongo.connection(MONGO_URL); diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index 8ec3873f..26cbdc32 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -492,6 +492,18 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { }; } + case 'BackupVariableBoilerplate': { + const { + contractName, + privateStates = [], + } = fields; + return { + nodeType, + contractName, + privateStates, + }; + } + default: throw new TypeError(nodeType); } diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index bb076de5..1e642a75 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -798,7 +798,7 @@ integrationApiServicesBoilerplate = { ` }, preStatements(): string{ - return ` import { startEventFilter, getSiblingPath } from './common/timber.mjs';\nimport fs from "fs";\nimport logger from './common/logger.mjs';\nimport { decrypt } from "./common/number-theory.mjs";\nimport { getAllCommitments, getCommitmentsByState, reinstateNullifiers, getBalance, getSharedSecretskeys , getBalanceByState, addConstructorNullifiers } from "./common/commitment-storage.mjs";\nimport { backupDataRetriever } from "./BackupDataRetriever.mjs";\nimport web3 from './common/web3.mjs';\n\n + return ` import { startEventFilter, getSiblingPath } from './common/timber.mjs';\nimport fs from "fs";\nimport logger from './common/logger.mjs';\nimport { decrypt } from "./common/number-theory.mjs";\nimport { getAllCommitments, getCommitmentsByState, reinstateNullifiers, getBalance, getSharedSecretskeys , getBalanceByState, addConstructorNullifiers } from "./common/commitment-storage.mjs";\nimport { backupDataRetriever } from "./BackupDataRetriever.mjs";\nimport { backupVariable } from "./BackupVariable.mjs";\nimport web3 from './common/web3.mjs';\n\n /** NOTE: this is the api service file, if you need to call any function use the correct url and if Your input contract has two functions, add() and minus(). minus() cannot be called before an initial add(). */ @@ -887,6 +887,17 @@ integrationApiServicesBoilerplate = { res.send({ errors: [err.message] }); } } + export async function service_backupVariable(req, res, next) { + try { + const { name } = req.body; + await backupVariable(name); + res.send("Complete"); + await sleep(10); + } catch (err) { + logger.error(err); + res.send({ errors: [err.message] }); + } + } export async function service_getSharedKeys(req, res, next) { try { const { recipientAddress } = req.body; @@ -921,7 +932,7 @@ integrationApiRoutesBoilerplate = { return `router.post('/FUNCTION_NAME', this.serviceMgr.service_FUNCTION_NAME.bind(this.serviceMgr),);` }, commitmentImports(): string { - return `import { service_allCommitments, service_getCommitmentsByState, service_reinstateNullifiers, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, } from "./api_services.mjs";\n`; + return `import { service_allCommitments, service_getCommitmentsByState, service_reinstateNullifiers, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable,} from "./api_services.mjs";\n`; }, commitmentRoutes(): string { return `// commitment getter routes @@ -934,6 +945,7 @@ integrationApiRoutesBoilerplate = { router.post("/getSharedKeys", service_getSharedKeys); // backup route router.post("/backupDataRetriever", service_backupData); + router.post("/backupVariable", service_backupVariable); `; } }; diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 3631b374..6c55321e 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -534,6 +534,187 @@ const prepareStartupScript = (file: localFile, node: any) => { file.file = file.file.replace(/CONSTRUCTOR_CALL/g, constructorCall); } +const prepareBackupVariable = (node: any) => { + // import generic test skeleton + let genericApiServiceFile: any = `/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ + import config from "config"; + import utils from "zkp-utils"; + import GN from "general-number"; + import fs from "fs"; + import mongo from "./common/mongo.mjs"; + + import { + storeCommitment, + markNullified, + deleteCommitmentsByState + } from "./common/commitment-storage.mjs"; + + import { getContractInstance, getContractAddress } from "./common/contract.mjs"; + + import Web3 from "./common/web3.mjs"; + import { + decompressStarlightKey, + compressStarlightKey, + encrypt, + decrypt, + poseidonHash, + scalarMult, + } from "./common/number-theory.mjs"; + + const { generalise } = GN; + const web3 = Web3.connection(); + const keyDb = "/app/orchestration/common/db/key.json"; + const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; + + export async function backupVariable(_name) { + + let requestedName = _name; + + deleteCommitmentsByState(requestedName, null); + + const instance = await getContractInstance("AssignShield"); + + const backDataEvent = await instance.getPastEvents("EncryptedBackupData", { + fromBlock: 0, + toBlock: "latest", + }); + + const keys = JSON.parse( + fs.readFileSync(keyDb, "utf-8", (err) => { + console.log(err); + }) + ); + const secretKey = generalise(keys.secretKey); + const publicKey = generalise(keys.publicKey); + let storedCommitments = []; + for (const log of backDataEvent) { + for (let i = 0; i < log.returnValues.encPreimages.length; i++) { + let cipherText = log.returnValues.encPreimages[i].cipherText; + let ephPublicKey = log.returnValues.encPreimages[i].ephPublicKey; + let varName = log.returnValues.encPreimages[i].varName; + let name = varName.replace(" a", "").replace(" s", "").replace(" u", ""); + if (requestedName !== name) { + continue; + } + let isArray = false; + let isStruct = false; + if (varName.includes(" a")) { + isArray = true; + } else if (varName.includes(" s")) { + isStruct = true; + } + const plainText = decrypt( + cipherText, + secretKey.hex(32), + [ + decompressStarlightKey(generalise(ephPublicKey))[0].hex(32), + decompressStarlightKey(generalise(ephPublicKey))[1].hex(32), + ] + ); + let mappingKey = null; + let stateVarId; + let value; + console.log("Decrypted pre-image of commitment for variable name: " + name + ": "); + let salt = generalise(plainText[0]); + console.log(\`\\tSalt: \${salt.integer}\`); + if (isArray){ + console.log(\`\\tState variable StateVarId: \${plainText[2]}\`); + mappingKey = generalise(plainText[1]); + console.log(\`\\tMapping Key: \${mappingKey.integer}\`); + let reGenStateVarId = generalise( + utils.mimcHash( + [ + generalise(plainText[2]).bigInt, + generalise(plainText[1]).bigInt, + ], + "ALT_BN_254" + ) + ); + stateVarId = reGenStateVarId; + console.log(\`Regenerated StateVarId: \${reGenStateVarId.bigInt}\`); + value = generalise(plainText[3]); + console.log(\`\\tValue: \${value.integer}\`); + } else { + stateVarId = generalise(plainText[1]); + console.log(\`\\tStateVarId: \${plainText[1]}\`); + if (isStruct){ + value = {};`; + + node.privateStates.forEach((stateVar: any) => { + if (stateVar.structProperties){ + let propCount = 2; + genericApiServiceFile += `\nif (stateVarId.integer === '${stateVar.stateVarId}') {` + stateVar.structProperties.forEach((prop: any) => { + genericApiServiceFile += `value.${prop} = plainText[${propCount}];\n`; + propCount++; + }); + genericApiServiceFile += `}\n`; + } + }); + + genericApiServiceFile += `console.log(\`\\tValue: \${value}\`); + } else { + value = generalise(plainText[2]); + console.log(\`\\tValue: \${value.integer}\`); + } + } + let newCommitment; + if (isStruct){ + let hashInput = [BigInt(stateVarId.hex(32))]; + for (let i = 2; i < plainText.length; i++) { + hashInput.push(BigInt(generalise(plainText[i]).hex(32))); + } + hashInput.push(BigInt(publicKey.hex(32))); + hashInput.push(BigInt(salt.hex(32))); + newCommitment = generalise(poseidonHash(hashInput)); + } else { + newCommitment = generalise(poseidonHash([ + BigInt(stateVarId.hex(32)), + BigInt(value.hex(32)), + BigInt(publicKey.hex(32)), + BigInt(salt.hex(32)), + ])); + } + if (!varName.includes(" u")){ + let oldCommitments = storedCommitments.filter( + (element) => + element.stateVarId.integer === stateVarId.integer && + (!mappingKey || element.mappingKey === mappingKey?.integer) + ); + for (const oldCommitment of oldCommitments){ + await markNullified(oldCommitment.hash, secretKey.hex(32)); + let index = storedCommitments.findIndex(element => element === oldCommitment); + if (index !== -1) { + storedCommitments.splice(index, 1); + } + } + } + await storeCommitment({ + hash: newCommitment, + name: name, + mappingKey: mappingKey?.integer, + preimage: { + stateVarId: stateVarId, + value: value, + salt: salt, + publicKey: publicKey, + }, + secretKey: secretKey, + isNullified: false, + }); + storedCommitments.push({stateVarId: stateVarId, hash: newCommitment, mappingKey: mappingKey?.integer}); + } + }; + }`; + + // replace references to contract and functions with ours + let outputApiServiceFile = genericApiServiceFile.replace( + /CONTRACT_NAME/g, + node.contractName, + ); + return outputApiServiceFile; +}; + const prepareBackupDataRetriever = (node: any) => { // import generic test skeleton let genericApiServiceFile: any = `/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ @@ -828,6 +1009,12 @@ export default function fileGenerator(node: any) { const backupDataRetriever = prepareBackupDataRetriever(node); return backupDataRetriever; } + + case 'BackupVariableBoilerplate': { + const backupVariable = prepareBackupVariable(node); + return backupVariable; + } + case 'IntegrationEncryptedListenerBoilerplate': { const encryptedListener = prepareIntegrationEncryptedListener(node); return encryptedListener; diff --git a/src/codeGenerators/orchestration/nodejs/toOrchestration.ts b/src/codeGenerators/orchestration/nodejs/toOrchestration.ts index b0dce722..66cc04a5 100644 --- a/src/codeGenerators/orchestration/nodejs/toOrchestration.ts +++ b/src/codeGenerators/orchestration/nodejs/toOrchestration.ts @@ -321,6 +321,9 @@ export default function codeGenerator(node: any, options: any = {}): any { case 'BackupDataRetrieverBoilerplate': // Separate files are handled by the fileGenerator return fileGenerator(node); + case 'BackupVariableBoilerplate': + // Separate files are handled by the fileGenerator + return fileGenerator(node); case 'IntegrationEncryptedListenerBoilerplate': return fileGenerator(node); case 'InitialisePreimage': diff --git a/src/transformers/visitors/toOrchestrationVisitor.ts b/src/transformers/visitors/toOrchestrationVisitor.ts index 76deb3d4..7847e670 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -423,6 +423,17 @@ const visitor = { ], }); node._newASTPointer.push(newNode); + newNode = buildNode('File', { + fileName: 'BackupVariable', + fileExtension: '.mjs', + nodes: [ + buildNode('BackupVariableBoilerplate', { + contractName, + privateStates: [], + }), + ], + }); + node._newASTPointer.push(newNode); if (scope.indicators.encryptionRequired) { newNode = buildNode('File', { fileName: 'encrypted-data-listener', @@ -859,6 +870,16 @@ const visitor = { file.nodes?.[0].privateStates.push(newNode); } } + if (file.nodes?.[0].nodeType === 'BackupVariableBoilerplate') { + let newNode = buildPrivateStateNode('EncryptBackupPreimage', { + privateStateName: name, + id, + indicator: stateVarIndicator, + }); + if (!file.nodes?.[0].privateStates.some((n: any) => n.stateVarId === newNode.stateVarId)){ + file.nodes?.[0].privateStates.push(newNode); + } + } } } diff --git a/src/types/orchestration-types.ts b/src/types/orchestration-types.ts index 35c44ae8..cf148529 100644 --- a/src/types/orchestration-types.ts +++ b/src/types/orchestration-types.ts @@ -290,6 +290,7 @@ export default function buildNode(nodeType: string, fields: any = {}): any { case 'IntegrationApiServicesBoilerplate': case 'IntegrationApiRoutesBoilerplate': case 'BackupDataRetrieverBoilerplate': + case 'BackupVariableBoilerplate': case 'IntegrationEncryptedListenerBoilerplate': case 'IntegrationTestFunction': case 'IntegrationApiServiceFunction':