vocdoni / vocdoni-sdk

Vocdoni SDK for API
GNU Affero General Public License v3.0
13 stars 6 forks source link

Add frame voting support to SDK #354

Open p4u opened 8 months ago

p4u commented 8 months ago

New protobuf models

There is a new protobuf Proof model:

message Proof {
    oneof payload {
                ......
        ProofFarcasterFrame farcasterFrame = 9;
    }
}
// ProofFarcasterFrame is a proof created on the Farcaster network
message ProofFarcasterFrame {
    bytes signedFrameMessageBody = 1;
    ProofArbo censusProof = 2;
    bytes publicKey = 3;
}

signedFrameMessageBody

This is a byte buffer that comes from the farcaster frame Specification

When a user interacts with a Frame backend, the backend receives the following JSON object:

{
  "untrustedData": {
    "fid": 2,
    "url": "https://farcaster.vote/poll/0x7464837316182738372612638471aabcde",
    "messageHash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974",
    "timestamp": 1706243218,
    "network": 1,
    "buttonIndex": 2,
    "inputText": "hello world", // "" if requested and no input, undefined if input not requested
    "castId": {
      "fid": 226,
      "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9"
    }
  },
  "trustedData": {
    "messageBytes": "d2b1ddc6c88e865a33cb1a565e0058d757042974..."
  }
}

reference https://docs.farcaster.xyz/reference/frames/spec#data-structures

The messageBytes is signed by the user publicKey. So in the Vochain we can extract it and check the publicKey is on the census.

censusProof

This is a standard OFF_CHAIN_TREE_WEIGHTED arbo based census proof (the same we use for standard voting). This proof is used to verify the pubKey extracted from the farcaster message is actually on the census.

publicKey

This is the publicKey extracted from the farcaster message, to facilitate the Vochain vote verification

The Arbo census

This is a standard census, but since the publicKeys of Farcaster are not Ethereum addresses but ed25519 public keys, we introduce a way to create farcaster addresses, this is: keccack256(publicKey)[:20].

So the census3 or other census based services need to implement these kind of addresses. Usually a pure farcaster census would be then:

->  key: `keccack256(publicKey1)[:20]` | weight: 1
->  key: `keccack256(publicKey2)[:20]` | weight: 1
- > key: `keccack256(publicKey3)[:20]` | weight: 1
....

Restrictions

Expected implementation on the SDK

The SDK could just facilitate create farcaster elections and help build and send farcaster votes. Meaning that we might skip the farcaster frame specific details, such as the Message Frame JSON or the Farcaster Protobuf model.

There are many libraries around that allow deserialize the farcaster message and extract the public key, we can just ask the user of the SDK to introduce such data.

p4u commented 5 months ago

The way we create census keys has changed to include the Farcaster ID (FID).

Here is the Go reference implementation

func NewFarcasterVoterID(publicKey []byte, fid uint64) []byte {
    fidBytes := make([]byte, 8)
    binary.LittleEndian.PutUint64(fidBytes, fid)
    hashedPubKey := keccack(append(publicKey, fidBytes...))
        return hashedPubKey[:20]
}

And here the proposed implementation made by chatGPT

import { keccak256 } from 'js-sha3';

/**
 * Generate a Farcaster Voter ID based on the public key and fid.
 * 
 * @param {Uint8Array} publicKey - The public key as a byte array.
 * @param {number} fid - The fid as a number.
 * @returns {Uint8Array} - The first 20 bytes of the keccak256 hash of the combined input.
 */
function newFarcasterVoterID(publicKey: Uint8Array, fid: number): Uint8Array {
    // Create an 8-byte array to hold the little-endian representation of fid
    const fidBytes = new Uint8Array(8);

    // Convert fid to a little-endian byte array
    for (let i = 0; i < 8; i++) {
        fidBytes[i] = fid & 0xff; // Get the least significant byte of fid
        fid = fid >> 8; // Shift fid right by 8 bits to process the next byte
    }

    // Create a new array to hold the combined publicKey and fidBytes
    const combined = new Uint8Array(publicKey.length + fidBytes.length);

    // Copy publicKey into the combined array
    combined.set(publicKey);

    // Copy fidBytes into the combined array, starting after the publicKey
    combined.set(fidBytes, publicKey.length);

    // Hash the combined array using keccak256 and convert the result to an ArrayBuffer
    const hashedPubKey = keccak256.arrayBuffer(combined);

    // Return the first 20 bytes of the hashed public key
    return new Uint8Array(hashedPubKey).slice(0, 20);
}

Here some test data

var frameVote1 = farcasterFrame{
    signedMessage: "0a8b01080d109fc20e18b4a7f52e200182017b0a5b68747470733a2f2f63656c6f6e692e766f63646f6e692e6e65742f3633663537626539386638303666393539323134623435383165623837393165366338306561663732313032643465393366373631303030303030303030303310011a1a089fc20e1214000000000000000000000000000000000000000112142d9bd29806c7e54cf5f80f98d9adf710a2ebc58518012240379f4f9897901b24544fba46fcb51183f79d79a5041c47f554c5a4e407c020fdbf43ef27490b944e05372850b1dc78dd97c728a88bdbc14f0174ed589c795a0928013220ec327cd438995a59ce78fddd29631e9b2e41eafc3a6946dd26b4da749f47140d",
    pubkey:        "ec327cd438995a59ce78fddd29631e9b2e41eafc3a6946dd26b4da749f47140d",
    buttonIndex:   1,
    fid:           237855,
}

var frameVote2 = farcasterFrame{
    buttonIndex:   3,
    signedMessage: "0a8901080d10e04e18d1aaf52e200182017a0a5b68747470733a2f2f63656c6f6e692e766f63646f6e692e6e65742f3633663537626539386638303666393539323134623435383165623837393165366338306561663732313032643465393366373631303030303030303030303310031a1908e04e12140000000000000000000000000000000000000001121496f560a1f5c90fa24278277321d7be35d18cf0711801224029863962ecff4b7db6dd8736fc1c238ed6ed5a147d3a36e6eac32e06f10d2dcc1df1618d5da6ce21286e0233656ef985a5b1cced2bee5f2cbb4fd1bfc168aa0128013220d6424e655287aa61df38205da19ddab23b0ff9683c6800e0dbc3e8b65d3eb2e3",
    pubkey:        "d6424e655287aa61df38205da19ddab23b0ff9683c6800e0dbc3e8b65d3eb2e3",
    fid:           10080,
}