Skip to content

Commit

Permalink
Verification with Standard JSON Input (#980)
Browse files Browse the repository at this point in the history
* Refactor UI for shared SerchSelect,init json endp.

Refactor SearchSelect's onChange function and its types to be able to
reuse SearchSelect consistently.

Initial naive implementation of using solcJson as input in session

* Don't use deprecated btoa function

* Don't overflow Error message UI

* Use nightlies toggle, style json input

* Don't validate solc JSON file extension

To allow multipart/form-data requests

* Add solc-json session tests

* Change import solc json icon

* Solc-json non-session endpoint, tests

* Fix build errors, show json input errors

* Fix compiler version test

* Remove "simulation" variables from solcjson endpts

* Fix copy-paste typo in endpoint definition

* Check solc-json language and contracts output
  • Loading branch information
kuzdogan authored Apr 26, 2023
1 parent 3028f63 commit 79d05f7
Show file tree
Hide file tree
Showing 16 changed files with 631 additions and 139 deletions.
42 changes: 41 additions & 1 deletion packages/lib-sourcify/src/lib/solidityCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from 'fs';
import { spawnSync } from 'child_process';
import { fetchWithTimeout } from './utils';
import { StatusCodes } from 'http-status-codes';
import { JsonInput } from './types';
import { JsonInput, PathBuffer } from './types';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const solc = require('solc');

Expand Down Expand Up @@ -100,6 +100,46 @@ export async function useCompiler(version: string, solcJsonInput: JsonInput) {
return compiledJSON;
}

export async function getAllMetadataAndSourcesFromSolcJson(
solcJson: JsonInput,
compilerVersion: string
): Promise<PathBuffer[]> {
if (solcJson.language !== 'Solidity')
throw new Error(
'Only Solidity is supported, the json has language: ' + solcJson.language
);
const outputSelection = {
'*': {
'*': ['metadata'],
},
};
if (!solcJson.settings) {
solcJson.settings = {
outputSelection: outputSelection,
};
}
solcJson.settings.outputSelection = outputSelection;
const compiled = await useCompiler(compilerVersion, solcJson);
const metadataAndSources: PathBuffer[] = [];
if (!compiled.contracts)
throw new Error('No contracts found in the compiled json output');
for (const contractPath in compiled.contracts) {
for (const contract in compiled.contracts[contractPath]) {
const metadata = compiled.contracts[contractPath][contract].metadata;
const metadataPath = `${contractPath}-metadata.json`;
metadataAndSources.push({
path: metadataPath,
buffer: Buffer.from(metadata),
});
metadataAndSources.push({
path: `${contractPath}`,
buffer: Buffer.from(solcJson.sources[contractPath].content as string),
});
}
}
return metadataAndSources;
}

// TODO: Handle where and how solc is saved
export async function getSolcExecutable(
platform: string,
Expand Down
153 changes: 153 additions & 0 deletions src/server/controllers/VerificationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getIpfsGateway,
performFetch,
verifyCreate2,
getAllMetadataAndSourcesFromSolcJson,
} from "@ethereum-sourcify/lib-sourcify";
import { decode as bytecodeDecode } from "@ethereum-sourcify/bytecode-utils";
import VerificationService from "../services/VerificationService";
Expand Down Expand Up @@ -197,6 +198,109 @@ export default class VerificationController
res.send(getSessionJSON(session));
};

private addInputSolcJsonEndpoint = async (req: Request, res: Response) => {
validateRequest(req);
const inputFiles = extractFiles(req, true);
if (!inputFiles)
throw new ValidationError([{ param: "files", msg: "No files found" }]);

const compilerVersion = req.body.compilerVersion;

for (const inputFile of inputFiles) {
let solcJson;
try {
solcJson = JSON.parse(inputFile.buffer.toString());
} catch (error: any) {
throw new BadRequestError(
`Couldn't parse JSON ${inputFile.path}. Make sure the contents of the file are syntaxed correctly.`
);
}

const metadataAndSources = await getAllMetadataAndSourcesFromSolcJson(
solcJson,
compilerVersion
);
const metadataAndSourcesPathContents: PathContent[] =
metadataAndSources.map((pb) => {
return { path: pb.path, content: pb.buffer.toString(FILE_ENCODING) };
});

const session = req.session;
const newFilesCount = saveFiles(metadataAndSourcesPathContents, session);
if (newFilesCount) {
await checkContractsInSession(session);
}
res.send(getSessionJSON(session));
}
};

