Skip to content

Commit

Permalink
feat: FIP-8 verifications for contract wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
horsefacts committed Oct 3, 2023
1 parent 378be35 commit d3a1e05
Show file tree
Hide file tree
Showing 17 changed files with 513 additions and 96 deletions.
88 changes: 72 additions & 16 deletions apps/hubble/src/hubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { L2EventsProvider, OptimismConstants } from "./eth/l2EventsProvider.js";
import { prettyPrintTable } from "./profile/profile.js";
import packageJson from "./package.json" assert { type: "json" };
import { createPublicClient, fallback, http } from "viem";
import { mainnet } from "viem/chains";
import { mainnet, optimism } from "viem/chains";
import { AddrInfo } from "@chainsafe/libp2p-gossipsub/types";
import { CheckIncomingPortsJobScheduler } from "./storage/jobs/checkIncomingPortsJob.js";
import { NetworkConfig, applyNetworkConfig, fetchNetworkConfig } from "./network/utils/networkConfig.js";
Expand Down Expand Up @@ -343,13 +343,20 @@ export class Hub implements HubInterface {
lockTimeout: options.commitLockTimeout,
});

const opMainnetRpcUrls = options.l2RpcUrl.split(",");
const opTransports = opMainnetRpcUrls.map((url) => http(url, { retryCount: 2 }));
const opClient = createPublicClient({
chain: optimism,
transport: fallback(opTransports, { rank: options.rankRpcs ?? false }),
});

const ethMainnetRpcUrls = options.ethMainnetRpcUrl.split(",");
const transports = ethMainnetRpcUrls.map((url) => http(url, { retryCount: 2 }));
const mainnetClient = createPublicClient({
chain: mainnet,
transport: fallback(transports, { rank: options.rankRpcs ?? false }),
});
this.engine = new Engine(this.rocksDB, options.network, eventHandler, mainnetClient);
this.engine = new Engine(this.rocksDB, options.network, eventHandler, mainnetClient, opClient);

const profileSync = options.profileSync ?? false;
this.syncEngine = new SyncEngine(
Expand Down Expand Up @@ -511,7 +518,11 @@ export class Hub implements HubInterface {
if (dbResult.isErr()) {
retryCount++;
logger.error(
{ retryCount, error: dbResult.error, errorMessage: dbResult.error.message },
{
retryCount,
error: dbResult.error,
errorMessage: dbResult.error.message,
},
"failed to open rocksdb. Retry in 15s",
);

Expand Down Expand Up @@ -589,7 +600,9 @@ export class Hub implements HubInterface {
if (this.options.network === FarcasterNetwork.MAINNET) {
const networkConfig = await fetchNetworkConfig();
if (networkConfig.isErr()) {
log.error("failed to fetch network config", { error: networkConfig.error });
log.error("failed to fetch network config", {

Check warning on line 603 in apps/hubble/src/hubble.ts

View check run for this annotation

Codecov / codecov/patch

apps/hubble/src/hubble.ts#L603

Added line #L603 was not covered by tests
error: networkConfig.error,
});
} else {
const shouldExit = this.applyNetworkConfig(networkConfig.value);
if (shouldExit) {
Expand Down Expand Up @@ -711,7 +724,9 @@ export class Hub implements HubInterface {
log.info({ latestSnapshotKey }, "found latest S3 snapshot");

const snapshotUrl = `https://download.farcaster.xyz/${latestSnapshotKey}`;
const response2 = await axios.get(snapshotUrl, { responseType: "stream" });
const response2 = await axios.get(snapshotUrl, {

Check warning on line 727 in apps/hubble/src/hubble.ts

View check run for this annotation

Codecov / codecov/patch

apps/hubble/src/hubble.ts#L727

Added line #L727 was not covered by tests
responseType: "stream",
});
const totalSize = parseInt(response2.headers["content-length"], 10);

let downloadedSize = 0;
Expand Down Expand Up @@ -778,7 +793,11 @@ export class Hub implements HubInterface {
const gossipPort = nodeMultiAddr?.nodeAddress().port;
const rpcPort = this.rpcServer.address?.map((addr) => addr.port).unwrapOr(0);

const gossipAddressContactInfo = GossipAddressInfo.create({ address: announceIp, family, port: gossipPort });
const gossipAddressContactInfo = GossipAddressInfo.create({
address: announceIp,
family,
port: gossipPort,
});
const rpcAddressContactInfo = GossipAddressInfo.create({
address: announceIp,
family,
Expand Down Expand Up @@ -847,7 +866,11 @@ export class Hub implements HubInterface {
const result = await ResultAsync.fromPromise(getHubState(this.rocksDB), (e) => e as HubError);
if (result.isErr() && result.error.errCode === "not_found") {
log.info("hub state not found, resetting state");
const hubState = HubState.create({ lastEthBlock: 0, lastFnameProof: 0, syncEvents: false });
const hubState = HubState.create({
lastEthBlock: 0,
lastFnameProof: 0,
syncEvents: false,
});
await putHubState(this.rocksDB, hubState);
return ok(hubState);
}
Expand All @@ -870,7 +893,10 @@ export class Hub implements HubInterface {
} else {
const contactInfo = contactInfoResult.value;
log.info(
{ rpcAddress: contactInfo.rpcAddress?.address, rpcPort: contactInfo.rpcAddress?.port },
{
rpcAddress: contactInfo.rpcAddress?.address,
rpcPort: contactInfo.rpcAddress?.port,
},
"gossiping contact info",
);

Expand Down Expand Up @@ -899,7 +925,10 @@ export class Hub implements HubInterface {
// If there are too many messages in the queue, drop this message. This is a gossip message, so the sync
// will eventually re-fetch and merge this message in anyway.
log.warn(
{ syncTrieQ: this.syncEngine.syncTrieQSize, syncMergeQ: this.syncEngine.syncMergeQSize },
{
syncTrieQ: this.syncEngine.syncTrieQSize,
syncMergeQ: this.syncEngine.syncMergeQSize,
},
"Sync queue is full, dropping gossip message",
);
return err(new HubError("unavailable", "Sync queue is full"));
Expand Down Expand Up @@ -935,15 +964,23 @@ export class Hub implements HubInterface {

if (p2pMultiAddrResult.isErr()) {
log.error(
{ error: p2pMultiAddrResult.error, message, address: addressInfo.value },
{
error: p2pMultiAddrResult.error,
message,
address: addressInfo.value,
},
"failed to create multiaddr",
);
return;
}

if (p2pMultiAddrResult.value.isErr()) {
log.error(
{ error: p2pMultiAddrResult.value.error, message, address: addressInfo.value },
{
error: p2pMultiAddrResult.value.error,
message,
address: addressInfo.value,
},
"failed to parse multiaddr",
);
return;
Expand Down Expand Up @@ -1107,7 +1144,10 @@ export class Hub implements HubInterface {

async submitMessage(message: Message, source?: HubSubmitSource): HubAsyncResult<number> {
// message is a reserved key in some logging systems, so we use submittedMessage instead
const logMessage = log.child({ submittedMessage: messageToLog(message), source });
const logMessage = log.child({
submittedMessage: messageToLog(message),
source,
});

if (this.syncEngine.syncTrieQSize > MAX_MESSAGE_QUEUE_SIZE) {
log.warn({ syncTrieQSize: this.syncEngine.syncTrieQSize }, "SubmitMessage rejected: Sync trie queue is full");
Expand Down Expand Up @@ -1152,7 +1192,10 @@ export class Hub implements HubInterface {
}

async submitUserNameProof(usernameProof: UserNameProof, source?: HubSubmitSource): HubAsyncResult<number> {
const logEvent = log.child({ event: usernameProofToLog(usernameProof), source });
const logEvent = log.child({

Check warning on line 1195 in apps/hubble/src/hubble.ts

View check run for this annotation

Codecov / codecov/patch

apps/hubble/src/hubble.ts#L1195

Added line #L1195 was not covered by tests
event: usernameProofToLog(usernameProof),
source,
});

const mergeResult = await this.engine.mergeUserNameProof(usernameProof);

Expand Down Expand Up @@ -1282,7 +1325,12 @@ export class Hub implements HubInterface {
const versionCheckResult = ensureAboveMinFarcasterVersion(theirVersion);
if (versionCheckResult.isErr()) {
log.warn(
{ peerId: otherPeerId, theirVersion, ourVersion: FARCASTER_VERSION, errMsg: versionCheckResult.error.message },
{
peerId: otherPeerId,
theirVersion,
ourVersion: FARCASTER_VERSION,
errMsg: versionCheckResult.error.message,
},
"Peer is running an outdated version, ignoring",
);
return false;
Expand Down Expand Up @@ -1333,7 +1381,11 @@ export class Hub implements HubInterface {
const latestJsonParams = {
Bucket: this.s3_snapshot_bucket,
Key: `${this.getSnapshotFolder()}/latest.json`,
Body: JSON.stringify({ key, timestamp: Date.now(), serverDate: new Date().toISOString() }),
Body: JSON.stringify({
key,
timestamp: Date.now(),
serverDate: new Date().toISOString(),
}),
};

try {
Expand All @@ -1347,7 +1399,11 @@ export class Hub implements HubInterface {
}

async listS3Snapshots(): HubAsyncResult<
Array<{ Key: string | undefined; Size: number | undefined; LastModified: Date | undefined }>
Array<{
Key: string | undefined;
Size: number | undefined;
LastModified: Date | undefined;
}>
> {
const network = FarcasterNetwork[this.options.network].toString();

Expand Down
32 changes: 28 additions & 4 deletions apps/hubble/src/storage/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Engine extends TypedEmitter<EngineEvents> {
private _db: RocksDB;
private _network: FarcasterNetwork;
private _publicClient: PublicClient | undefined;
private _l2PublicClient: PublicClient | undefined;

private _linkStore: LinkStore;
private _reactionStore: ReactionStore;
Expand All @@ -98,11 +99,18 @@ class Engine extends TypedEmitter<EngineEvents> {

private _totalPruneSize: number;

constructor(db: RocksDB, network: FarcasterNetwork, eventHandler?: StoreEventHandler, publicClient?: PublicClient) {
constructor(
db: RocksDB,
network: FarcasterNetwork,
eventHandler?: StoreEventHandler,
publicClient?: PublicClient,
l2PublicClient?: PublicClient,
) {
super();
this._db = db;
this._network = network;
this._publicClient = publicClient;
this._l2PublicClient = l2PublicClient;

this.eventHandler = eventHandler ?? new StoreEventHandler(db);

Expand Down Expand Up @@ -880,7 +888,10 @@ class Engine extends TypedEmitter<EngineEvents> {
);
if (custodyEvent.isErr()) {
log.error(
{ errCode: custodyEvent.error.errCode, errMessage: custodyEvent.error.message },
{
errCode: custodyEvent.error.errCode,
errMessage: custodyEvent.error.message,
},
`failed to get v2 custody event for ${message.data.fid}`,
);
} else {
Expand Down Expand Up @@ -972,8 +983,19 @@ class Engine extends TypedEmitter<EngineEvents> {
worker.postMessage({ id, message });
});
} else {
return validations.validateMessage(message, nativeValidationMethods);
return validations.validateMessage(message, nativeValidationMethods, this.getPublicClients());
}
}

private getPublicClients() {
const clients: { [chainId: number]: PublicClient } = {};
if (this._publicClient?.chain) {
clients[this._publicClient.chain.id] = this._publicClient;
}
if (this._l2PublicClient?.chain) {
clients[this._l2PublicClient.chain.id] = this._l2PublicClient;

Check warning on line 996 in apps/hubble/src/storage/engine/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/hubble/src/storage/engine/index.ts#L996

Added line #L996 was not covered by tests
}
return Object.keys(clients).length > 0 ? clients : undefined;
}

private async validateEnsUsernameProof(
Expand All @@ -987,7 +1009,9 @@ class Engine extends TypedEmitter<EngineEvents> {
let resolvedAddress;
let resolvedAddressString;
try {
resolvedAddressString = await this._publicClient?.getEnsAddress({ name: normalize(nameResult.value) });
resolvedAddressString = await this._publicClient?.getEnsAddress({
name: normalize(nameResult.value),
});
const resolvedAddressBytes = hexStringToBytes(resolvedAddressString || "");
if (resolvedAddressBytes.isErr() || resolvedAddressBytes.value.length === 0) {
return err(new HubError("bad_request.validation_failure", `no valid address for ${nameResult.value}`));
Expand Down
64 changes: 52 additions & 12 deletions packages/core/src/crypto/eip712.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ResultAsync } from "neverthrow";
import { HubAsyncResult, HubError } from "../errors";
import { VerificationEthAddressClaim } from "../verifications";
import { UserNameProofClaim } from "../userNameProof";
import { PublicClients, defaultPublicClients } from "../eth/clients";
import { CHAIN_IDS } from "../eth/chains";

export const EIP_712_FARCASTER_DOMAIN = {
name: "Farcaster Verify Ethereum Address",
Expand Down Expand Up @@ -30,6 +32,8 @@ export const EIP_712_FARCASTER_VERIFICATION_CLAIM = [
},
] as const;

export const EIP_712_FARCASTER_VERIFICATION_CLAIM_CHAIN_IDS = [...CHAIN_IDS, 0];

export const EIP_712_FARCASTER_MESSAGE_DATA = [
{
name: "hash",
Expand All @@ -54,20 +58,56 @@ export const verifyVerificationEthAddressClaimSignature = async (
claim: VerificationEthAddressClaim,
signature: Uint8Array,
address: Uint8Array,
verificationType: number,
chainId: number,
publicClients: PublicClients = defaultPublicClients,
): HubAsyncResult<boolean> => {
const valid = await ResultAsync.fromPromise(
verifyTypedData({
address: bytesToHex(address),
domain: EIP_712_FARCASTER_DOMAIN,
types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM },
primaryType: "VerificationClaim",
message: claim,
signature,
}),
(e) => new HubError("unknown", e as Error),
);
if (!EIP_712_FARCASTER_VERIFICATION_CLAIM_CHAIN_IDS.includes(chainId)) {
return ResultAsync.fromPromise(
Promise.reject(),
() => new HubError("bad_request.invalid_param", "Invalid chain ID"),
);
}

return valid;
if (verificationType === 0 && chainId === 0) {
const valid = await ResultAsync.fromPromise(
verifyTypedData({
address: bytesToHex(address),
domain: EIP_712_FARCASTER_DOMAIN,
types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM },
primaryType: "VerificationClaim",
message: claim,
signature,
}),
(e) => new HubError("unknown", e as Error),
);
return valid;
} else if (verificationType === 1 && chainId !== 0) {
const client = publicClients[chainId];
if (!client) {
return ResultAsync.fromPromise(

Check warning on line 88 in packages/core/src/crypto/eip712.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/crypto/eip712.ts#L88

Added line #L88 was not covered by tests
Promise.reject(),
() => new HubError("bad_request.invalid_param", "Invalid chain ID"),

Check warning on line 90 in packages/core/src/crypto/eip712.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/crypto/eip712.ts#L90

Added line #L90 was not covered by tests
);
}
const valid = await ResultAsync.fromPromise(
client.verifyTypedData({
address: bytesToHex(address),
domain: { ...EIP_712_FARCASTER_DOMAIN, chainId },
types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM },
primaryType: "VerificationClaim",
message: claim,
signature,
}),
(e) => new HubError("unknown", e as Error),

Check warning on line 102 in packages/core/src/crypto/eip712.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/crypto/eip712.ts#L102

Added line #L102 was not covered by tests
);
return valid;
} else {
return ResultAsync.fromPromise(
Promise.reject(),
() => new HubError("bad_request.invalid_param", "Invalid verification type"),
);
}
};

export const verifyUserNameProofClaim = async (
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/eth/chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { mainnet, goerli, optimism, optimismGoerli } from "viem/chains";

export const CHAIN_IDS = [mainnet.id, goerli.id, optimism.id, optimismGoerli.id] as const;
Loading

0 comments on commit d3a1e05

Please sign in to comment.