Skip to content

Commit

Permalink
feat: Support ENS names in the hub (farcasterxyz#1101)
Browse files Browse the repository at this point in the history
* feat: Add ENS username proof support

* Add UserNameProof store

* ENS name ownership validations

* Add remaining ens validations

* Generate events correctly

* Allow setting ens names on UserData and handle revoking it

* Add changeset and cli flag
  • Loading branch information
sanjayprabhu authored Jul 11, 2023
1 parent d5d65bd commit cd0ddd6
Show file tree
Hide file tree
Showing 54 changed files with 3,258 additions and 1,756 deletions.
8 changes: 8 additions & 0 deletions .changeset/rotten-keys-smell.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
---

feat: Add support for ens names
11 changes: 8 additions & 3 deletions apps/hubble/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Testnet is a sandboxed environment where you can read and write messages without

1. Create a `.env` file in your `apps/hubble` directory, substituting the relevant value for your `ETH_RPC_URL`:
```
ETH_RPC_URL=your-ETH-RPC-URL
ETH_RPC_URL=your-ETH-goerli-RPC-URL
ETH_MAINNET_RPC_URL=your-ETH-mainnet-RPC-URL
FC_NETWORK_ID=2
BOOTSTRAP_NODE=/dns/testnet1.farcaster.xyz/tcp/2282
```
Expand Down Expand Up @@ -52,7 +53,8 @@ Mainnet is Farcaster's production environment apps use and writing a message her
1. Update the `.env` file in your `apps/hubble` directory, substituting the relevant value for your `ETH_RPC_URL`:
```
# Note: this should still point to goerli and not eth mainnet
ETH_RPC_URL=your-ETH-RPC-URL
ETH_RPC_URL=your-ETH-goerli-RPC-URL
ETH_MAINNET_RPC_URL=your-ETH-mainnet-RPC-URL
FC_NETWORK_ID=1
BOOTSTRAP_NODE=/dns/nemes.farcaster.xyz/tcp/2282
```
Expand Down Expand Up @@ -93,6 +95,7 @@ Create a `.env` file in your `apps/hubble` directory, substituting the relevant

```
ETH_RPC_URL=your-ETH-RPC-URL
ETH_MAINNET_RPC_URL=your-ETH-mainnet-RPC-URL
FC_NETWORK_ID=2
BOOTSTRAP_NODE=/dns/testnet1.farcaster.xyz/tcp/2282
```
Expand Down Expand Up @@ -122,6 +125,7 @@ Mainnet is Farcaster's production environment apps use and writing a message her
```
# Note: this should still point to goerli and not eth mainnet
ETH_RPC_URL=your-ETH-RPC-URL
ETH_MAINNET_RPC_URL=your-ETH-mainnet-RPC-URL
FC_NETWORK_ID=1
BOOTSTRAP_NODE=/dns/nemes.farcaster.xyz/tcp/2282
```
Expand Down Expand Up @@ -150,7 +154,8 @@ Fetching data from Hubble requires communicating with its [gRPC](https://grpc.io
1. Go to `apps/hubble` in this repo
2. Pull the latest image: `docker pull farcasterxyz/hubble:latest`
3. Run `docker compose stop && docker compose up -d --force-recreate`
4. If you are upgrading from a non-docker deployment to docker, make sure the `.hub` and `.rocks` directories are writable for all users.
4. If you are upgrading from a non-docker deployment to docker, make sure the `.hub` and `.rocks` directories are writable for all users.
5. If you're upgrading from 1.3.3 or below, please set `ETH_MAINNET_RPC_URL=your-ETH-mainnet-RPC-URL` (if using docker) or provide the `--eth-mainnet-rpc-url` flag (if not using docker)
Check the logs to ensure your hub is running successfully:
```sh
Expand Down
2 changes: 1 addition & 1 deletion apps/hubble/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
hubble:
image: farcasterxyz/hubble:latest
restart: unless-stopped
command: ["yarn", "start", "--ip", "0.0.0.0", "--gossip-port", "2282", "--rpc-port", "2283", "--eth-rpc-url", "$ETH_RPC_URL", "--network", "$FC_NETWORK_ID", "-b", "$BOOTSTRAP_NODE"]
command: ["yarn", "start", "--ip", "0.0.0.0", "--gossip-port", "2282", "--rpc-port", "2283", "--eth-rpc-url", "$ETH_RPC_URL", "--eth-mainnet-rpc-url", "$ETH_MAINNET_RPC_URL", "--network", "$FC_NETWORK_ID", "-b", "$BOOTSTRAP_NODE"]
ports:
- '2282:2282' # Gossip
- '2283:2283' # RPC
Expand Down
2 changes: 2 additions & 0 deletions apps/hubble/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ app
.command("start")
.description("Start a Hub")
.option("-e, --eth-rpc-url <url>", "RPC URL of a Goerli Ethereum Node")
.option("-m, --eth-mainnet-rpc-url <url>", "RPC URL of a mainnet Ethereum Node")
.option("-c, --config <filepath>", "Path to a config file with options")
.option("--fir-address <address>", "The address of the Farcaster ID Registry contract")
.option("--fnr-address <address>", "The address of the Farcaster Name Registry contract")
Expand Down Expand Up @@ -308,6 +309,7 @@ app
gossipPort: hubAddressInfo.value.port,
network,
ethRpcUrl: cliOptions.ethRpcUrl ?? hubConfig.ethRpcUrl,
ethMainnetRpcUrl: cliOptions.ethMainnetRpcUrl ?? hubConfig.ethMainnetRpcUrl,
fnameServerUrl: cliOptions.fnameServerUrl ?? hubConfig.fnameServerUrl ?? DEFAULT_FNAME_SERVER_URL,
idRegistryAddress: cliOptions.firAddress ?? hubConfig.firAddress,
nameRegistryAddress: cliOptions.fnrAddress ?? hubConfig.fnrAddress,
Expand Down
2 changes: 2 additions & 0 deletions apps/hubble/src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const Config = {
id: "./.hub/default_id.protobuf",
/** Network URL of the IdRegistry Contract */
// ethRpcUrl: '',
/** ETH mainnet RPC URL */
// ethMainnetRpcUrl: '',
/** FName Registry Server URL */
// fnameServerUrl: '';
/** Address of the IdRegistry Contract */
Expand Down
16 changes: 15 additions & 1 deletion apps/hubble/src/hubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import StoreEventHandler from "./storage/stores/storeEventHandler.js";
import { FNameRegistryClient, FNameRegistryEventsProvider } from "./eth/fnameRegistryEventsProvider.js";
import { GOSSIP_PROTOCOL_VERSION } from "./network/p2p/protocol.js";
import packageJson from "./package.json" assert { type: "json" };
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

export type HubSubmitSource = "gossip" | "rpc" | "eth-provider" | "sync" | "fname-registry";

Expand Down Expand Up @@ -131,6 +133,9 @@ export interface HubOptions {
/** Network URL of the IdRegistry Contract */
ethRpcUrl?: string;

/** ETH mainnet RPC URL */
ethMainnetRpcUrl?: string;

/** FName Registry Server URL */
fnameServerUrl?: string;

Expand Down Expand Up @@ -247,6 +252,10 @@ export class Hub implements HubInterface {
log.warn("No ETH RPC URL provided, not syncing with ETH contract events");
}

if (!options.ethMainnetRpcUrl || options.ethMainnetRpcUrl === "") {
log.warn("No ETH mainnet RPC URL provided, unable to validate ens names");
}

if (options.fnameServerUrl && options.fnameServerUrl !== "") {
this.fNameRegistryEventsProvider = new FNameRegistryEventsProvider(
new FNameRegistryClient(options.fnameServerUrl),
Expand All @@ -261,7 +270,12 @@ export class Hub implements HubInterface {
lockMaxPending: options.commitLockMaxPending,
lockTimeout: options.commitLockTimeout,
});
this.engine = new Engine(this.rocksDB, options.network, eventHandler);
const mainnetClient = createPublicClient({
chain: mainnet,
transport: http(options.ethMainnetRpcUrl, { retryCount: 2 }),
});

this.engine = new Engine(this.rocksDB, options.network, eventHandler, mainnetClient);
this.syncEngine = new SyncEngine(this, this.rocksDB, this.ethRegistryProvider);

this.rpcServer = new Server(
Expand Down
14 changes: 14 additions & 0 deletions apps/hubble/src/rpc/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
SyncStatusResponse,
SyncStatus,
UserNameProof,
UsernameProofsResponse,
} from "@farcaster/hub-nodejs";
import { err, ok, Result, ResultAsync } from "neverthrow";
import { APP_NICKNAME, APP_VERSION, HubInterface } from "../hubble.js";
Expand Down Expand Up @@ -655,6 +656,19 @@ export default class Server {
},
);
},
getUserNameProofsByFid: async (call, callback) => {
const request = call.request;

const usernameProofResult = await this.engine?.getUserNameProofsByFid(request.fid);
usernameProofResult?.match(
(usernameProofs: UserNameProof[]) => {
callback(null, UsernameProofsResponse.create({ proofs: usernameProofs }));
},
(err: HubError) => {
callback(toServiceError(err));
},
);
},
getVerification: async (call, callback) => {
const request = call.request;

Expand Down
80 changes: 66 additions & 14 deletions apps/hubble/src/rpc/test/userDataService.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
import {
Message,
bytesToHexString,
bytesToUtf8String,
Factories,
FarcasterNetwork,
FidRequest,
getInsecureHubRpcClient,
HubError,
HubRpcClient,
IdRegistryEvent,
Message,
SignerAddMessage,
UserDataAddMessage,
UserDataType,
UserDataRequest,
FidRequest,
bytesToUtf8String,
Factories,
HubError,
getInsecureHubRpcClient,
HubRpcClient,
UsernameProofRequest,
UserDataType,
UserNameProof,
UsernameProofMessage,
UsernameProofRequest,
UsernameProofsResponse,
UserNameType,
} from "@farcaster/hub-nodejs";
import { Ok } from "neverthrow";
import SyncEngine from "../../network/sync/syncEngine.js";
import Server from "../server.js";
import { jestRocksDB } from "../../storage/db/jestUtils.js";
import Engine from "../../storage/engine/index.js";
import { MockHub } from "../../test/mocks.js";
import { jest } from "@jest/globals";
import { publicClient } from "../../test/utils.js";

const db = jestRocksDB("protobufs.rpc.userdataservice.test");
const network = FarcasterNetwork.TESTNET;
const engine = new Engine(db, network);
const engine = new Engine(db, network, undefined, publicClient);
const hub = new MockHub(db, engine);

let server: Server;
Expand Down Expand Up @@ -55,6 +61,9 @@ let pfpAdd: UserDataAddMessage;
let displayAdd: UserDataAddMessage;
let addFname: UserDataAddMessage;

let ensNameProof: UsernameProofMessage;
let addEnsName: UserDataAddMessage;

beforeAll(async () => {
const signerKey = (await signer.getSignerKey())._unsafeUnwrap();
custodySignerKey = (await custodySigner.getSignerKey())._unsafeUnwrap();
Expand Down Expand Up @@ -88,6 +97,39 @@ beforeAll(async () => {
},
{ transient: { signer } },
);

const custodySignerAddress = bytesToHexString(custodySignerKey)._unsafeUnwrap();

jest.spyOn(publicClient, "getEnsAddress").mockImplementation(() => {
return Promise.resolve(custodySignerAddress);
});
ensNameProof = await Factories.UsernameProofMessage.create(
{
data: {
fid,
usernameProofBody: Factories.UserNameProof.build({
fid,
owner: custodySignerKey,
name: Factories.EnsName.build(),
type: UserNameType.USERNAME_TYPE_ENS_L1,
}),
},
},
{ transient: { signer } },
);
addEnsName = await Factories.UserDataAddMessage.create(
{
data: {
fid,
userDataBody: {
type: UserDataType.FNAME,
value: bytesToUtf8String(ensNameProof.data.usernameProofBody.name)._unsafeUnwrap(),
},
timestamp: addFname.data.timestamp + 2,
},
},
{ transient: { signer } },
);
});

describe("getUserData", () => {
Expand All @@ -106,15 +148,25 @@ describe("getUserData", () => {
const display = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.DISPLAY }));
expect(Message.toJSON(display._unsafeUnwrap())).toEqual(Message.toJSON(displayAdd));

const nameProof = Factories.UserNameProof.build({ name: fname, owner: custodySignerKey });
await engine.mergeUserNameProof(nameProof);
const fnameProof = Factories.UserNameProof.build({ name: fname, owner: custodySignerKey });
await engine.mergeUserNameProof(fnameProof);

expect(await engine.mergeMessage(addFname)).toBeInstanceOf(Ok);
const fnameData = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.FNAME }));
expect(Message.toJSON(fnameData._unsafeUnwrap())).toEqual(Message.toJSON(addFname));

const usernameProof = await client.getUsernameProof(UsernameProofRequest.create({ name: nameProof.name }));
expect(UserNameProof.toJSON(usernameProof._unsafeUnwrap())).toEqual(UserNameProof.toJSON(nameProof));
const usernameProof = await client.getUsernameProof(UsernameProofRequest.create({ name: fnameProof.name }));
expect(UserNameProof.toJSON(usernameProof._unsafeUnwrap())).toEqual(UserNameProof.toJSON(fnameProof));

expect(await engine.mergeMessage(ensNameProof)).toBeInstanceOf(Ok);
const usernameProofs = await client.getUserNameProofsByFid(FidRequest.create({ fid }));
expect(UsernameProofsResponse.toJSON(usernameProofs._unsafeUnwrap())).toEqual(
UsernameProofsResponse.toJSON({ proofs: [ensNameProof.data.usernameProofBody] }),
);

expect(await engine.mergeMessage(addEnsName)).toBeInstanceOf(Ok);
const ensNameData = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.FNAME }));
expect(Message.toJSON(ensNameData._unsafeUnwrap())).toEqual(Message.toJSON(addEnsName));
});

test("fails when user data is missing", async () => {
Expand Down
8 changes: 8 additions & 0 deletions apps/hubble/src/storage/db/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const makeFidKey = (fid: number): Buffer => {
return buffer;
};

export const readFidKey = (buffer: Buffer): number => {
return buffer.readUInt32BE(0);
};

/** <user prefix byte, fid> */
export const makeUserKey = (fid: number): Buffer => {
return Buffer.concat([Buffer.from([RootPrefix.User]), makeFidKey(fid)]);
Expand Down Expand Up @@ -95,6 +99,10 @@ export const typeToSetPostfix = (type: MessageType): UserMessagePostfix => {
return UserPostfix.LinkMessage;
}

if (type === MessageType.USERNAME_PROOF) {
return UserPostfix.UsernameProofMessage;
}

throw new Error("invalid type");
};

Expand Down
10 changes: 5 additions & 5 deletions apps/hubble/src/storage/db/nameRegistryEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export const makeNameRegistryEventPrimaryKey = (fname: Uint8Array): Buffer => {
return Buffer.concat([Buffer.from([RootPrefix.NameRegistryEvent]), Buffer.from(fname)]);
};

export const makeUserNameProofPrimaryKey = (name: Uint8Array): Buffer => {
return Buffer.concat([Buffer.from([RootPrefix.UserNameProof]), Buffer.from(name)]);
export const makeFNameUserNameProofKey = (name: Uint8Array): Buffer => {
return Buffer.concat([Buffer.from([RootPrefix.FNameUserNameProof]), Buffer.from(name)]);
};

export const makeNameRegistryEventByExpiryKey = (expiry: number, fname?: Uint8Array): Buffer => {
Expand All @@ -29,7 +29,7 @@ export const getNameRegistryEvent = async (db: RocksDB, fname: Uint8Array): Prom
};

export const getUserNameProof = async (db: RocksDB, name: Uint8Array): Promise<UserNameProof> => {
const primaryKey = makeUserNameProofPrimaryKey(name);
const primaryKey = makeFNameUserNameProofKey(name);
const buffer = await db.get(primaryKey);
return UserNameProof.decode(new Uint8Array(buffer));
};
Expand Down Expand Up @@ -71,14 +71,14 @@ export const putNameRegistryEventTransaction = (txn: Transaction, event: NameReg
export const putUserNameProofTransaction = (txn: Transaction, usernameProof: UserNameProof): Transaction => {
const proofBuffer = Buffer.from(UserNameProof.encode(usernameProof).finish());

const primaryKey = makeUserNameProofPrimaryKey(usernameProof.name);
const primaryKey = makeFNameUserNameProofKey(usernameProof.name);
const putTxn = txn.put(primaryKey, proofBuffer);

return putTxn;
};

export const deleteUserNameProofTransaction = (txn: Transaction, usernameProof: UserNameProof): Transaction => {
const primaryKey = makeUserNameProofPrimaryKey(usernameProof.name);
const primaryKey = makeFNameUserNameProofKey(usernameProof.name);
const deleteTxn = txn.del(primaryKey);

return deleteTxn;
Expand Down
10 changes: 7 additions & 3 deletions apps/hubble/src/storage/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export enum RootPrefix {
HubEvents = 15,
/* The network ID that the rocksDB was created with */
Network = 16,
/* Used to store name proofs */
UserNameProof = 17,
/* Used to store fname server name proofs */
FNameUserNameProof = 17,
/* Used to store gossip network metrics */
GossipMetrics = 18,
/* Used to index user submited username proofs */
UserNameProofByName = 19,
}

/**
Expand All @@ -76,6 +78,7 @@ export enum UserPostfix {
VerificationMessage = 4,
SignerMessage = 5,
UserDataMessage = 6,
UsernameProofMessage = 7,

/** Index records (must be 86-255) */

Expand Down Expand Up @@ -117,4 +120,5 @@ export type UserMessagePostfix =
| UserPostfix.VerificationMessage
| UserPostfix.SignerMessage
| UserPostfix.ReactionMessage
| UserPostfix.UserDataMessage;
| UserPostfix.UserDataMessage
| UserPostfix.UsernameProofMessage;
Loading

0 comments on commit cd0ddd6

Please sign in to comment.