private verifySolcJsonEndpoint = async (req: Request, res: Response) => {
validateRequest(req);
const inputFiles = extractFiles(req, true);
if (!inputFiles)
throw new ValidationError([{ param: "files", msg: "No files found" }]);
if (inputFiles.length !== 1)
throw new BadRequestError(
"Only one Solidity JSON Input file at a time is allowed"
);

let solcJson;
try {
solcJson = JSON.parse(inputFiles[0].buffer.toString());
} catch (error: any) {
throw new BadRequestError(
`Couldn't parse JSON ${inputFiles[0].path}. Make sure the contents of the file are syntaxed correctly.`
);
}
const compilerVersion = req.body.compilerVersion;
const contractName = req.body.contractName;
const chain = req.body.chain;
const address = req.body.address;

const metadataAndSourcesPathBuffers =
await getAllMetadataAndSourcesFromSolcJson(solcJson, compilerVersion);

const checkedContracts = await checkFiles(metadataAndSourcesPathBuffers);
const contractToVerify = checkedContracts.find(
(c) => c.name === contractName
);
if (!contractToVerify) {
throw new BadRequestError(
`Couldn't find contract ${contractName} in the provided Solidity JSON Input file.`
);
}

const match = await this.verificationService.verifyDeployed(
contractToVerify,
chain,
address,
// req.body.contextVariables,
req.body.creatorTxHash
);
// Send to verification again with all source files.
if (match.status === "extra-file-input-bug") {
const contractWithAllSources = await useAllSources(
contractToVerify,
metadataAndSourcesPathBuffers
);
const tempMatch = await this.verificationService.verifyDeployed(
contractWithAllSources,
chain,
address, // Due to the old API taking an array of addresses.
// req.body.contextVariables,
req.body.creatorTxHash
);
if (tempMatch.status === "perfect") {
await this.repositoryService.storeMatch(contractToVerify, tempMatch);
return res.send({ result: [tempMatch] });
}
}
if (match.status) {
await this.repositoryService.storeMatch(contractToVerify, match);
}
return res.send({ result: [match] }); // array is an old expected behavior (e.g. by frontend)
};

