Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: peer id claim for strong FID<>Peer ID authentication #2164

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .changeset/happy-kangaroos-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@farcaster/hub-nodejs": patch
"@farcaster/hub-web": patch
"@farcaster/core": patch
"@farcaster/hubble": patch
---

fix: require FID on hub startup, add FID to gossip contact info
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:

- name: Run Hubble
shell: bash
run: docker run --name hub --detach -p2282:2282 -p2283:2283 farcasterxyz/hubble:test sh -c 'node build/cli.js identity create && HUBBLE_ARGS="start --rpc-port 2283 --ip 0.0.0.0 --gossip-port 2282 --eth-mainnet-rpc-url https://eth-mainnet.g.alchemy.com/v2/8cz__IXnQ5FK_GNYDlfooLzYhBAW7ta0 --l2-rpc-url https://opt-mainnet.g.alchemy.com/v2/3xWX-cWV-an3IPXmVCRXX51PpQzc-8iJ --network 3 --allowed-peers none --catchup-sync-with-snapshot false" npx pm2-runtime start pm2.config.cjs'
run: docker run --name hub --detach -p2282:2282 -p2283:2283 farcasterxyz/hubble:test sh -c 'node build/cli.js identity create && HUBBLE_ARGS="start --rpc-port 2283 --ip 0.0.0.0 --gossip-port 2282 --eth-mainnet-rpc-url https://eth-mainnet.g.alchemy.com/v2/8cz__IXnQ5FK_GNYDlfooLzYhBAW7ta0 --l2-rpc-url https://opt-mainnet.g.alchemy.com/v2/3xWX-cWV-an3IPXmVCRXX51PpQzc-8iJ --network 3 --allowed-peers none --catchup-sync-with-snapshot false --hub-operator-fid 1" npx pm2-runtime start pm2.config.cjs'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this still needs valid claim file, to be passed in via --peer-identity-claim - working on it


- name: Download grpcurl
shell: bash
Expand Down Expand Up @@ -125,6 +125,9 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install turbo
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might not be needed - CI is failing, still investigating

run: yarn global add turbo

- name: Install dependencies
run: yarn install

