From 4b327c1f84554d2a95f9eb3838973fa0308546bf Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Wed, 3 Sep 2025 12:21:03 +0200 Subject: [PATCH 1/6] Add signature-util for extracting SignatureData from ABIs --- .../server/services/utils/signature-util.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 services/server/src/server/services/utils/signature-util.ts diff --git a/services/server/src/server/services/utils/signature-util.ts b/services/server/src/server/services/utils/signature-util.ts new file mode 100644 index 000000000..fc14a92f4 --- /dev/null +++ b/services/server/src/server/services/utils/signature-util.ts @@ -0,0 +1,33 @@ +import { Interface, keccak256, Fragment, JsonFragment } from "ethers"; + +export interface SignatureData { + signature: string; + signatureHash32: string; + signatureType: "function" | "event" | "error"; +} + +export function extractSignaturesFromAbi(abi: JsonFragment[]): SignatureData[] { + const signatures: SignatureData[] = []; + + const iface = new Interface(abi); + + iface.fragments.forEach((fragment) => { + switch (fragment.type) { + case "function": + case "event": + case "error": + signatures.push(getSignatureData(fragment)); + } + }); + + return signatures; +} + +function getSignatureData(fragment: Fragment): SignatureData { + const signature = fragment.format("sighash"); + return { + signature, + signatureHash32: keccak256(signature), + signatureType: fragment.type as "function" | "event" | "error", + }; +} From fd4bd649b5f75996d3ec1de3a97fd9794926f7a2 Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Wed, 3 Sep 2025 12:22:09 +0200 Subject: [PATCH 2/6] Add signature tables interfaces --- .../src/server/services/utils/database-util.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/server/src/server/services/utils/database-util.ts b/services/server/src/server/services/utils/database-util.ts index 272712b64..073cfe890 100644 --- a/services/server/src/server/services/utils/database-util.ts +++ b/services/server/src/server/services/utils/database-util.ts @@ -164,6 +164,19 @@ export namespace Tables { onchain_runtime_code: Nullable; creation_transaction_hash: Nullable; } + + export interface Signatures { + signature_hash_32: BytesKeccak; + signature_hash_4: Bytes; + signature: string; + } + + export interface CompiledContractsSignatures { + id: string; + compilation_id: string; + signature_hash_32: BytesKeccak; + signature_type: "function" | "event" | "error"; + } } export interface SourceInformation { From 9be3c136469ca6ebe99191986aeeca7f2750414b Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Thu, 4 Sep 2025 12:13:35 +0200 Subject: [PATCH 3/6] Add logic for writing signatures to SourcifyDatabaseService --- .../server/services/VerificationService.ts | 4 +- .../AbstractDatabaseService.ts | 10 +-- .../SourcifyDatabaseService.ts | 77 +++++++++++++++++-- .../src/server/services/utils/Database.ts | 74 +++++++++++++++++- 4 files changed, 152 insertions(+), 13 deletions(-) diff --git a/services/server/src/server/services/VerificationService.ts b/services/server/src/server/services/VerificationService.ts index 5fceeea58..5138a8eb8 100644 --- a/services/server/src/server/services/VerificationService.ts +++ b/services/server/src/server/services/VerificationService.ts @@ -345,14 +345,14 @@ export class VerificationService { return verificationId; } - private async verifyViaWorker( + private verifyViaWorker( verificationId: VerificationJobId, functionName: string, input: | VerifyFromJsonInput | VerifyFromMetadataInput | VerifyFromEtherscanInput, - ): Promise { + ): void { const task = this.workerPool .run(input, { name: functionName }) .then((output: VerifyOutput) => { diff --git a/services/server/src/server/services/storageServices/AbstractDatabaseService.ts b/services/server/src/server/services/storageServices/AbstractDatabaseService.ts index b103d18cc..0d4a68fbc 100644 --- a/services/server/src/server/services/storageServices/AbstractDatabaseService.ts +++ b/services/server/src/server/services/storageServices/AbstractDatabaseService.ts @@ -1,6 +1,6 @@ import { VerificationExport } from "@ethereum-sourcify/lib-sourcify"; import * as DatabaseUtil from "../utils/database-util"; -import { bytesFromString } from "../utils/database-util"; +import { bytesFromString, Tables } from "../utils/database-util"; import { Database, DatabaseOptions } from "../utils/Database"; import { PoolClient, QueryResult } from "pg"; @@ -44,7 +44,7 @@ export default abstract class AbstractDatabaseService { async insertNewVerifiedContract( databaseColumns: DatabaseUtil.DatabaseColumns, client: PoolClient, - ): Promise { + ): Promise { try { let recompiledCreationCodeInsertResult: | QueryResult> @@ -127,7 +127,7 @@ export default abstract class AbstractDatabaseService { async updateExistingVerifiedContract( databaseColumns: DatabaseUtil.DatabaseColumns, client: PoolClient, - ): Promise { + ): Promise { // runtime bytecodes must exist if (databaseColumns.recompiledRuntimeCode.bytecode === undefined) { throw new Error("Missing normalized runtime bytecode"); @@ -224,8 +224,8 @@ export default abstract class AbstractDatabaseService { poolClient: PoolClient, ): Promise<{ type: "update" | "insert"; - verifiedContractId: string; - oldVerifiedContractId?: string; + verifiedContractId: Tables.VerifiedContract["id"]; + oldVerifiedContractId?: Tables.VerifiedContract["id"]; }> { this.validateVerificationBeforeStoring(verification); diff --git a/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts b/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts index a2606d288..882045a9b 100644 --- a/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts +++ b/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts @@ -12,6 +12,7 @@ import { Field, FIELDS_TO_STORED_PROPERTIES, StoredProperties, + Tables, } from "../utils/database-util"; import { ContractData, @@ -28,6 +29,7 @@ import { VerificationJob, Match, VerificationJobId, + BytesKeccak, } from "../../types"; import Path from "path"; import { @@ -37,6 +39,7 @@ import { toMatchLevel, } from "../utils/util"; import { getAddress, id as keccak256Str } from "ethers"; +import { extractSignaturesFromAbi } from "../utils/signature-util"; import { BadRequestError } from "../../../common/errors"; import { RWStorageIdentifiers } from "./identifiers"; import semver from "semver"; @@ -884,6 +887,57 @@ export class SourcifyDatabaseService }); } + private async storeSignatures( + poolClient: PoolClient, + verifiedContractId: Tables.VerifiedContract["id"], + verification: VerificationExport, + ): Promise { + try { + const compiledContractResult = + await this.database.getCompilationIdForVerifiedContract( + verifiedContractId, + poolClient, + ); + if (compiledContractResult.rowCount === 0) { + throw new Error( + `No compilation found for verifiedContractId ${verifiedContractId}`, + ); + } + const compilationId = compiledContractResult.rows[0].compilation_id; + + const abi = verification.compilation.contractCompilerOutput.abi; + if (!abi) { + throw new Error("No ABI found in compilation output"); + } + + const signatureData = extractSignaturesFromAbi(abi); + const signatureColumns = signatureData.map((sig) => ({ + signature_hash_32: bytesFromString(sig.signatureHash32), + signature: sig.signature, + signature_type: sig.signatureType, + })); + + await this.database.insertSignatures(signatureColumns, poolClient); + await this.database.insertCompiledContractSignatures( + compilationId, + signatureColumns, + poolClient, + ); + + logger.info("Stored signatures to SourcifyDatabase", { + verifiedContractId, + compilationId, + signatureCount: signatureData.length, + }); + } catch (error) { + // Don't throw on errors, the job should not fail + logger.error("Error storing signatures", { + verifiedContractId, + error: error, + }); + } + } + // Override this method to include the SourcifyMatch async storeVerificationWithPoolClient( poolClient: PoolClient, @@ -892,10 +946,10 @@ export class SourcifyDatabaseService verificationId: VerificationJobId; finishTime: Date; }, - ): Promise { + ): Promise<{ verifiedContractId: Tables.VerifiedContract["id"] }> { try { const { type, verifiedContractId, oldVerifiedContractId } = - await super.insertOrUpdateVerification(verification, poolClient); + await this.insertOrUpdateVerification(verification, poolClient); if (type === "insert") { if (!verifiedContractId) { @@ -962,6 +1016,8 @@ export class SourcifyDatabaseService poolClient, ); } + + return { verifiedContractId: verifiedContractId }; } catch (error: any) { logger.error("Error storing verification", { error: error, @@ -970,7 +1026,6 @@ export class SourcifyDatabaseService } } - // Override this method to include the SourcifyMatch async storeVerification( verification: VerificationExport, jobData?: { @@ -978,11 +1033,23 @@ export class SourcifyDatabaseService finishTime: Date; }, ): Promise { + const { verifiedContractId } = await this.withTransaction( + async (transactionPoolClient) => { + return await this.storeVerificationWithPoolClient( + transactionPoolClient, + verification, + jobData, + ); + }, + ); + + // Separate transaction because storing the verification should not fail + // if signatures cannot be stored await this.withTransaction(async (transactionPoolClient) => { - await this.storeVerificationWithPoolClient( + await this.storeSignatures( transactionPoolClient, + verifiedContractId, verification, - jobData, ); }); } diff --git a/services/server/src/server/services/utils/Database.ts b/services/server/src/server/services/utils/Database.ts index 66a4a7219..79bee9259 100644 --- a/services/server/src/server/services/utils/Database.ts +++ b/services/server/src/server/services/utils/Database.ts @@ -1,5 +1,5 @@ import { Pool, PoolClient, QueryResult } from "pg"; -import { Bytes } from "../../types"; +import { Bytes, BytesKeccak } from "../../types"; import { bytesFromString, GetSourcifyMatchByChainAddressResult, @@ -285,6 +285,16 @@ ${ ); } + async getCompilationIdForVerifiedContract( + verifiedContractId: Tables.VerifiedContract["id"], + poolClient?: PoolClient, + ): Promise>> { + return await (poolClient || this.pool).query( + `SELECT compilation_id FROM verified_contracts WHERE id = $1`, + [verifiedContractId], + ); + } + async insertSourcifyMatch( { verified_contract_id, @@ -716,6 +726,68 @@ ${ ); } + async insertSignatures( + signatures: Omit[], + poolClient?: PoolClient, + ): Promise { + if (signatures.length === 0) { + return; + } + + const valueIndexes: string[] = []; + const queryValues: (BytesKeccak | string)[] = []; + + signatures.forEach((_, index) => { + const baseIndex = index * 2 + 1; + valueIndexes.push(`($${baseIndex}, $${baseIndex + 1})`); + }); + + signatures.forEach(({ signature_hash_32, signature }) => { + queryValues.push(signature_hash_32, signature); + }); + + await (poolClient || this.pool).query( + `INSERT INTO ${this.schema}.signatures (signature_hash_32, signature) + VALUES ${valueIndexes.join(", ")} + ON CONFLICT (signature_hash_32) DO NOTHING`, + queryValues, + ); + } + + async insertCompiledContractSignatures( + compilation_id: string, + signatures: Omit< + Tables.CompiledContractsSignatures, + "id" | "compilation_id" + >[], + poolClient?: PoolClient, + ): Promise { + if (signatures.length === 0) { + return; + } + + const valueIndexes: string[] = []; + const queryValues: (BytesKeccak | string)[] = []; + + signatures.forEach((_, index) => { + const baseIndex = index * 3 + 1; + valueIndexes.push( + `($${baseIndex}, $${baseIndex + 1}, $${baseIndex + 2})`, + ); + }); + + signatures.forEach(({ signature_hash_32, signature_type }) => { + queryValues.push(compilation_id, signature_hash_32, signature_type); + }); + + await (poolClient || this.pool).query( + `INSERT INTO ${this.schema}.compiled_contracts_signatures (compilation_id, signature_hash_32, signature_type) + VALUES ${valueIndexes.join(", ")} + ON CONFLICT (compilation_id, signature_hash_32, signature_type) DO NOTHING`, + queryValues, + ); + } + async insertVerifiedContract( poolClient: PoolClient, { From 3702f3b89579fc9dd4fdcd40e295a0d30e35045a Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Thu, 4 Sep 2025 15:59:27 +0200 Subject: [PATCH 4/6] Add tests --- .../server/services/utils/signature-util.ts | 4 +- services/server/test/helpers/helpers.ts | 2 + .../SourcifyDatabaseService.spec.ts | 134 +++++++ .../test/unit/utils/signature-util.spec.ts | 335 ++++++++++++++++++ 4 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 services/server/test/unit/utils/signature-util.spec.ts diff --git a/services/server/src/server/services/utils/signature-util.ts b/services/server/src/server/services/utils/signature-util.ts index fc14a92f4..41e9c28c2 100644 --- a/services/server/src/server/services/utils/signature-util.ts +++ b/services/server/src/server/services/utils/signature-util.ts @@ -1,4 +1,4 @@ -import { Interface, keccak256, Fragment, JsonFragment } from "ethers"; +import { Interface, id as keccak256str, Fragment, JsonFragment } from "ethers"; export interface SignatureData { signature: string; @@ -27,7 +27,7 @@ function getSignatureData(fragment: Fragment): SignatureData { const signature = fragment.format("sighash"); return { signature, - signatureHash32: keccak256(signature), + signatureHash32: keccak256str(signature), signatureType: fragment.type as "function" | "event" | "error", }; } diff --git a/services/server/test/helpers/helpers.ts b/services/server/test/helpers/helpers.ts index 2db40ec2a..34e792cca 100644 --- a/services/server/test/helpers/helpers.ts +++ b/services/server/test/helpers/helpers.ts @@ -291,6 +291,8 @@ export async function resetDatabase(sourcifyDatabase: Pool) { ); await sourcifyDatabase.query("DELETE FROM verified_contracts"); await sourcifyDatabase.query("DELETE FROM contract_deployments"); + await sourcifyDatabase.query("DELETE FROM compiled_contracts_signatures"); + await sourcifyDatabase.query("DELETE FROM signatures"); await sourcifyDatabase.query("DELETE FROM compiled_contracts_sources"); await sourcifyDatabase.query("DELETE FROM sources"); await sourcifyDatabase.query("DELETE FROM compiled_contracts"); diff --git a/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts b/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts index 20106abb7..6b29151eb 100644 --- a/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts +++ b/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts @@ -3,11 +3,21 @@ import { SourcifyDatabaseService } from "../../../src/server/services/storageSer import config from "config"; import chaiAsPromised from "chai-as-promised"; import { MockVerificationExport } from "../../helpers/mocks"; +import { resetDatabase } from "../../helpers/helpers"; +import sinon from "sinon"; +import * as signatureUtil from "../../../src/server/services/utils/signature-util"; +import { QueryResult } from "pg"; +import { + bytesFromString, + type Tables, +} from "../../../src/server/services/utils/database-util"; +import { id as keccak256str } from "ethers"; use(chaiAsPromised); describe("SourcifyDatabaseService", function () { let databaseService: SourcifyDatabaseService; + const sandbox = sinon.createSandbox(); before(async () => { process.env.SOURCIFY_POSTGRES_PORT = @@ -34,6 +44,15 @@ describe("SourcifyDatabaseService", function () { }, config.get("serverUrl"), ); + await databaseService.init(); + }); + + this.beforeEach(async () => { + await resetDatabase(databaseService.database.pool); + }); + + afterEach(() => { + sandbox.restore(); }); it("should throw an error if no verified_contracts row can be inserted for a verification update", async () => { @@ -46,4 +65,119 @@ describe("SourcifyDatabaseService", function () { await expect(databaseService.storeVerification(MockVerificationExport)).to .eventually.be.rejected; }); + + it("should store signatures correctly when storeVerification is called", async () => { + await databaseService.storeVerification(MockVerificationExport); + + const signaturesResult: QueryResult = + await databaseService.database.pool.query("SELECT * FROM signatures"); + + expect(signaturesResult.rowCount).to.equal(2); + + const signatures = signaturesResult.rows; + const retrieveSignature = signatures.find( + (s) => s.signature === "retrieve()", + ); + const storeSignature = signatures.find( + (s) => s.signature === "store(uint256)", + ); + + expect(retrieveSignature).to.exist; + expect(storeSignature).to.exist; + + const expectedRetrieveSignatureHash32 = bytesFromString( + keccak256str(retrieveSignature!.signature), + ); + const expectedStoreSignatureHash32 = bytesFromString( + keccak256str(storeSignature!.signature), + ); + + expect(retrieveSignature!.signature_hash_32).to.be.instanceOf(Buffer); + expect(retrieveSignature!.signature_hash_32.length).to.equal(32); + expect( + retrieveSignature!.signature_hash_32.equals( + expectedRetrieveSignatureHash32, + ), + ).to.be.true; + expect(retrieveSignature!.signature_hash_4).to.be.instanceOf(Buffer); + expect(retrieveSignature!.signature_hash_4.length).to.equal(4); + expect(retrieveSignature!.signature_hash_4).to.deep.equal( + expectedRetrieveSignatureHash32.subarray(0, 4), + ); + + expect(storeSignature!.signature_hash_32).to.be.instanceOf(Buffer); + expect(storeSignature!.signature_hash_32.length).to.equal(32); + expect( + storeSignature!.signature_hash_32.equals(expectedStoreSignatureHash32), + ).to.be.true; + expect(storeSignature!.signature_hash_4).to.be.instanceOf(Buffer); + expect(storeSignature!.signature_hash_4.length).to.equal(4); + expect(storeSignature!.signature_hash_4).to.deep.equal( + expectedStoreSignatureHash32.subarray(0, 4), + ); + + const compiledContractSignaturesResult: QueryResult = + await databaseService.database.pool.query( + "SELECT * FROM compiled_contracts_signatures", + ); + + expect(compiledContractSignaturesResult.rowCount).to.equal(2); + + const contractSignatures = compiledContractSignaturesResult.rows; + const compiledContractRetrieveSig = + compiledContractSignaturesResult.rows.find((csig) => + csig.signature_hash_32.equals(expectedRetrieveSignatureHash32), + ); + const compiledContractStoreSig = contractSignatures.find((csig) => + csig.signature_hash_32.equals(expectedStoreSignatureHash32), + ); + + expect(compiledContractRetrieveSig).to.exist; + expect(compiledContractStoreSig).to.exist; + expect(compiledContractRetrieveSig!.compilation_id).to.equal( + compiledContractStoreSig!.compilation_id, + ); + expect(compiledContractRetrieveSig!.signature_type).to.equal("function"); + expect(compiledContractStoreSig!.signature_type).to.equal("function"); + }); + + it("should handle duplicate signature storage gracefully", async () => { + // Change mock to be able to store the verification twice + const modifiedVerification = structuredClone(MockVerificationExport); + modifiedVerification.status.creationMatch = "partial"; + modifiedVerification.compilation.language = "Vyper"; + + await databaseService.storeVerification(modifiedVerification); + await expect(databaseService.storeVerification(MockVerificationExport)).to + .not.be.rejected; + + const signaturesResult = await databaseService.database.pool.query( + "SELECT COUNT(*) as count FROM signatures", + ); + expect(parseInt(signaturesResult.rows[0].count)).to.equal(2); + }); + + it("should still store verification even if signature storage fails", async () => { + sandbox + .stub(signatureUtil, "extractSignaturesFromAbi") + .throws(new Error("Simulated signature extraction error")); + + await expect(databaseService.storeVerification(MockVerificationExport)).to + .not.be.rejected; + + const verifiedContractsResult = await databaseService.database.pool.query( + "SELECT COUNT(*) FROM verified_contracts", + ); + expect(parseInt(verifiedContractsResult.rows[0].count)).to.equal(1); + + const signaturesResult = await databaseService.database.pool.query( + "SELECT COUNT(*) as count FROM signatures", + ); + expect(parseInt(signaturesResult.rows[0].count)).to.equal(0); + + const contractSignaturesResult = await databaseService.database.pool.query( + "SELECT COUNT(*) as count FROM compiled_contracts_signatures", + ); + expect(parseInt(contractSignaturesResult.rows[0].count)).to.equal(0); + }); }); diff --git a/services/server/test/unit/utils/signature-util.spec.ts b/services/server/test/unit/utils/signature-util.spec.ts new file mode 100644 index 000000000..aa213c463 --- /dev/null +++ b/services/server/test/unit/utils/signature-util.spec.ts @@ -0,0 +1,335 @@ +import chai from "chai"; +import { extractSignaturesFromAbi } from "../../../src/server/services/utils/signature-util"; +import { JsonFragment, id as keccak256str } from "ethers"; + +describe("signature-util", function () { + describe("extractSignaturesFromAbi", function () { + it("should extract function signatures", function () { + const abi: JsonFragment[] = [ + { + inputs: [], + name: "retrieve", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "num", type: "uint256" }], + name: "store", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(2); + + const retrieveSig = result.find((r) => r.signature === "retrieve()"); + const storeSig = result.find((r) => r.signature === "store(uint256)"); + + chai.expect(retrieveSig).to.exist; + chai.expect(retrieveSig!.signatureType).to.equal("function"); + chai + .expect(retrieveSig!.signatureHash32) + .to.equal(keccak256str(retrieveSig!.signature)); + + chai.expect(storeSig).to.exist; + chai.expect(storeSig!.signatureType).to.equal("function"); + chai + .expect(storeSig!.signatureHash32) + .to.equal(keccak256str(storeSig!.signature)); + }); + + it("should ignore constructor signatures", function () { + const abi: JsonFragment[] = [ + { + inputs: [ + { + internalType: "uint256", + name: "a", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "getValue", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(1); + chai.expect(result[0].signatureType).to.equal("function"); + chai.expect(result[0].signature).to.equal("getValue()"); + }); + + it("should extract event signatures", function () { + const abi: JsonFragment[] = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(2); + + const approvalSig = result.find( + (r) => r.signature === "Approval(address,address,uint256)", + ); + const transferSig = result.find( + (r) => r.signature === "Transfer(address,address,uint256)", + ); + + chai.expect(approvalSig).to.exist; + chai.expect(approvalSig!.signatureType).to.equal("event"); + chai + .expect(approvalSig!.signatureHash32) + .to.equal(keccak256str(approvalSig!.signature)); + + chai.expect(transferSig).to.exist; + chai.expect(transferSig!.signatureType).to.equal("event"); + chai + .expect(transferSig!.signatureHash32) + .to.equal(keccak256str(transferSig!.signature)); + }); + + it("should extract error signatures", function () { + const abi: JsonFragment[] = [ + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "allowance", type: "uint256" }, + { internalType: "uint256", name: "needed", type: "uint256" }, + ], + name: "ERC20InsufficientAllowance", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "balance", type: "uint256" }, + { internalType: "uint256", name: "needed", type: "uint256" }, + ], + name: "ERC20InsufficientBalance", + type: "error", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(2); + chai.expect(result[0].signatureType).to.equal("error"); + chai + .expect(result[0].signature) + .to.equal("ERC20InsufficientAllowance(address,uint256,uint256)"); + chai + .expect(result[0].signatureHash32) + .to.equal(keccak256str(result[0].signature)); + chai.expect(result[1].signatureType).to.equal("error"); + chai + .expect(result[1].signature) + .to.equal("ERC20InsufficientBalance(address,uint256,uint256)"); + chai + .expect(result[1].signatureHash32) + .to.equal(keccak256str(result[1].signature)); + }); + + it("should handle mixed ABI with functions and events (ignoring constructors)", function () { + const abi: JsonFragment[] = [ + { + inputs: [ + { internalType: "uint256", name: "initialValue", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "getValue", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "newValue", + type: "uint256", + }, + ], + name: "ValueChanged", + type: "event", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(2); + + const functionSig = result.find((r) => r.signatureType === "function"); + const eventSig = result.find((r) => r.signatureType === "event"); + + chai.expect(functionSig).to.exist; + chai.expect(eventSig).to.exist; + + chai.expect(functionSig!.signature).to.equal("getValue()"); + chai.expect(eventSig!.signature).to.equal("ValueChanged(uint256)"); + }); + + it("should handle empty ABI", function () { + const result = extractSignaturesFromAbi([]); + chai.expect(result).to.be.an("array").that.is.empty; + }); + + it("should ignore fallback and receive functions", function () { + const abi: JsonFragment[] = [ + { + stateMutability: "payable", + type: "fallback", + }, + { + stateMutability: "payable", + type: "receive", + }, + { + inputs: [], + name: "getValue", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(1); + chai.expect(result[0].signatureType).to.equal("function"); + chai.expect(result[0].signature).to.equal("getValue()"); + }); + + it("should generate correct signature hashes", function () { + const abi: JsonFragment[] = [ + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(1); + chai.expect(result[0].signature).to.equal("transfer(address,uint256)"); + chai.expect(result[0].signatureHash32).to.be.a("string"); + chai.expect(result[0].signatureHash32).to.have.lengthOf(66); + chai.expect(result[0].signatureHash32).to.match(/^0x[a-fA-F0-9]{64}$/); + }); + + it("should handle complex function signatures with arrays and tuples", function () { + const abi: JsonFragment[] = [ + { + inputs: [ + { internalType: "uint256[]", name: "amounts", type: "uint256[]" }, + { + internalType: "address[]", + name: "recipients", + type: "address[]", + }, + { + components: [ + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + internalType: "struct Permit", + name: "permit", + type: "tuple", + }, + ], + name: "batchTransferWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ]; + + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(1); + chai + .expect(result[0].signature) + .to.equal( + "batchTransferWithPermit(uint256[],address[],(uint256,uint8,bytes32,bytes32))", + ); + chai.expect(result[0].signatureType).to.equal("function"); + chai + .expect(result[0].signatureHash32) + .to.equal(keccak256str(result[0].signature)); + }); + }); +}); From 6bfbf5f848e869451a294475797efd1017f1fb8b Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Mon, 8 Sep 2025 10:36:13 +0200 Subject: [PATCH 5/6] address PR feedback --- .../SourcifyDatabaseService.ts | 2 +- .../server/services/utils/signature-util.ts | 25 +++++++++------- .../SourcifyDatabaseService.spec.ts | 4 +-- .../test/unit/utils/signature-util.spec.ts | 30 +++++++++++++++++++ 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts b/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts index 882045a9b..7e4a8524b 100644 --- a/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts +++ b/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts @@ -949,7 +949,7 @@ export class SourcifyDatabaseService ): Promise<{ verifiedContractId: Tables.VerifiedContract["id"] }> { try { const { type, verifiedContractId, oldVerifiedContractId } = - await this.insertOrUpdateVerification(verification, poolClient); + await super.insertOrUpdateVerification(verification, poolClient); if (type === "insert") { if (!verifiedContractId) { diff --git a/services/server/src/server/services/utils/signature-util.ts b/services/server/src/server/services/utils/signature-util.ts index 41e9c28c2..27e613ac5 100644 --- a/services/server/src/server/services/utils/signature-util.ts +++ b/services/server/src/server/services/utils/signature-util.ts @@ -1,4 +1,4 @@ -import { Interface, id as keccak256str, Fragment, JsonFragment } from "ethers"; +import { id as keccak256str, Fragment, JsonFragment } from "ethers"; export interface SignatureData { signature: string; @@ -9,16 +9,21 @@ export interface SignatureData { export function extractSignaturesFromAbi(abi: JsonFragment[]): SignatureData[] { const signatures: SignatureData[] = []; - const iface = new Interface(abi); - - iface.fragments.forEach((fragment) => { - switch (fragment.type) { - case "function": - case "event": - case "error": - signatures.push(getSignatureData(fragment)); + try { + for (const item of abi) { + const fragment = Fragment.from(item); + switch (fragment.type) { + case "function": + case "event": + case "error": + signatures.push(getSignatureData(fragment)); + } } - }); + } catch (error) { + throw new Error( + "Failed to extract signatures from ABI due to an invalid fragment", + ); + } return signatures; } diff --git a/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts b/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts index 6b29151eb..46a90577a 100644 --- a/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts +++ b/services/server/test/integration/storageServices/SourcifyDatabaseService.spec.ts @@ -86,10 +86,10 @@ describe("SourcifyDatabaseService", function () { expect(storeSignature).to.exist; const expectedRetrieveSignatureHash32 = bytesFromString( - keccak256str(retrieveSignature!.signature), + keccak256str("retrieve()"), ); const expectedStoreSignatureHash32 = bytesFromString( - keccak256str(storeSignature!.signature), + keccak256str("store(uint256)"), ); expect(retrieveSignature!.signature_hash_32).to.be.instanceOf(Buffer); diff --git a/services/server/test/unit/utils/signature-util.spec.ts b/services/server/test/unit/utils/signature-util.spec.ts index aa213c463..8bb2c4f48 100644 --- a/services/server/test/unit/utils/signature-util.spec.ts +++ b/services/server/test/unit/utils/signature-util.spec.ts @@ -331,5 +331,35 @@ describe("signature-util", function () { .expect(result[0].signatureHash32) .to.equal(keccak256str(result[0].signature)); }); + + it("should throw for invalid ABIs", function () { + const abi: JsonFragment[] = [ + { + inputs: [{ internalType: "uint256", name: "num", type: "uint256" }], + name: "store", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + name: "get", + type: "function", + inputs: [ + { + name: "dataStore", + type: "DataStore", + internalType: "contract DataStore", + }, + { + name: "key", + type: "bytes32", + internalType: "bytes32", + }, + ], + }, + ]; + + chai.expect(() => extractSignaturesFromAbi(abi)).to.throw(); + }); }); }); From c4e71161d5e5dfb60e2fcafa62d298d34b440fbf Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Mon, 8 Sep 2025 15:45:42 +0200 Subject: [PATCH 6/6] Ignore ABI fragments with custom type --- .../server/services/utils/signature-util.ts | 27 ++++++++++--------- .../test/unit/utils/signature-util.spec.ts | 8 ++++-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/services/server/src/server/services/utils/signature-util.ts b/services/server/src/server/services/utils/signature-util.ts index 27e613ac5..0c3a85e38 100644 --- a/services/server/src/server/services/utils/signature-util.ts +++ b/services/server/src/server/services/utils/signature-util.ts @@ -9,20 +9,21 @@ export interface SignatureData { export function extractSignaturesFromAbi(abi: JsonFragment[]): SignatureData[] { const signatures: SignatureData[] = []; - try { - for (const item of abi) { - const fragment = Fragment.from(item); - switch (fragment.type) { - case "function": - case "event": - case "error": - signatures.push(getSignatureData(fragment)); - } + for (const item of abi) { + let fragment: Fragment; + try { + fragment = Fragment.from(item); + } catch (error) { + // Ignore invalid fragments + // e.g. with custom type as they can appear in library ABIs + continue; + } + switch (fragment.type) { + case "function": + case "event": + case "error": + signatures.push(getSignatureData(fragment)); } - } catch (error) { - throw new Error( - "Failed to extract signatures from ABI due to an invalid fragment", - ); } return signatures; diff --git a/services/server/test/unit/utils/signature-util.spec.ts b/services/server/test/unit/utils/signature-util.spec.ts index 8bb2c4f48..6849e6770 100644 --- a/services/server/test/unit/utils/signature-util.spec.ts +++ b/services/server/test/unit/utils/signature-util.spec.ts @@ -332,7 +332,7 @@ describe("signature-util", function () { .to.equal(keccak256str(result[0].signature)); }); - it("should throw for invalid ABIs", function () { + it("should ignore fragments with custom type", function () { const abi: JsonFragment[] = [ { inputs: [{ internalType: "uint256", name: "num", type: "uint256" }], @@ -359,7 +359,11 @@ describe("signature-util", function () { }, ]; - chai.expect(() => extractSignaturesFromAbi(abi)).to.throw(); + const result = extractSignaturesFromAbi(abi); + + chai.expect(result).to.have.lengthOf(1); + chai.expect(result[0].signatureType).to.equal("function"); + chai.expect(result[0].signature).to.equal("store(uint256)"); }); }); });