private restartSessionEndpoint = async (req: Request, res: Response) => {
req.session.destroy((error: Error) => {
let msg = "";
Expand Down Expand Up @@ -588,6 +692,48 @@ export default class VerificationController
this.safeHandler(this.legacyVerifyEndpoint)
);

this.router.route(["/verify/solc-json"]).post(
body("address")
.exists()
.bail()
.custom(
(address, { req }) => (req.addresses = validateAddresses(address))
),
body("chain")
.exists()
.bail()
.custom((chain, { req }) => (req.chain = checkChainId(chain))),
body("compilerVersion").exists().bail(),
body("contractName").exists().bail(),
// body("contextVariables.msgSender").optional(),
// body("contextVariables.abiEncodedConstructorArguments").optional(),
// Handle non-json multipart/form-data requests.
// body("abiEncodedConstructorArguments")
// .optional()
// .custom(
// (abiEncodedConstructorArguments, { req }) =>
// (req.body.contextVariables = {
// abiEncodedConstructorArguments,
// ...req.body.contextVariables,
// })
// ),
// body("msgSender")
// .optional()
// .custom(
// (msgSender, { req }) =>
// (req.body.contextVariables = {
// msgSender,
// ...req.body.contextVariables,
// })
// ),
body("creatorTxHash")
.optional()
.custom(
(creatorTxHash, { req }) => (req.body.creatorTxHash = creatorTxHash)
),
this.safeHandler(this.verifySolcJsonEndpoint)
);

// Session APIs with session cookies require non "*" CORS
this.router
.route(["/session-data", "/session/data"])
Expand All @@ -597,6 +743,13 @@ export default class VerificationController
.route(["/input-files", "/session/input-files"])
.post(this.safeHandler(this.addInputFilesEndpoint));

this.router
.route(["/session/input-solc-json"])
.post(
body("compilerVersion").exists().bail(),
this.safeHandler(this.addInputSolcJsonEndpoint)
);

this.router
.route(["/session/input-contract"])
.post(this.safeHandler(this.addInputContractEndpoint));
Expand Down
128 changes: 128 additions & 0 deletions test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,84 @@ describe("Server", function () {
chai.expect(isExist, "Immutable references not saved").to.be.true;
});

it("should return validation error for adding standard input JSON without a compiler version", async () => {
const address = await deployFromAbiAndBytecode(
localWeb3Provider,
artifact.abi, // Storage.sol
artifact.bytecode,
accounts[0]
);
const solcJsonPath = path.join(
"test",
"testcontracts",
"Storage",
"StorageJsonInput.json"
);
const solcJsonBuffer = fs.readFileSync(solcJsonPath);

const res = await chai
.request(server.app)
.post("/verify/solc-json")
.attach("files", solcJsonBuffer)
.field("address", address)
.field("chain", defaultContractChain)
.field("contractName", "Storage");

assertValidationError(null, res, "compilerVersion");
});

it("should return validation error for adding standard input JSON without a contract name", async () => {
const address = await deployFromAbiAndBytecode(
localWeb3Provider,
artifact.abi, // Storage.sol
artifact.bytecode,
accounts[0]
);
const solcJsonPath = path.join(
"test",
"testcontracts",
"Storage",
"StorageJsonInput.json"
);
const solcJsonBuffer = fs.readFileSync(solcJsonPath);

const res = await chai
.request(server.app)
.post("/verify/solc-json")
.attach("files", solcJsonBuffer)
.field("address", address)
.field("chain", defaultContractChain)
.field("compilerVersion", "0.8.4+commit.c7e474f2");

assertValidationError(null, res, "contractName");
});

it("should verify a contract with Solidity standard input JSON", async () => {
const address = await deployFromAbiAndBytecode(
localWeb3Provider,
artifact.abi, // Storage.sol
artifact.bytecode,
accounts[0]
);
const solcJsonPath = path.join(
"test",
"testcontracts",
"Storage",
"StorageJsonInput.json"
);
const solcJsonBuffer = fs.readFileSync(solcJsonPath);

const res = await chai
.request(server.app)
.post("/verify/solc-json")
.attach("files", solcJsonBuffer)
.field("address", address)
.field("chain", defaultContractChain)
.field("compilerVersion", "0.8.4+commit.c7e474f2")
.field("contractName", "Storage");

assertVerification(null, res, null, address, defaultContractChain);
});
describe("hardhat build-info file support", function () {
this.timeout(EXTENDED_TIME);
let address;
Expand Down Expand Up @@ -1467,6 +1545,56 @@ describe("Server", function () {
assertSingleContractStatus(res, "perfect");
});

it("should return validation error for adding standard input JSON without a compiler version", async () => {
const agent = chai.request.agent(server.app);

const solcJsonPath = path.join(
"test",
"testcontracts",
"Storage",
"StorageJsonInput.json"
);
const solcJsonBuffer = fs.readFileSync(solcJsonPath);

const res = await agent
.post("/session/input-solc-json")
.attach("files", solcJsonBuffer);

assertValidationError(null, res, "compilerVersion");
});

it("should verify a contract with Solidity standard input JSON", async () => {
const agent = chai.request.agent(server.app);
const address = await deployFromAbiAndBytecode(
localWeb3Provider,
artifact.abi, // Storage.sol
artifact.bytecode,
accounts[0]
);
const solcJsonPath = path.join(
"test",
"testcontracts",
"Storage",
"StorageJsonInput.json"
);
const solcJsonBuffer = fs.readFileSync(solcJsonPath);

const res = await agent
.post("/session/input-solc-json")
.field("compilerVersion", "0.8.4+commit.c7e474f2")
.attach("files", solcJsonBuffer);

const contracts = assertSingleContractStatus(res, "error");

contracts[0].address = address;
contracts[0].chainId = defaultContractChain;

const res2 = await agent
.post("/session/verify-validated")
.send({ contracts });
assertSingleContractStatus(res2, "perfect");
});

// Test also extra-file-bytecode-mismatch via v2 API as well since the workaround is at the API level i.e. VerificationController
describe("solc v0.6.12 and v0.7.0 extra files in compilation causing metadata match but bytecode mismatch", function () {
// Deploy the test contract locally
Expand Down
Loading

0 comments on commit 79d05f7

Please sign in to comment.