jest.mock('child_process'); jest.mock('find-up'); jest.mock('glob'); jest.mock('snarkjs'); const { exec } = require('child_process'); const findUp = require('find-up'); const path = require('path'); const somePath = path.normalize('/some/path'); findUp.sync.mockImplementation(() => somePath); const glob = require('glob'); const snarkjs = require('snarkjs'); const someString = 'someString'; snarkjs.bigInt = class { toString() { return someString; } }; describe('embark-snark', () => { let Snarks, plugin; describe('side effects', () => { describe('bigInt patching', () => { it("should patch the prototype of snarkjs' bigInt constructor with a toJSON method", () => { expect(snarkjs.bigInt.prototype.toJSON).toBeUndefined(); ({ Snarks, default: plugin } = require('../src/index.js')); expect(new snarkjs.bigInt().toJSON()).toBe(someString); }); }); }); describe('plugin', () => { let dappPath, embark; beforeAll(() => { dappPath = jest.fn(() => {}); embark = { config: { dappPath } }; }); it('should call the implementation (Snarks class) with its argument', () => { plugin(embark); expect(dappPath).toHaveBeenCalled(); }); }); describe('Snarks class', () => { describe('static properties', () => { it('should have the expected static properties', () => { expect(Snarks.circomBinary).toBe(somePath); expect(Snarks.snarkjsBinary).toBe(somePath); }); }); describe('constructor', () => { let dappPath, fs, logger, pluginConfig, embark, embarkNoPluginConfig; beforeAll(() => { dappPath = jest.fn(() => somePath); fs = {}; logger = {}; pluginConfig = {}; embark = { config: { dappPath }, fs, logger, pluginConfig }; embarkNoPluginConfig = { config: { dappPath }, fs, logger }; jest.spyOn(Snarks.prototype, 'registerEvents'); Snarks.prototype.registerEvents.mockImplementation(() => {}); }); afterAll(() => { Snarks.prototype.registerEvents.mockRestore(); }); it('should setup the expected instance properties', () => { const snarks = new Snarks(embark); expect(snarks.embark).toBe(embark); expect(snarks.circuitsConfig).toBe(pluginConfig); expect(snarks.compiledCircuitsPath).toBe(somePath); expect(snarks.fs).toBe(fs); expect(snarks.logger).toBe(logger); }); it('should call Snarks#registerEvents only when there is a plugin config', () => { let snarks = new Snarks(embark); expect(snarks.registerEvents).toHaveBeenCalled(); Snarks.prototype.registerEvents.mockClear(); snarks = new Snarks(embarkNoPluginConfig); expect(snarks.registerEvents).not.toHaveBeenCalled(); }); }); describe('instance methods', () => { let errorLogger, infoLogger, snarks; beforeAll(() => { errorLogger = jest.fn(() => {}); infoLogger = jest.fn(() => {}); }); beforeEach(() => { snarks = Object.create(Snarks.prototype); snarks.compiledCircuitsPath = somePath; snarks.logger = { error: errorLogger, info: infoLogger }; }); describe('registerEvents', () => { let bind, bound, registerActionForEvent; beforeAll(() => { bound = () => {}; bind = jest.fn(() => bound); registerActionForEvent = jest.fn(() => {}); }); beforeEach(() => { snarks.compileAndGenerateContracts = { bind }; snarks.embark = { registerActionForEvent }; }); it("should call embark's registerActionForEvent with instance-bound Snarks#compileAndGenerateContracts", () => { snarks.registerEvents(); expect(registerActionForEvent).toHaveBeenCalledWith( expect.any(String), bound ); expect(bind).toHaveBeenCalledWith(snarks); }); }); describe('verifierFilepath', () => { it('should combine a path, basename, and extension', () => { const basename = 'foo'; const combined = snarks.verifierFilepath(basename); expect(combined).toBe( path.join(somePath, `${basename}${Snarks.Extensions.VkVerifier}`) ); }); }); describe('verifierContractPath', () => { it('should combine a path, basename, and extension', () => { const basename = 'foo'; const combined = snarks.verifierContractPath(basename); expect(combined).toBe( path.join(somePath, `${basename}${Snarks.Extensions.Solidity}`) ); }); }); describe('proofFilepath', () => { it('should combine a path, basename, and extension', () => { const basename = 'foo'; const combined = snarks.proofFilepath(basename); expect(combined).toBe( path.join(somePath, `${basename}${Snarks.Extensions.VkProof}`) ); }); }); describe('compileCircuits', () => { let bar, foo, ensureDir; beforeAll(() => { foo = `foo${Snarks.Extensions.Circom}`; bar = `bar${Snarks.Extensions.Circom}`; ensureDir = jest.fn(() => {}); glob.mockImplementation((_, cb) => cb(null, [foo, 'A.aaa', bar, 'B.bbb']) ); }); beforeEach(() => { snarks.compileCircuit = jest.fn(() => {}); snarks.fs = { ensureDir }; }); afterAll(() => { glob.mockReset(); }); it('should not compile if no matching circuits are found', async () => { const circuits = []; snarks.circuitsConfig = { circuits }; await snarks.compileCircuits(); expect(snarks.compileCircuit).not.toHaveBeenCalled(); }); it('should compile the configured circuits', async () => { const circuits = ['whatever']; snarks.circuitsConfig = { circuits }; await snarks.compileCircuits(); expect(snarks.compileCircuit).toHaveBeenNthCalledWith(1, foo); expect(snarks.compileCircuit).toHaveBeenNthCalledWith(2, bar); expect(snarks.compileCircuit).toHaveBeenCalledTimes(2); }); }); describe('compileCircuit', () => { let basename, filepath; beforeAll(() => { basename = 'foo'; filepath = path.join( somePath, `${basename}${Snarks.Extensions.Circom}` ); }); afterEach(() => { exec.mockReset(); }); it('should compile the ciruict', async () => { exec.mockImplementation((_, cb) => cb()); await snarks.compileCircuit(filepath); expect(exec).toHaveBeenCalledWith( expect.stringContaining(filepath), expect.any(Function) ); expect(exec).toHaveBeenCalledWith( expect.stringContaining( path.join(somePath, `${basename}${Snarks.Extensions.Json}`) ), expect.any(Function) ); }); it('should reject if the compiler has an error', async () => { const message = 'error'; exec.mockImplementation((_, cb) => cb(new Error(message))); await expect(snarks.compileCircuit(filepath)).rejects.toThrow( message ); }); }); describe('generateProofs', () => { let bar, foo, readdir; beforeAll(() => { foo = `foo${Snarks.Extensions.Json}`; bar = `bar${Snarks.Extensions.Json}`; readdir = jest.fn(async () => [foo, 'A.aaa', bar, 'B.bbb']); }); beforeEach(() => { snarks.generateProof = jest.fn(() => {}); snarks.fs = { readdir }; }); it('should generate proofs for the compiled circuits', async () => { await snarks.generateProofs(); expect(snarks.generateProof).toHaveBeenNthCalledWith(1, foo); expect(snarks.generateProof).toHaveBeenNthCalledWith(2, bar); expect(snarks.generateProof).toHaveBeenCalledTimes(2); }); }); describe('generateProof', () => { let basename, calculateWitness, filename, foo, inputs, proof, publicSignals, setup, vk_verifier; beforeAll(() => { basename = 'foo'; calculateWitness = jest.fn(() => {}); filename = `${basename}${Snarks.Extensions.Json}`; foo = {}; inputs = { foo }; proof = {}; publicSignals = {}; vk_verifier = {}; setup = { vk_verifier }; snarkjs.original.genProof.mockImplementation(() => ({ proof, publicSignals })); }); beforeEach(() => { snarks.circuitsConfig = { inputs }; snarks.getCircuit = jest.fn(() => ({ calculateWitness })); snarks.generateSetup = jest.fn(() => setup); snarks.generateVerifier = jest.fn(() => {}); }); afterEach(() => { snarkjs.original.isValid.mockReset(); }); afterAll(() => { snarkjs.original.genProof.mockReset(); }); it('should not generate if there is no matching input', async () => { snarks.circuitsConfig = { inputs: {} }; await snarks.generateProof(filename); expect(snarkjs.original.isValid).not.toHaveBeenCalled(); }); it('should generate the proof for a circuit', async () => { snarkjs.original.isValid.mockImplementation(() => true); await snarks.generateProof(filename); expect(snarkjs.original.isValid).toHaveBeenCalledWith( vk_verifier, proof, publicSignals ); expect(snarks.generateVerifier).toHaveBeenCalledWith( path.basename(filename, Snarks.Extensions.Json) ); }); it('should reject if the proof is not valid', async () => { snarkjs.original.isValid.mockImplementation(() => false); await expect(snarks.generateProof(filename)).rejects.toThrow( /^The proof is not valid/ ); expect(snarks.generateVerifier).not.toHaveBeenCalled(); }); }); describe('getCircuit', () => { let circuit, filepath, readFile; beforeAll(() => { circuit = {}; filepath = 'whatever'; readFile = jest.fn(async () => '{}'); snarkjs.Circuit.mockImplementation(function() { return circuit; }); }); beforeEach(() => { snarks.fs = { readFile }; }); afterAll(() => { snarkjs.Circuit.mockReset(); }); it("should return a circuit object given a compiled-circuit's path", async () => { await expect(snarks.getCircuit(filepath)).resolves.toBe(circuit); }); }); describe('generateSetup', () => { let basename, circuit, filepath, setup, vk_proof, vk_verifier, writeFile; beforeAll(() => { basename = 'foo'; circuit = {}; filepath = path.join(somePath, basename); vk_proof = {}; vk_verifier = {}; setup = { vk_proof, vk_verifier }; writeFile = jest.fn(async () => {}); snarkjs.original.setup.mockImplementation(() => setup); }); beforeEach(() => { snarks.fs = { writeFile }; snarks.proofFilepath = jest.fn(() => filepath); snarks.verifierFilepath = jest.fn(() => filepath); }); afterAll(() => { snarkjs.original.setup.mockReset(); }); it('should write proof and verifier files for a circuit', async () => { await snarks.generateSetup(circuit, basename); expect(writeFile).toHaveBeenNthCalledWith(1, filepath, '{}', 'utf8'); expect(writeFile).toHaveBeenNthCalledWith(2, filepath, '{}', 'utf8'); expect(writeFile).toHaveBeenCalledTimes(2); }); it('should return the snarkjs setup', async () => { await expect(snarks.generateSetup(circuit, basename)).resolves.toBe( setup ); }); }); describe('generateVerifier', () => { let basename, contractPath, vkVerifierPath; beforeAll(() => { basename = 'foo'; contractPath = path.join( somePath, `${basename}${Snarks.Extensions.Solidity}` ); vkVerifierPath = path.join( somePath, `${basename}${Snarks.Extensions.VkVerifier}` ); }); beforeEach(() => { snarks.verifierFilepath = jest.fn(() => vkVerifierPath); snarks.verifierContractPath = jest.fn(() => contractPath); }); afterEach(() => { exec.mockReset(); }); it('should generate the verifier-contracts', async () => { exec.mockImplementation((_, cb) => cb()); await snarks.generateVerifier(basename); expect(exec).toHaveBeenCalledWith( expect.stringContaining(vkVerifierPath), expect.any(Function) ); expect(exec).toHaveBeenCalledWith( expect.stringContaining(contractPath), expect.any(Function) ); }); it('should reject if there is an error during generation', async () => { const message = 'error'; exec.mockImplementation((_, cb) => cb(new Error(message))); await expect(snarks.generateVerifier(basename)).rejects.toThrow( message ); }); }); describe('addVerifiersToContracts', () => { let bar, contractFiles, foo, readdir; beforeAll(() => { foo = `foo${Snarks.Extensions.Solidity}`; bar = `bar${Snarks.Extensions.Solidity}`; contractFiles = []; readdir = jest.fn(async () => [foo, 'A.aaa', bar, 'B.bbb']); }); beforeEach(() => { snarks.addVerifierToContracts = jest.fn(() => {}); snarks.fs = { readdir }; }); it('should add all verifier-contracts', async () => { await snarks.addVerifiersToContracts(contractFiles); expect(snarks.addVerifierToContracts).toHaveBeenNthCalledWith( 1, foo, contractFiles ); expect(snarks.addVerifierToContracts).toHaveBeenNthCalledWith( 2, bar, contractFiles ); expect(snarks.addVerifierToContracts).toHaveBeenCalledTimes(2); }); }); describe('addVerifierToContracts', () => { let contractFiles, events, existingContract, filename, filepath, request; beforeAll(() => { existingContract = { path: path.join('/path/to', `whatever${Snarks.Extensions.Solidity}`) }; contractFiles = [existingContract]; filename = `foo${Snarks.Extensions.Solidity}`; filepath = path.join(somePath, filename); request = jest.fn(() => {}); events = { request }; }); beforeEach(() => { snarks.embark = { events }; }); it('should add a verifier-contract to the array of contract files', () => { snarks.addVerifierToContracts(filename, contractFiles); expect(contractFiles).toEqual([existingContract, { path: filepath }]); }); }); describe('compileAndGenerateContracts', () => { let callback, contractFiles, error; beforeAll(() => { callback = jest.fn(() => {}); contractFiles = []; error = new Error('error'); }); beforeEach(() => { snarks.compileCircuits = jest.fn(() => {}); snarks.generateProofs = jest.fn(() => {}); snarks.addVerifiersToContracts = jest.fn(() => {}); }); it('should call back without error and with the array of contract files after successfully compiling and generating', async () => { await snarks.compileAndGenerateContracts(contractFiles, callback); expect(callback).toHaveBeenCalledWith(null, contractFiles); }); it('should call back with an error if a step fails', async () => { snarks.generateProofs = jest.fn(() => { throw error; }); await snarks.compileAndGenerateContracts(contractFiles, callback); expect(callback).toHaveBeenCalledWith(error); }); }); }); }); });