Expand Down
3 changes: 2 additions & 1 deletion apps/hubble/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"build:all": "yarn build:rust && yarn build:ts && yarn copy:rust",
"build": "yarn build:all",
"clean": "rimraf ./build && cargo clean --manifest-path ./src/addon/Cargo.toml",
"claim-peer-id": "node --no-warnings build/cli.js claim-peer-id",
"dev": "yarn start | yarn pino-pretty",
"lint": "yarn lint:ts && yarn lint:rust",
"lint:ts": "yarn lint:customjs && biome format src/ --write && biome check src/ --apply",
Expand Down Expand Up @@ -72,7 +73,7 @@
"@aws-sdk/client-sts": "^3.398.0",
"@aws-sdk/lib-storage": "^3.504.0",
"@chainsafe/libp2p-gossipsub": "6.2.0",
"@chainsafe/libp2p-noise": "^11.0.0 ",
"@chainsafe/libp2p-noise": "^11.0.0",
"@datastructures-js/priority-queue": "^6.3.1",
"@faker-js/faker": "~7.6.0",
"@farcaster/hub-nodejs": "^0.11.21",
Expand Down
158 changes: 121 additions & 37 deletions apps/hubble/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FarcasterNetwork, farcasterNetworkFromJSON } from "@farcaster/hub-nodejs";
import { FarcasterNetwork, farcasterNetworkFromJSON, HubResult } from "@farcaster/hub-nodejs";
import { peerIdFromString } from "@libp2p/peer-id";
import { PeerId } from "@libp2p/interface-peer-id";
import { createEd25519PeerId, createFromProtobuf, exportToProtobuf } from "@libp2p/peer-id-factory";
import { AddrInfo } from "@chainsafe/libp2p-gossipsub/types";
import { Command } from "commander";
import fs, { existsSync } from "fs";
import fs, { existsSync, writeFileSync } from "fs";
import { mkdir, readFile, writeFile } from "fs/promises";
import { Result, ResultAsync } from "neverthrow";
import { dirname, resolve } from "path";
import { dirname, join, resolve } from "path";
import {
APP_VERSION,
FARCASTER_VERSION,
Expand All @@ -22,7 +22,11 @@ import { addressInfoFromParts, hostPortFromString, ipMultiAddrStrFromAddressInfo
import { DEFAULT_RPC_CONSOLE, startConsole } from "./console/console.js";
import RocksDB, { DB_DIRECTORY } from "./storage/db/rocksdb.js";
import { parseNetwork } from "./utils/command.js";
import { Config as DefaultConfig, DEFAULT_CATCHUP_SYNC_SNAPSHOT_MESSAGE_LIMIT } from "./defaultConfig.js";
import {
Config as DefaultConfig,
DEFAULT_CATCHUP_SYNC_SNAPSHOT_MESSAGE_LIMIT,
DEFAULT_PEER_IDENTITY_FILENAME,
} from "./defaultConfig.js";
import { profileStorageUsed } from "./profile/profile.js";
import { profileRPCServer } from "./profile/rpcProfile.js";
import { profileGossipServer } from "./profile/gossipProfile.js";
Expand All @@ -37,6 +41,12 @@ import axios from "axios";
import { r2Endpoint, snapshotURLAndMetadata } from "./utils/snapshot.js";
import { DEFAULT_DIAGNOSTIC_REPORT_URL, initDiagnosticReporter } from "./utils/diagnosticReport.js";
import { ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
import {
generateClaimForPeerID,
isPeerClaimValid,
PeerIdentityClaimWithAccountSignature,
verifyPeerIdentityClaimWithAccountSignature,
} from "./peerclaim/index.js";

/** A CLI to accept options from the user and start the Hub */

Expand Down Expand Up @@ -84,6 +94,10 @@ app
.option("-i, --id <filepath>", "Path to the PeerId file.")
.option("--hub-operator-fid <fid>", "The FID of the hub operator")
.option("-c, --config <filepath>", "Path to the config file.")
.option(
"--peer-identity-claim <filepath>",
"Path to claim file, used to established secure, verifiable association between FID and Peer ID.",
)
.option("--db-name <name>", "The name of the RocksDB instance. (default: rocks.hub._default)")
.option("--process-file-prefix <prefix>", 'Prefix for file to which hub process number is written. (default: "")')
.option(
Expand Down Expand Up @@ -373,6 +387,61 @@ app

startupCheck.printStartupCheckStatus(StartupCheckStatus.OK, `Found PeerId ${peerId.toString()}`);

// Startup check for Hub Operator FID
const hubOperatorFid = parseInt(cliOptions.hubOperatorFid ?? hubConfig.hubOperatorFid);
if (!hubOperatorFid || isNaN(hubOperatorFid)) {
startupCheck.printStartupCheckStatus(
StartupCheckStatus.ERROR,
"Hub Operator FID is not set",
"https://www.thehubble.xyz/intro/install.html#troubleshooting",
);
return flushAndExit(1);
}

try {
const fid = hubOperatorFid;
const response = await axios.get(`https://fnames.farcaster.xyz/transfers/current?fid=${fid}`);
const transfer = response.data.transfer;
if (transfer?.username) {
startupCheck.printStartupCheckStatus(StartupCheckStatus.OK, `Hub Operator FID is ${fid}(${transfer.username})`);
} else {
startupCheck.printStartupCheckStatus(
StartupCheckStatus.WARNING,
`Hub Operator FID is ${fid}, but no username was found`,
);
}
} catch (e) {
logger.error(e, `Error fetching username for Hub Operator FID ${hubOperatorFid}`);
startupCheck.printStartupCheckStatus(
StartupCheckStatus.WARNING,
`Hub Operator FID is ${hubOperatorFid}, but no username was found`,
);
}

// Read peer identity claim file and verify:
// 1. Peer signature is valid
// 2. Account signature is valid
// 3. Peer ID & FID in claim match hub's values
// NOTE: An additional check is needed to verify the account key is associated with a given FID.
// Since that check requires on chain events to match FID with KeyRegistry, it is performed later in the Hub.
const peerClaimFileData = await readFile(
resolve(cliOptions.peerIdentityClaim ?? hubConfig.peerIdentityClaim),
"utf8",
);
const peerIdentityClaim: PeerIdentityClaimWithAccountSignature = JSON.parse(peerClaimFileData);
const peerClaimVerificationResult = await isPeerClaimValid(hubOperatorFid, peerId, peerIdentityClaim);
if (peerClaimVerificationResult.isErr() || !peerClaimVerificationResult.value) {
startupCheck.printStartupCheckStatus(
StartupCheckStatus.ERROR,
`Peer Identity Claim is invalid ${
peerClaimVerificationResult.isErr() ? `: ${peerClaimVerificationResult.error}` : ""
}`,
"https://www.thehubble.xyz/intro/install.html#troubleshooting",
);
return flushAndExit(1);
}
startupCheck.printStartupCheckStatus(StartupCheckStatus.OK, "Peer Identity Claim is valid");

// Read RPC Auth from 1. CLI option, 2. Environment variable, 3. Config file
let rpcAuth;
if (cliOptions.rpcAuth) {
Expand Down Expand Up @@ -529,6 +598,7 @@ app

const options: HubOptions = {
peerId,
peerIdentityClaim,
logIndividualMessages: cliOptions.logIndividualMessages ?? hubConfig.logIndividualMessages ?? false,
ipMultiAddr: ipMultiAddrResult.value,
rpcServerHost: hubAddressInfo.value.address,
Expand Down Expand Up @@ -578,42 +648,10 @@ app
disableSnapshotSync: cliOptions.disableSnapshotSync ?? hubConfig.disableSnapshotSync ?? false,
enableSnapshotToS3,
s3SnapshotBucket: cliOptions.s3SnapshotBucket ?? hubConfig.s3SnapshotBucket,
hubOperatorFid: parseInt(cliOptions.hubOperatorFid ?? hubConfig.hubOperatorFid),
hubOperatorFid,
connectToDbPeers: hubConfig.connectToDbPeers ?? true,
};

// Startup check for Hub Operator FID
if (options.hubOperatorFid && !isNaN(options.hubOperatorFid)) {
try {
const fid = options.hubOperatorFid;
const response = await axios.get(`https://fnames.farcaster.xyz/transfers?fid=${fid}`);
const transfers = response.data.transfers;
if (transfers && transfers.length > 0) {
const usernameField = transfers[transfers.length - 1].username;
if (usernameField !== null && usernameField !== undefined) {
startupCheck.printStartupCheckStatus(StartupCheckStatus.OK, `Hub Operator FID is ${fid}(${usernameField})`);
} else {
startupCheck.printStartupCheckStatus(
StartupCheckStatus.WARNING,
`Hub Operator FID is ${fid}, but no username was found`,
);
}
}
} catch (e) {
logger.error(e, `Error fetching username for Hub Operator FID ${options.hubOperatorFid}`);
startupCheck.printStartupCheckStatus(
StartupCheckStatus.WARNING,
`Hub Operator FID is ${options.hubOperatorFid}, but no username was found`,
);
}
} else {
startupCheck.printStartupCheckStatus(
StartupCheckStatus.WARNING,
"Hub Operator FID is not set",
"https://www.thehubble.xyz/intro/install.html#troubleshooting",
);
}

await startupCheck.rpcCheck(options.ethMainnetRpcUrl, mainnet, "L1");
await startupCheck.rpcCheck(options.l2RpcUrl, optimism, "L2", options.l2ChainId);

Expand Down Expand Up @@ -801,6 +839,52 @@ app
.addCommand(createIdCommand)
.addCommand(verifyIdCommand);

/*//////////////////////////////////////////////////////////////
PEER ID CLAIM COMMAND
//////////////////////////////////////////////////////////////*/
const claimPeerIdCommand = new Command("claim-peer-id")
.description("Create a signed message to claim a Peer ID for a given FID with a verified address.")
.option("-I, --id <filepath>", "Path to the PeerId file", DEFAULT_PEER_ID_LOCATION)
.option("-F, --fid <number>", "FID of the user claiming the Peer ID")
.option("-O, --output <directory>", "Directory where the generated claim should be stored", DEFAULT_PEER_ID_DIR)
.option(
"-K, --account-key <filepath>",
"Path to the account key file, where account key is a signer registered for a given FID",
)
.action(async (options) => {
const peerId = await readPeerId(options.id);
if (!options.fid || isNaN(options.fid)) {
logger.error("FID is required");
return flushAndExit(1);
}
const fid = parseInt(options.fid);
if (!options.accountKey) {
logger.error("Account key path is required");
return flushAndExit(1);
}
const privateKeyBuffer = await readFile(options.accountKey);
const accountPrivateKey = new Uint8Array(privateKeyBuffer);

const message: HubResult<PeerIdentityClaimWithAccountSignature> = await generateClaimForPeerID(
fid,
peerId,
accountPrivateKey,
);
if (message.isErr()) {
logger.error("Failed to generate claim message", message.error);
return flushAndExit(1);
}

const directory = options.output && existsSync(options.output) ? options.output : process.cwd();
const filepath = resolve(join(directory, DEFAULT_PEER_IDENTITY_FILENAME));
writeFileSync(filepath, JSON.stringify(message.value, null, 2), "utf8");

logger.info(`Wrote claim message to ${filepath}`);
logger.flush();
console.log(message.value);
});
app.addCommand(claimPeerIdCommand);

/*//////////////////////////////////////////////////////////////
STATUS COMMAND
//////////////////////////////////////////////////////////////*/
Expand Down
4 changes: 4 additions & 0 deletions apps/hubble/src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* Note: CLI options take precedence over the options specified in a config file
*/
import { DEFAULT_DIAGNOSTIC_REPORT_URL } from "./utils/diagnosticReport.js";
import { join } from "path";

export const DEFAULT_PEER_IDENTITY_FILENAME = "peer_identity_claim.json";
const DEFAULT_GOSSIP_PORT = 2282;
const DEFAULT_RPC_PORT = 2283;
const DEFAULT_HTTP_API_PORT = 2281;
Expand All @@ -15,6 +17,8 @@ export const DEFAULT_CATCHUP_SYNC_SNAPSHOT_MESSAGE_LIMIT = 50_000_000;
export const Config = {
/** Path to a PeerId file */
id: "./.hub/default_id.protobuf",
/** Path to Peer Identity claim file */
peerIdentityClaim: join(".hub", DEFAULT_PEER_IDENTITY_FILENAME),
/** ETH mainnet RPC URL */
// ethMainnetRpcUrl: '',
/** FName Registry Server URL */
Expand Down
Loading
Loading