A collection of Solidity libraries for interacting with the Farcaster messages on-chain
A set of Solidity libraries for verifying and parsing Farcaster messages onchain. Made by fastfourier.eth.
The difference between a Farcaster and many other social networks is the cryptographic protocol that allows any user to verify any action, that happened on the network. Using cryptographic signatures and an onchain key registry, it is possible to verify the correctness of the cast, like, the following relation, etc.
The goal of this project is to provide a set of Solidity libraries and examples, helping to verify and parse Farcaster messages on-chain.
Farcaster messages (cast, like, following, etc) are represented as Protobuf messages, signed with the user's private key. Here's an illustration of what happens when a new cast is sent to the Farcaster network:
KeyRegistry
on Optimism Mainnet.All these actions can be done inside the smart contract, verifying that Alice indeed sent the message!
The full example can be found at contracts/Test.sol.
function verifyCastAddMessage(
bytes32 public_key,
bytes32 signature_r,
bytes32 signature_s,
bytes memory message
) external {
MessageData memory message_data = _verifyMessage(
public_key,
signature_r,
signature_s,
message
);
if (message_data.type_ != MessageType.MESSAGE_TYPE_CAST_ADD) {
revert InvalidMessageType();
}
emit MessageCastAddVerified(
message_data.fid,
message_data.cast_add_body.text,
message_data.cast_add_body.mentions
);
}
Gas usage mainly consists of three components:
·-----------------------------------------|----------------------------|-------------|-----------------------------·
| Solc version: 0.8.19 · Optimizer enabled: false · Runs: 200 · Block limit: 30000000 gas │
··········································|····························|·············|······························
| Methods │
·············|····························|··············|·············|·············|···············|··············
| Contract · Method · Min · Max · Avg · # calls · usd (avg) │
·············|····························|··············|·············|·············|···············|··············
| Test · verifyCastAddMessage · - · - · 1922353 · 2 · - │
·············|····························|··············|·············|·············|···············|··············
| Test · verifyReactionAddMessage · - · - · 1518784 · 2 · - │
·············|····························|··············|·············|·············|···············|··············
Yes, absolutely! The main idea is to move expensive computations (Blake3 hashing and Ed25519 signature verification) offchain, using ZK proofs. In this case, the Solidity contract verifies only a short proof (≈300k gas vs 2m). This approach can be scaled with batching, so in the case of 10 messages, it takes 300k / 10 = 30k gas to verify a single message.
Most of them. Due to the contract size limitations, decodings for CastRemoveBody
, VerificationRemoveBody
, and UserNameProof
are not included. You can include them yourself, by modifying the message.proto, check out the Running Locally section.
Try to use external libraries, as they are quite heavy to be used internally. Check the test.ts for deployment reference.
Farcaster uses onchain KeyRegistry
contract (spec), which stores the relation between the user's FID (Farcaster ID) and a public key. Here's a verification example:
address KEY_REGISTRY = 0x....;
IKeyRegistry.KeyData memory keyData = IKeyRegistry(KEY_REGISTRY).keyDataOf(
messageData.fid,
bytes.concat(publicKey)
);
if (keyData.state != IKeyRegistry.KeyState.ADDED) revert InvalidKey();
Keep in mind, that KeyRegistry contact has been migrated multiple times, so make sure you use the actual address from the Farcaster Specification.
Technically it's possible, but it's quite tricky. The best way to achieve this will be by using storage proofs for KeyRegistry storage verification.
import { NextRequest, NextResponse } from "next/server";
import {
Factories,
FarcasterNetwork,
FrameActionBody,
getSSLHubRpcClient,
Message,
MessageData,
MessageType,
toFarcasterTime,
UserDataType,
} from "@farcaster/hub-nodejs";
export async function POST(req: NextRequest) {
const {
trustedData: { messageBytes },
} = await req.json();
const frameMessage = Message.decode(Buffer.from(messageBytes, "hex"));
const messageSignature = Buffer.from(message.signature).toString('hex');
const messageData: MessageData = {
type: message.data?.type as MessageType,
fid: message.data?.fid as number,
timestamp: message.data?.timestamp as number,
network: message.data?.network as FarcasterNetwork,
frameActionBody: message.data?.frameActionBody,
};
const messageEncoded = (MessageData.encode(messageData).finish());
const args = [
'0x' + Buffer.from(message.signer).toString('hex'), // public_key
'0x' + Buffer.from(messageSignature).slice(0, 32).toString('hex'), // signature_r
'0x' + Buffer.from(messageSignature).slice(32, 64).toString('hex'), // signature_s
'0x' + Buffer.from(messageEncoded).toString('hex') // message
];
// Send your transaction here
// ...
}
node --version
v18.17.1
npm --version
9.6.7
yarn
yarn hh:compile
yarn hh:test
# Get contract's size in kb
yarn hh:size
To modify .proto schemes, install protobuf3-solidity.
git clone https://github.com/celestiaorg/protobuf3-solidity
cd protobuf3-solidity
make
Export the path to the binary and run yarn protoc
to update Solidity libraries and TS definitions
export PROTO_GEN_SOL=./../protobuf3-solidity/bin/protoc-gen-sol
cd farcaster-solidity
yarn protoc
Keep in mind, that not all Protobuf features are supported (oneOf
fields, repeated strings, non-packed repeated fields). Another important thing is that message fields should be enumerated in a strict incremental order.