Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions services/server/src/server/services/VerificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,14 @@ export class VerificationService {
return verificationId;
}

private async verifyViaWorker(
private verifyViaWorker(
verificationId: VerificationJobId,
functionName: string,
input:
| VerifyFromJsonInput
| VerifyFromMetadataInput
| VerifyFromEtherscanInput,
): Promise<void> {
): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, how did you recognize this?

Actually this makes me realize now this function is difficult to read with .then()s. It's ok though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just read over the function and wondered why the function is async.

We mainly have the then() style here because we need to keep a reference to the promise object. We could move the inner logic to another function to use await, but I think it's fine.

const task = this.workerPool
.run(input, { name: functionName })
.then((output: VerifyOutput) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -44,7 +44,7 @@ export default abstract class AbstractDatabaseService {
async insertNewVerifiedContract(
databaseColumns: DatabaseUtil.DatabaseColumns,
client: PoolClient,
): Promise<string> {
): Promise<Tables.VerifiedContract["id"]> {
try {
let recompiledCreationCodeInsertResult:
| QueryResult<Pick<DatabaseUtil.Tables.Code, "bytecode_hash">>
Expand Down Expand Up @@ -127,7 +127,7 @@ export default abstract class AbstractDatabaseService {
async updateExistingVerifiedContract(
databaseColumns: DatabaseUtil.DatabaseColumns,
client: PoolClient,
): Promise<string> {
): Promise<Tables.VerifiedContract["id"]> {
// runtime bytecodes must exist
if (databaseColumns.recompiledRuntimeCode.bytecode === undefined) {
throw new Error("Missing normalized runtime bytecode");
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Field,
FIELDS_TO_STORED_PROPERTIES,
StoredProperties,
Tables,
} from "../utils/database-util";
import {
ContractData,
Expand All @@ -28,6 +29,7 @@ import {
VerificationJob,
Match,
VerificationJobId,
BytesKeccak,
} from "../../types";
import Path from "path";
import {
Expand All @@ -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";
Expand Down Expand Up @@ -884,6 +887,57 @@ export class SourcifyDatabaseService
});
}

private async storeSignatures(
poolClient: PoolClient,
verifiedContractId: Tables.VerifiedContract["id"],
verification: VerificationExport,
): Promise<void> {
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<BytesKeccak>(sig.signatureHash32),
signature: sig.signature,
signature_type: sig.signatureType,
}));

await this.database.insertSignatures(signatureColumns, poolClient);
await this.database.insertCompiledContractSignatures(
compilationId,
Comment on lines +914 to +922
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing but we are passing the same signatureColumns to both insertSignatures and insertCompiledContractSignatures functions. Even though the parameter signatures in both functions have different types. This works because the signatureColumns is a union of both types.

I don't have a strong opinion here but just wanted to point out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did this on purpose to keep the code shorter. IMO, when you accept an object typed via an interface, you should also expect the object to possibly have more properties, because TypeScript doesn't guarantee the absence of other properties ("duck typing").

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,
Expand All @@ -892,7 +946,7 @@ export class SourcifyDatabaseService
verificationId: VerificationJobId;
finishTime: Date;
},
): Promise<void> {
): Promise<{ verifiedContractId: Tables.VerifiedContract["id"] }> {
try {
const { type, verifiedContractId, oldVerifiedContractId } =
await super.insertOrUpdateVerification(verification, poolClient);
Expand Down Expand Up @@ -962,6 +1016,8 @@ export class SourcifyDatabaseService
poolClient,
);
}

return { verifiedContractId: verifiedContractId };
} catch (error: any) {
logger.error("Error storing verification", {
error: error,
Expand All @@ -970,19 +1026,30 @@ export class SourcifyDatabaseService
}
}

// Override this method to include the SourcifyMatch
async storeVerification(
verification: VerificationExport,
jobData?: {
verificationId: VerificationJobId;
finishTime: Date;
},
): Promise<void> {
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,
);
});
}
Expand Down
74 changes: 73 additions & 1 deletion services/server/src/server/services/utils/Database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Pool, PoolClient, QueryResult } from "pg";
import { Bytes } from "../../types";
import { Bytes, BytesKeccak } from "../../types";
import {
bytesFromString,
GetSourcifyMatchByChainAddressResult,
Expand Down Expand Up @@ -285,6 +285,16 @@ ${
);
}

async getCompilationIdForVerifiedContract(
verifiedContractId: Tables.VerifiedContract["id"],
poolClient?: PoolClient,
): Promise<QueryResult<Pick<Tables.VerifiedContract, "compilation_id">>> {
return await (poolClient || this.pool).query(
`SELECT compilation_id FROM verified_contracts WHERE id = $1`,
[verifiedContractId],
);
}

async insertSourcifyMatch(
{
verified_contract_id,
Expand Down Expand Up @@ -716,6 +726,68 @@ ${
);
}

async insertSignatures(
signatures: Omit<Tables.Signatures, "signature_hash_4">[],
poolClient?: PoolClient,
): Promise<void> {
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<void> {
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,
{
Expand Down
13 changes: 13 additions & 0 deletions services/server/src/server/services/utils/database-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ export namespace Tables {
onchain_runtime_code: Nullable<Bytes>;
creation_transaction_hash: Nullable<Bytes>;
}

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 {
Expand Down
39 changes: 39 additions & 0 deletions services/server/src/server/services/utils/signature-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { id as keccak256str, Fragment, JsonFragment } from "ethers";

export interface SignatureData {
signature: string;
signatureHash32: string;
signatureType: "function" | "event" | "error";
}

export function extractSignaturesFromAbi(abi: JsonFragment[]): SignatureData[] {
const signatures: SignatureData[] = [];

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));
}
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should wrapt this in a try/catch because fragment.format() will fail for some library contracts that have an invalid "type" field. Library ABI items are allowed to have custom types because their ABIs are not intended to be publicly callable. However this breaks with ethers when assembling the signature. See the latest messages in the Solidity private chat.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add a test case for the above issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I made the function throwing in this case now, so we would not store any of the signatures if only one custom type was found. Or was your intention rather to ignore the fragments with custom types?

}

return signatures;
}

function getSignatureData(fragment: Fragment): SignatureData {
const signature = fragment.format("sighash");
return {
signature,
signatureHash32: keccak256str(signature),
signatureType: fragment.type as "function" | "event" | "error",
};
}
2 changes: 2 additions & 0 deletions services/server/test/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading