From 09e30725a8b68d34a733a5712f811bf6d75f5eac Mon Sep 17 00:00:00 2001 From: horsefacts Date: Mon, 2 Oct 2023 14:29:58 -0400 Subject: [PATCH] feat: FIP-8 verifications for contract wallets --- apps/hubble/src/hubble.ts | 88 ++++++++++--- apps/hubble/src/storage/engine/index.ts | 32 ++++- packages/core/src/crypto/eip712.ts | 64 ++++++++-- packages/core/src/eth/chains.ts | 3 + packages/core/src/eth/clients.ts | 34 +++++ packages/core/src/factories.ts | 91 ++++++++++--- .../core/src/protobufs/generated/message.ts | 38 +++++- packages/core/src/signers/eip712Signer.ts | 5 +- .../core/src/signers/ethersEip712Signer.ts | 12 +- .../core/src/signers/ethersV5Eip712Signer.ts | 8 +- packages/core/src/signers/testUtils.ts | 2 +- .../core/src/signers/viemLocalEip712Signer.ts | 8 +- packages/core/src/validations.test.ts | 120 +++++++++++++++--- packages/core/src/validations.ts | 13 +- packages/hub-nodejs/src/generated/message.ts | 38 +++++- packages/hub-web/src/generated/message.ts | 38 +++++- protobufs/schemas/message.proto | 16 ++- 17 files changed, 514 insertions(+), 96 deletions(-) create mode 100644 packages/core/src/eth/chains.ts create mode 100644 packages/core/src/eth/clients.ts diff --git a/apps/hubble/src/hubble.ts b/apps/hubble/src/hubble.ts index 3d2dbfc7de..ce43de24b8 100644 --- a/apps/hubble/src/hubble.ts +++ b/apps/hubble/src/hubble.ts @@ -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"; @@ -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( @@ -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", ); @@ -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", { + error: networkConfig.error, + }); } else { const shouldExit = this.applyNetworkConfig(networkConfig.value); if (shouldExit) { @@ -712,7 +725,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, { + responseType: "stream", + }); const totalSize = parseInt(response2.headers["content-length"], 10); let downloadedSize = 0; @@ -779,7 +794,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, @@ -848,7 +867,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); } @@ -871,7 +894,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", ); @@ -900,7 +926,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")); @@ -936,7 +965,11 @@ 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; @@ -944,7 +977,11 @@ export class Hub implements HubInterface { 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; @@ -1108,7 +1145,10 @@ export class Hub implements HubInterface { async submitMessage(message: Message, source?: HubSubmitSource): HubAsyncResult { // 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"); @@ -1153,7 +1193,10 @@ export class Hub implements HubInterface { } async submitUserNameProof(usernameProof: UserNameProof, source?: HubSubmitSource): HubAsyncResult { - const logEvent = log.child({ event: usernameProofToLog(usernameProof), source }); + const logEvent = log.child({ + event: usernameProofToLog(usernameProof), + source, + }); const mergeResult = await this.engine.mergeUserNameProof(usernameProof); @@ -1283,7 +1326,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; @@ -1329,7 +1377,11 @@ export class Hub implements HubInterface { const latestJsonParams = { Bucket: this.s3_snapshot_bucket, Key: `snapshots/${network}/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 { @@ -1343,7 +1395,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(); diff --git a/apps/hubble/src/storage/engine/index.ts b/apps/hubble/src/storage/engine/index.ts index cdfabe8b46..a5b9252ab7 100644 --- a/apps/hubble/src/storage/engine/index.ts +++ b/apps/hubble/src/storage/engine/index.ts @@ -79,6 +79,7 @@ class Engine extends TypedEmitter { private _db: RocksDB; private _network: FarcasterNetwork; private _publicClient: PublicClient | undefined; + private _l2PublicClient: PublicClient | undefined; private _linkStore: LinkStore; private _reactionStore: ReactionStore; @@ -98,11 +99,18 @@ class Engine extends TypedEmitter { 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); @@ -880,7 +888,10 @@ class Engine extends TypedEmitter { ); 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 { @@ -972,8 +983,19 @@ class Engine extends TypedEmitter { 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; } + return Object.keys(clients).length > 0 ? clients : undefined; } private async validateEnsUsernameProof( @@ -987,7 +1009,9 @@ class Engine extends TypedEmitter { 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}`)); diff --git a/packages/core/src/crypto/eip712.ts b/packages/core/src/crypto/eip712.ts index b64842be1f..011793dcca 100644 --- a/packages/core/src/crypto/eip712.ts +++ b/packages/core/src/crypto/eip712.ts @@ -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", @@ -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", @@ -54,20 +58,56 @@ export const verifyVerificationEthAddressClaimSignature = async ( claim: VerificationEthAddressClaim, signature: Uint8Array, address: Uint8Array, + verificationType: number, + chainId: number, + publicClients: PublicClients = defaultPublicClients, ): HubAsyncResult => { - 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( + Promise.reject(), + () => new HubError("bad_request.invalid_param", "Invalid chain ID"), + ); + } + 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), + ); + return valid; + } else { + return ResultAsync.fromPromise( + Promise.reject(), + () => new HubError("bad_request.invalid_param", "Invalid verification type"), + ); + } }; export const verifyUserNameProofClaim = async ( diff --git a/packages/core/src/eth/chains.ts b/packages/core/src/eth/chains.ts new file mode 100644 index 0000000000..e6c1fc01c8 --- /dev/null +++ b/packages/core/src/eth/chains.ts @@ -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; diff --git a/packages/core/src/eth/clients.ts b/packages/core/src/eth/clients.ts new file mode 100644 index 0000000000..0f5a5c082e --- /dev/null +++ b/packages/core/src/eth/clients.ts @@ -0,0 +1,34 @@ +import { PublicClient, createPublicClient, http } from "viem"; +import { mainnet, goerli, optimism, optimismGoerli } from "viem/chains"; +import { CHAIN_IDS } from "./chains"; + +export type PublicClients = { + [chainId: number]: PublicClient; +}; + +export const defaultL1PublicClient: PublicClient = createPublicClient({ + chain: mainnet, + transport: http(), +}); + +export const defaultL2PublicClient: PublicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +export const defaultL1PublicTestClient: PublicClient = createPublicClient({ + chain: goerli, + transport: http(), +}); + +export const defaultL2PublicTestClient: PublicClient = createPublicClient({ + chain: optimismGoerli, + transport: http(), +}); + +export const defaultPublicClients: PublicClients = { + [mainnet.id]: defaultL1PublicClient, + [optimism.id]: defaultL2PublicClient, + [goerli.id]: defaultL1PublicTestClient, + [optimismGoerli.id]: defaultL2PublicTestClient, +}; diff --git a/packages/core/src/factories.ts b/packages/core/src/factories.ts index 87e7997dae..a8e9427acd 100644 --- a/packages/core/src/factories.ts +++ b/packages/core/src/factories.ts @@ -242,11 +242,16 @@ const CastAddDataFactory = Factory.define(() => { const CastAddMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: CastAddDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: CastAddDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.CastAddMessage; }, @@ -268,11 +273,16 @@ const CastRemoveDataFactory = Factory.define(() => { const CastRemoveMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: CastRemoveDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: CastRemoveDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.CastRemoveMessage; }, @@ -295,11 +305,16 @@ const LinkAddDataFactory = Factory.define(() => { const LinkAddMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: LinkAddDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: LinkAddDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.LinkAddMessage; }, @@ -315,11 +330,16 @@ const LinkRemoveDataFactory = Factory.define(() => { const LinkRemoveMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: LinkRemoveDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: LinkRemoveDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.LinkRemoveMessage; }, @@ -342,11 +362,16 @@ const ReactionAddDataFactory = Factory.define(() => { const ReactionAddMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: ReactionAddDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: ReactionAddDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.ReactionAddMessage; }, @@ -362,11 +387,16 @@ const ReactionRemoveDataFactory = Factory.define(( const ReactionRemoveMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: ReactionRemoveDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: ReactionRemoveDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.ReactionRemoveMessage; }, @@ -386,12 +416,19 @@ const VerificationEthAddressClaimFactory = Factory.define(({ onCreate, transientParams }) => { onCreate(async (body) => { const ethSigner = transientParams.signer ?? Eip712SignerFactory.build(); - body.address = (await ethSigner.getSignerKey())._unsafeUnwrap(); + if (!transientParams.contractSignature) { + body.address = (await ethSigner.getSignerKey())._unsafeUnwrap(); + } if (body.ethSignature.length === 0) { // Generate address and signature @@ -404,7 +441,7 @@ const VerificationAddEthAddressBodyFactory = Factory.define< blockHash: blockHash.isOk() ? blockHash.value : "0x", address: bytesToHexString(body.address)._unsafeUnwrap(), }); - body.ethSignature = (await ethSigner.signVerificationEthAddressClaim(claim))._unsafeUnwrap(); + body.ethSignature = (await ethSigner.signVerificationEthAddressClaim(claim, body.chainId))._unsafeUnwrap(); } return body; @@ -424,7 +461,11 @@ const VerificationAddEthAddressDataFactory = Factory.define< const body = data.verificationAddEthAddressBody; if (body.ethSignature.length === 0) { const signedBody = await VerificationAddEthAddressBodyFactory.create(body, { - transient: { fid: data.fid, network: data.network, signer: transientParams.signer }, + transient: { + fid: data.fid, + network: data.network, + signer: transientParams.signer, + }, }); data.verificationAddEthAddressBody = signedBody; } @@ -496,11 +537,16 @@ const UserDataAddDataFactory = Factory.define(() => { const UserDataAddMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: UserDataAddDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: UserDataAddDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.UserDataAddMessage; }, @@ -523,11 +569,16 @@ const UsernameProofDataFactory = Factory.define(() const UsernameProofMessageFactory = Factory.define( ({ onCreate, transientParams }) => { onCreate((message) => { - return MessageFactory.create(message, { transient: transientParams }) as Promise; + return MessageFactory.create(message, { + transient: transientParams, + }) as Promise; }); return MessageFactory.build( - { data: UsernameProofDataFactory.build(), signatureScheme: protobufs.SignatureScheme.ED25519 }, + { + data: UsernameProofDataFactory.build(), + signatureScheme: protobufs.SignatureScheme.ED25519, + }, { transient: transientParams }, ) as protobufs.UsernameProofMessage; }, diff --git a/packages/core/src/protobufs/generated/message.ts b/packages/core/src/protobufs/generated/message.ts index 5567b02501..b5bda2e1ff 100644 --- a/packages/core/src/protobufs/generated/message.ts +++ b/packages/core/src/protobufs/generated/message.ts @@ -431,6 +431,10 @@ export interface VerificationAddEthAddressBody { ethSignature: Uint8Array; /** Hash of the latest Ethereum block when the signature was produced */ blockHash: Uint8Array; + /** Type of verification. 0 = EOA, 1 = ERC-1271 */ + verificationType: number; + /** 0 for EOA verifications, 1 or 10 for ERC-1271 verifications */ + chainId: number; } /** Removes a Verification of any type */ @@ -1382,7 +1386,13 @@ export const ReactionBody = { }; function createBaseVerificationAddEthAddressBody(): VerificationAddEthAddressBody { - return { address: new Uint8Array(), ethSignature: new Uint8Array(), blockHash: new Uint8Array() }; + return { + address: new Uint8Array(), + ethSignature: new Uint8Array(), + blockHash: new Uint8Array(), + verificationType: 0, + chainId: 0, + }; } export const VerificationAddEthAddressBody = { @@ -1396,6 +1406,12 @@ export const VerificationAddEthAddressBody = { if (message.blockHash.length !== 0) { writer.uint32(26).bytes(message.blockHash); } + if (message.verificationType !== 0) { + writer.uint32(32).uint32(message.verificationType); + } + if (message.chainId !== 0) { + writer.uint32(40).uint32(message.chainId); + } return writer; }, @@ -1427,6 +1443,20 @@ export const VerificationAddEthAddressBody = { message.blockHash = reader.bytes(); continue; + case 4: + if (tag != 32) { + break; + } + + message.verificationType = reader.uint32(); + continue; + case 5: + if (tag != 40) { + break; + } + + message.chainId = reader.uint32(); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1441,6 +1471,8 @@ export const VerificationAddEthAddressBody = { address: isSet(object.address) ? bytesFromBase64(object.address) : new Uint8Array(), ethSignature: isSet(object.ethSignature) ? bytesFromBase64(object.ethSignature) : new Uint8Array(), blockHash: isSet(object.blockHash) ? bytesFromBase64(object.blockHash) : new Uint8Array(), + verificationType: isSet(object.verificationType) ? Number(object.verificationType) : 0, + chainId: isSet(object.chainId) ? Number(object.chainId) : 0, }; }, @@ -1454,6 +1486,8 @@ export const VerificationAddEthAddressBody = { )); message.blockHash !== undefined && (obj.blockHash = base64FromBytes(message.blockHash !== undefined ? message.blockHash : new Uint8Array())); + message.verificationType !== undefined && (obj.verificationType = Math.round(message.verificationType)); + message.chainId !== undefined && (obj.chainId = Math.round(message.chainId)); return obj; }, @@ -1468,6 +1502,8 @@ export const VerificationAddEthAddressBody = { message.address = object.address ?? new Uint8Array(); message.ethSignature = object.ethSignature ?? new Uint8Array(); message.blockHash = object.blockHash ?? new Uint8Array(); + message.verificationType = object.verificationType ?? 0; + message.chainId = object.chainId ?? 0; return message; }, }; diff --git a/packages/core/src/signers/eip712Signer.ts b/packages/core/src/signers/eip712Signer.ts index ddca7c890c..6b31fb5b3b 100644 --- a/packages/core/src/signers/eip712Signer.ts +++ b/packages/core/src/signers/eip712Signer.ts @@ -16,6 +16,9 @@ export abstract class Eip712Signer implements Signer { */ public abstract getSignerKey(): HubAsyncResult; public abstract signMessageHash(hash: Uint8Array): HubAsyncResult; - public abstract signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult; + public abstract signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId?: number, + ): HubAsyncResult; public abstract signUserNameProofClaim(claim: UserNameProofClaim): HubAsyncResult; } diff --git a/packages/core/src/signers/ethersEip712Signer.ts b/packages/core/src/signers/ethersEip712Signer.ts index 998f87d299..bc91c4db87 100644 --- a/packages/core/src/signers/ethersEip712Signer.ts +++ b/packages/core/src/signers/ethersEip712Signer.ts @@ -43,13 +43,13 @@ export class EthersEip712Signer extends Eip712Signer { return hexSignature.andThen((hex) => hexStringToBytes(hex)); } - public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult { + public async signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId = 0, + ): HubAsyncResult { + const domain = chainId === 0 ? EIP_712_FARCASTER_DOMAIN : { ...EIP_712_FARCASTER_DOMAIN, chainId }; const hexSignature = await ResultAsync.fromPromise( - this._ethersSigner.signTypedData( - EIP_712_FARCASTER_DOMAIN, - { VerificationClaim: [...EIP_712_FARCASTER_VERIFICATION_CLAIM] }, - claim, - ), + this._ethersSigner.signTypedData(domain, { VerificationClaim: [...EIP_712_FARCASTER_VERIFICATION_CLAIM] }, claim), (e) => new HubError("bad_request.invalid_param", e as Error), ); diff --git a/packages/core/src/signers/ethersV5Eip712Signer.ts b/packages/core/src/signers/ethersV5Eip712Signer.ts index dc4748b199..8eba34ca92 100644 --- a/packages/core/src/signers/ethersV5Eip712Signer.ts +++ b/packages/core/src/signers/ethersV5Eip712Signer.ts @@ -39,10 +39,14 @@ export class EthersV5Eip712Signer extends Eip712Signer { return hexSignature.andThen((hex) => hexStringToBytes(hex)); } - public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult { + public async signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId = 0, + ): HubAsyncResult { + const domain = chainId === 0 ? eip712.EIP_712_FARCASTER_DOMAIN : { ...eip712.EIP_712_FARCASTER_DOMAIN, chainId }; const hexSignature = await ResultAsync.fromPromise( this._typedDataSigner._signTypedData( - eip712.EIP_712_FARCASTER_DOMAIN, + domain, { VerificationClaim: [...eip712.EIP_712_FARCASTER_VERIFICATION_CLAIM] }, claim, ), diff --git a/packages/core/src/signers/testUtils.ts b/packages/core/src/signers/testUtils.ts index c783ff219b..2a5957c467 100644 --- a/packages/core/src/signers/testUtils.ts +++ b/packages/core/src/signers/testUtils.ts @@ -44,7 +44,7 @@ export const testEip712Signer = async (signer: Eip712Signer) => { }); test("succeeds", async () => { - const valid = await eip712.verifyVerificationEthAddressClaimSignature(claim, signature, signerKey); + const valid = await eip712.verifyVerificationEthAddressClaimSignature(claim, signature, signerKey, 0, 0); expect(valid).toEqual(ok(true)); }); diff --git a/packages/core/src/signers/viemLocalEip712Signer.ts b/packages/core/src/signers/viemLocalEip712Signer.ts index ff5b695063..fc689b37b2 100644 --- a/packages/core/src/signers/viemLocalEip712Signer.ts +++ b/packages/core/src/signers/viemLocalEip712Signer.ts @@ -44,10 +44,14 @@ export class ViemLocalEip712Signer extends Eip712Signer { return hexSignature.andThen((hex) => hexStringToBytes(hex)); } - public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult { + public async signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId = 0, + ): HubAsyncResult { + const domain = chainId === 0 ? EIP_712_FARCASTER_DOMAIN : { ...EIP_712_FARCASTER_DOMAIN, chainId }; const hexSignature = await ResultAsync.fromPromise( this._viemLocalAccount.signTypedData({ - domain: EIP_712_FARCASTER_DOMAIN, + domain, types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM }, primaryType: "VerificationClaim", message: claim, diff --git a/packages/core/src/validations.test.ts b/packages/core/src/validations.test.ts index 28b987d281..bb9e051366 100644 --- a/packages/core/src/validations.test.ts +++ b/packages/core/src/validations.test.ts @@ -262,7 +262,11 @@ describe("validateCastAddBody", () => { }); test("when text is empty", () => { - const body = Factories.CastAddBody.build({ text: "", mentions: [], mentionsPositions: [] }); + const body = Factories.CastAddBody.build({ + text: "", + mentions: [], + mentionsPositions: [], + }); expect(validations.validateCastAddBody(body)).toEqual(ok(body)); }); @@ -311,12 +315,18 @@ describe("validateCastAddBody", () => { }); test("with an embed url string over 256 ASCII characters", () => { - body = Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.random.alphaNumeric(257)] }); + body = Factories.CastAddBody.build({ + embeds: [], + embedsDeprecated: [faker.random.alphaNumeric(257)], + }); hubErrorMessage = "url > 256 bytes"; }); test("with an embed url string over 256 bytes", () => { - body = Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [`${faker.random.alphaNumeric(254)}🤓`] }); + body = Factories.CastAddBody.build({ + embeds: [], + embedsDeprecated: [`${faker.random.alphaNumeric(254)}🤓`], + }); hubErrorMessage = "url > 256 bytes"; }); }); @@ -338,12 +348,16 @@ describe("validateCastAddBody", () => { }); test("when text is null", () => { - body = Factories.CastAddBody.build({ text: null as unknown as undefined }); + body = Factories.CastAddBody.build({ + text: null as unknown as undefined, + }); hubErrorMessage = "text is missing"; }); test("when text is longer than 320 ASCII characters", () => { - body = Factories.CastAddBody.build({ text: faker.random.alphaNumeric(321) }); + body = Factories.CastAddBody.build({ + text: faker.random.alphaNumeric(321), + }); hubErrorMessage = "text > 320 bytes"; }); @@ -405,7 +419,9 @@ describe("validateCastAddBody", () => { }); test("when parent hash is missing", () => { - body = Factories.CastAddBody.build({ parentCastId: Factories.CastId.build({ hash: undefined }) }); + body = Factories.CastAddBody.build({ + parentCastId: Factories.CastId.build({ hash: undefined }), + }); hubErrorMessage = "hash is missing"; }); @@ -418,7 +434,10 @@ describe("validateCastAddBody", () => { }); test("with a parentUrl of empty string", () => { - body = Factories.CastAddBody.build({ parentUrl: "", parentCastId: undefined }); + body = Factories.CastAddBody.build({ + parentUrl: "", + parentCastId: undefined, + }); hubErrorMessage = "url < 1 byte"; }); @@ -505,7 +524,10 @@ describe("validateCastAddBody", () => { }); test("with non-integer mentionsPositions", () => { - body = Factories.CastAddBody.build({ mentions: [Factories.Fid.build()], mentionsPositions: [1.5] }); + body = Factories.CastAddBody.build({ + mentions: [Factories.Fid.build()], + mentionsPositions: [1.5], + }); hubErrorMessage = "mentionsPositions must be integers"; }); @@ -550,12 +572,17 @@ describe("validateReactionBody", () => { }); test("with invalid reaction type", () => { - body = Factories.ReactionBody.build({ type: 100 as unknown as protobufs.ReactionType }); + body = Factories.ReactionBody.build({ + type: 100 as unknown as protobufs.ReactionType, + }); hubErrorMessage = "invalid reaction type"; }); test("without target", () => { - body = Factories.ReactionBody.build({ targetCastId: undefined, targetUrl: undefined }); + body = Factories.ReactionBody.build({ + targetCastId: undefined, + targetUrl: undefined, + }); hubErrorMessage = "target is missing"; }); @@ -582,7 +609,10 @@ describe("validateReactionBody", () => { }); test("with a targetUrl of empty string", () => { - body = Factories.ReactionBody.build({ targetUrl: "", targetCastId: undefined }); + body = Factories.ReactionBody.build({ + targetUrl: "", + targetCastId: undefined, + }); hubErrorMessage = "url < 1 byte"; }); @@ -617,7 +647,9 @@ describe("validateVerificationAddEthAddressBody", () => { }); test("with missing eth address", async () => { - body = Factories.VerificationAddEthAddressBody.build({ address: undefined }); + body = Factories.VerificationAddEthAddressBody.build({ + address: undefined, + }); hubErrorMessage = "address is missing"; }); @@ -629,7 +661,9 @@ describe("validateVerificationAddEthAddressBody", () => { }); test("with missing block hash", async () => { - body = Factories.VerificationAddEthAddressBody.build({ blockHash: undefined }); + body = Factories.VerificationAddEthAddressBody.build({ + blockHash: undefined, + }); hubErrorMessage = "blockHash is missing"; }); @@ -646,7 +680,7 @@ describe("validateVerificationAddEthAddressSignature", () => { const fid = Factories.Fid.build(); const network = Factories.FarcasterNetwork.build(); - test("succeeds", async () => { + test("succeeds for eoas", async () => { const body = await Factories.VerificationAddEthAddressBody.create({}, { transient: { fid, network } }); const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); expect(result.isOk()).toBeTruthy(); @@ -656,10 +690,42 @@ describe("validateVerificationAddEthAddressSignature", () => { const body = await Factories.VerificationAddEthAddressBody.create({ ethSignature: Factories.Bytes.build({}, { transient: { length: 1 } }), }); - const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, 0); expect(result).toEqual(err(new HubError("unknown", "Cannot convert 0x to a BigInt"))); }); + test("fails with invalid verificationType", async () => { + const body = await Factories.VerificationAddEthAddressBody.create({ + ethSignature: Factories.Bytes.build({}, { transient: { length: 1 } }), + verificationType: 2, + }); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); + expect(result).toEqual(err(new HubError("bad_request.invalid_param", "Invalid verification type"))); + }); + + test("fails with invalid chainId", async () => { + const body = await Factories.VerificationAddEthAddressBody.create({ + ethSignature: Factories.Bytes.build({}, { transient: { length: 1 } }), + chainId: 7, + verificationType: 1, + }); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); + expect(result).toEqual(err(new HubError("bad_request.invalid_param", "Invalid chain ID"))); + }); + + test("succeeds for contract signatures", async () => { + const body = await Factories.VerificationAddEthAddressBody.create( + { + address: Uint8Array.from(Buffer.from("C89858205c6AdDAD842E1F58eD6c42452671885A", "hex")), + chainId: 1, + verificationType: 1, + }, + { transient: { fid, network, contractSignature: true } }, + ); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); + expect(result.isOk()).toBeTruthy(); + }); + test("fails with eth signature from different address", async () => { const blockHash = Factories.BlockHash.build(); const claim = makeVerificationEthAddressClaim(fid, ethSignerKey, network, blockHash)._unsafeUnwrap(); @@ -714,7 +780,10 @@ describe("validateUserDataAddBody", () => { }); test("succeeds for ens names", async () => { - const body = Factories.UserDataBody.build({ type: UserDataType.USERNAME, value: "averylongensname.eth" }); + const body = Factories.UserDataBody.build({ + type: UserDataType.USERNAME, + value: "averylongensname.eth", + }); expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); }); @@ -776,7 +845,9 @@ describe("validateUsernameProof", () => { }); test("when name does not end with .eth", async () => { const proof = await Factories.UsernameProofMessage.create({ - data: { usernameProofBody: { name: utf8StringToBytes("aname")._unsafeUnwrap() } }, + data: { + usernameProofBody: { name: utf8StringToBytes("aname")._unsafeUnwrap() }, + }, }); const result = await validations.validateUsernameProofBody(proof.data.usernameProofBody, proof.data); const hubError = result._unsafeUnwrapErr(); @@ -906,7 +977,10 @@ describe("validateMessageData", () => { test("fails with embedsDeprecated when timestamp is past cut-off", async () => { const data = Factories.CastAddData.build({ timestamp: validations.EMBEDS_V1_CUTOFF + 1, - castAddBody: Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.internet.url()] }), + castAddBody: Factories.CastAddBody.build({ + embeds: [], + embedsDeprecated: [faker.internet.url()], + }), }); const result = await validations.validateMessageData(data); expect(result).toEqual(err(new HubError("bad_request.validation_failure", "string embeds have been deprecated"))); @@ -915,7 +989,10 @@ describe("validateMessageData", () => { test("fails with embedsDeprecated when timestamp is at cut-off", async () => { const data = Factories.CastAddData.build({ timestamp: validations.EMBEDS_V1_CUTOFF, - castAddBody: Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.internet.url()] }), + castAddBody: Factories.CastAddBody.build({ + embeds: [], + embedsDeprecated: [faker.internet.url()], + }), }); const result = await validations.validateMessageData(data); expect(result).toEqual(err(new HubError("bad_request.validation_failure", "string embeds have been deprecated"))); @@ -924,7 +1001,10 @@ describe("validateMessageData", () => { test("succeeds with embedsDeprecated when timestamp is before cut-off", async () => { const data = Factories.CastAddData.build({ timestamp: validations.EMBEDS_V1_CUTOFF - 1, - castAddBody: Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.internet.url()] }), + castAddBody: Factories.CastAddBody.build({ + embeds: [], + embedsDeprecated: [faker.internet.url()], + }), }); const result = await validations.validateMessageData(data); expect(result).toEqual(ok(data)); diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index be2825662a..4ceb37cc81 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -8,6 +8,7 @@ import { getFarcasterTime, toFarcasterTime } from "./time"; import { makeVerificationEthAddressClaim } from "./verifications"; import { UserNameType } from "./protobufs"; import { normalize } from "viem/ens"; +import { defaultPublicClients, PublicClients } from "./eth/clients"; /** Number of seconds (10 minutes) that is appropriate for clock skew */ export const ALLOWED_CLOCK_SKEW_SECONDS = 10 * 60; @@ -113,13 +114,14 @@ export const validateEd25519PublicKey = (publicKey?: Uint8Array | null): HubResu export const validateMessage = async ( message: protobufs.Message, validationMethods: ValidationMethods = pureJSValidationMethods, + publicClients: PublicClients = defaultPublicClients, ): HubAsyncResult => { // 1. Check the message data const data = message.data; if (!data) { return err(new HubError("bad_request.validation_failure", "data is missing")); } - const validData = await validateMessageData(data); + const validData = await validateMessageData(data, publicClients); if (validData.isErr()) { return err(validData.error); } @@ -166,7 +168,10 @@ export const validateMessage = async ( return ok(message); }; -export const validateMessageData = async (data: T): HubAsyncResult => { +export const validateMessageData = async ( + data: T, + publicClients: PublicClients = defaultPublicClients, +): HubAsyncResult => { // 1. Validate fid const validFid = validateFid(data.fid); if (validFid.isErr()) { @@ -246,6 +251,7 @@ export const validateVerificationAddEthAddressSignature = async ( body: protobufs.VerificationAddEthAddressBody, fid: number, network: protobufs.FarcasterNetwork, + publicClients: PublicClients = defaultPublicClients, ): HubAsyncResult => { const reconstructedClaim = makeVerificationEthAddressClaim(fid, body.address, network, body.blockHash); if (reconstructedClaim.isErr()) { @@ -256,6 +262,9 @@ export const validateVerificationAddEthAddressSignature = async ( reconstructedClaim.value, body.ethSignature, body.address, + body.verificationType, + body.chainId, + publicClients, ); if (verificationResult.isErr()) { diff --git a/packages/hub-nodejs/src/generated/message.ts b/packages/hub-nodejs/src/generated/message.ts index 5567b02501..b5bda2e1ff 100644 --- a/packages/hub-nodejs/src/generated/message.ts +++ b/packages/hub-nodejs/src/generated/message.ts @@ -431,6 +431,10 @@ export interface VerificationAddEthAddressBody { ethSignature: Uint8Array; /** Hash of the latest Ethereum block when the signature was produced */ blockHash: Uint8Array; + /** Type of verification. 0 = EOA, 1 = ERC-1271 */ + verificationType: number; + /** 0 for EOA verifications, 1 or 10 for ERC-1271 verifications */ + chainId: number; } /** Removes a Verification of any type */ @@ -1382,7 +1386,13 @@ export const ReactionBody = { }; function createBaseVerificationAddEthAddressBody(): VerificationAddEthAddressBody { - return { address: new Uint8Array(), ethSignature: new Uint8Array(), blockHash: new Uint8Array() }; + return { + address: new Uint8Array(), + ethSignature: new Uint8Array(), + blockHash: new Uint8Array(), + verificationType: 0, + chainId: 0, + }; } export const VerificationAddEthAddressBody = { @@ -1396,6 +1406,12 @@ export const VerificationAddEthAddressBody = { if (message.blockHash.length !== 0) { writer.uint32(26).bytes(message.blockHash); } + if (message.verificationType !== 0) { + writer.uint32(32).uint32(message.verificationType); + } + if (message.chainId !== 0) { + writer.uint32(40).uint32(message.chainId); + } return writer; }, @@ -1427,6 +1443,20 @@ export const VerificationAddEthAddressBody = { message.blockHash = reader.bytes(); continue; + case 4: + if (tag != 32) { + break; + } + + message.verificationType = reader.uint32(); + continue; + case 5: + if (tag != 40) { + break; + } + + message.chainId = reader.uint32(); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1441,6 +1471,8 @@ export const VerificationAddEthAddressBody = { address: isSet(object.address) ? bytesFromBase64(object.address) : new Uint8Array(), ethSignature: isSet(object.ethSignature) ? bytesFromBase64(object.ethSignature) : new Uint8Array(), blockHash: isSet(object.blockHash) ? bytesFromBase64(object.blockHash) : new Uint8Array(), + verificationType: isSet(object.verificationType) ? Number(object.verificationType) : 0, + chainId: isSet(object.chainId) ? Number(object.chainId) : 0, }; }, @@ -1454,6 +1486,8 @@ export const VerificationAddEthAddressBody = { )); message.blockHash !== undefined && (obj.blockHash = base64FromBytes(message.blockHash !== undefined ? message.blockHash : new Uint8Array())); + message.verificationType !== undefined && (obj.verificationType = Math.round(message.verificationType)); + message.chainId !== undefined && (obj.chainId = Math.round(message.chainId)); return obj; }, @@ -1468,6 +1502,8 @@ export const VerificationAddEthAddressBody = { message.address = object.address ?? new Uint8Array(); message.ethSignature = object.ethSignature ?? new Uint8Array(); message.blockHash = object.blockHash ?? new Uint8Array(); + message.verificationType = object.verificationType ?? 0; + message.chainId = object.chainId ?? 0; return message; }, }; diff --git a/packages/hub-web/src/generated/message.ts b/packages/hub-web/src/generated/message.ts index 5567b02501..b5bda2e1ff 100644 --- a/packages/hub-web/src/generated/message.ts +++ b/packages/hub-web/src/generated/message.ts @@ -431,6 +431,10 @@ export interface VerificationAddEthAddressBody { ethSignature: Uint8Array; /** Hash of the latest Ethereum block when the signature was produced */ blockHash: Uint8Array; + /** Type of verification. 0 = EOA, 1 = ERC-1271 */ + verificationType: number; + /** 0 for EOA verifications, 1 or 10 for ERC-1271 verifications */ + chainId: number; } /** Removes a Verification of any type */ @@ -1382,7 +1386,13 @@ export const ReactionBody = { }; function createBaseVerificationAddEthAddressBody(): VerificationAddEthAddressBody { - return { address: new Uint8Array(), ethSignature: new Uint8Array(), blockHash: new Uint8Array() }; + return { + address: new Uint8Array(), + ethSignature: new Uint8Array(), + blockHash: new Uint8Array(), + verificationType: 0, + chainId: 0, + }; } export const VerificationAddEthAddressBody = { @@ -1396,6 +1406,12 @@ export const VerificationAddEthAddressBody = { if (message.blockHash.length !== 0) { writer.uint32(26).bytes(message.blockHash); } + if (message.verificationType !== 0) { + writer.uint32(32).uint32(message.verificationType); + } + if (message.chainId !== 0) { + writer.uint32(40).uint32(message.chainId); + } return writer; }, @@ -1427,6 +1443,20 @@ export const VerificationAddEthAddressBody = { message.blockHash = reader.bytes(); continue; + case 4: + if (tag != 32) { + break; + } + + message.verificationType = reader.uint32(); + continue; + case 5: + if (tag != 40) { + break; + } + + message.chainId = reader.uint32(); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1441,6 +1471,8 @@ export const VerificationAddEthAddressBody = { address: isSet(object.address) ? bytesFromBase64(object.address) : new Uint8Array(), ethSignature: isSet(object.ethSignature) ? bytesFromBase64(object.ethSignature) : new Uint8Array(), blockHash: isSet(object.blockHash) ? bytesFromBase64(object.blockHash) : new Uint8Array(), + verificationType: isSet(object.verificationType) ? Number(object.verificationType) : 0, + chainId: isSet(object.chainId) ? Number(object.chainId) : 0, }; }, @@ -1454,6 +1486,8 @@ export const VerificationAddEthAddressBody = { )); message.blockHash !== undefined && (obj.blockHash = base64FromBytes(message.blockHash !== undefined ? message.blockHash : new Uint8Array())); + message.verificationType !== undefined && (obj.verificationType = Math.round(message.verificationType)); + message.chainId !== undefined && (obj.chainId = Math.round(message.chainId)); return obj; }, @@ -1468,6 +1502,8 @@ export const VerificationAddEthAddressBody = { message.address = object.address ?? new Uint8Array(); message.ethSignature = object.ethSignature ?? new Uint8Array(); message.blockHash = object.blockHash ?? new Uint8Array(); + message.verificationType = object.verificationType ?? 0; + message.chainId = object.chainId ?? 0; return message; }, }; diff --git a/protobufs/schemas/message.proto b/protobufs/schemas/message.proto index 11569a5d33..5e3ade9081 100644 --- a/protobufs/schemas/message.proto +++ b/protobufs/schemas/message.proto @@ -1,9 +1,9 @@ syntax = "proto3"; import "username_proof.proto"; -/** - * A Message is a delta operation on the Farcaster network. The message protobuf is an envelope - * that wraps a MessageData object and contains a hash and signature which can verify its authenticity. +/** + * A Message is a delta operation on the Farcaster network. The message protobuf is an envelope + * that wraps a MessageData object and contains a hash and signature which can verify its authenticity. */ message Message { MessageData data = 1; // Contents of the message @@ -14,12 +14,12 @@ message Message { bytes signer = 6; // Public key or address of the key pair that produced the signature } - /** - * A MessageData object contains properties common to all messages and wraps a body object which + /** + * A MessageData object contains properties common to all messages and wraps a body object which * contains properties specific to the MessageType. */ message MessageData { - MessageType type = 1; // Type of message contained in the body + MessageType type = 1; // Type of message contained in the body uint64 fid = 2; // Farcaster ID of the user producing the message uint32 timestamp = 3; // Farcaster epoch timestamp in seconds FarcasterNetwork network = 4; // Farcaster network the message is intended for @@ -47,7 +47,7 @@ enum HashScheme { enum SignatureScheme { SIGNATURE_SCHEME_NONE = 0; SIGNATURE_SCHEME_ED25519 = 1; // Ed25519 signature (default) - SIGNATURE_SCHEME_EIP712 = 2; // ECDSA signature using EIP-712 scheme + SIGNATURE_SCHEME_EIP712 = 2; // ECDSA signature using EIP-712 scheme } /** Type of the MessageBody */ @@ -144,6 +144,8 @@ message VerificationAddEthAddressBody { bytes address = 1; // Ethereum address being verified bytes eth_signature = 2; // Signature produced by the user's Ethereum address bytes block_hash = 3; // Hash of the latest Ethereum block when the signature was produced + uint32 verification_type = 4; // Type of verification. 0 = EOA, 1 = ERC-1271 + uint32 chain_id = 5; // 0 for EOA verifications, 1 or 10 for ERC-1271 verifications } /** Removes a Verification of any type */