2024.10 Fractal Voting: Unlock New Possibilities with On-Chain Governance on Bitcoin

This article is written by @ccoincash and @slothnvivi.

Introduction to Fractal Voting

As we develop Fractal with the goal to be as native as possible to Bitcoin in both technology and values, it is only natural for us to think about building tools for on-chain voting and governance, to empower the community to truly adhere to the Bitcoin core values of decentralization, inclusion, and transparency.

This gave birth to Fractal Voting—an innovative initiative designed to bring decentralized staking and voting management to the Bitcoin network, achieved by leveraging Bitcoin’s UTXO (Unspent Transaction Output) system and OP_CAT (Concatenate) on the Fractal network. We encourage the community to utilize the platform proactively for governance on various issues, fostering a genuinely open, decentralized, and community-first ecosystem.

Here’s more on how Fractal Voting is built and how it functions.

How Fractal Voting Works

Fractal Voting enables the Fractal community to stake their assets and participate in governance through voting—all directly on the Fractal network.

Staking

Users stake their assets on Fractal by transferring them to a Taproot script address, effectively locking these assets on the chain. Unlike existing Bitcoin staking protocols, Fractal Voting does not impose a specific staking period, which means users can unstake anytime they wish. The script includes conditions that must be met before the assets can be unlocked, such as a cooling-off period and specific authorizations through signatures.

Voting System

Snapshot: A snapshot of staked assets is taken before a vote, and the voting power is proportional to the assets staked.

On-Chain Voting: The voting process is conducted on-chain, with each vote recorded directly on the Fractal network. This guarantees the transparency and immutability of the voting process and results.

Unlocking Assets (Unstaking) and Cooling-Off Period

When a user decides to withdraw their staked assets, they must sign the staking UTXO and wait for a cooling-off period before accessing their assets.

Key Innovations of Fractal Voting

Unlimited Staking Time with Innovative Timelock Mechanism:

Since Bitcoin lacks a smart contract layer, protocols such as Babylon enable staking by creating a covenant using the Bitcoin Script, where staking is expressed as the locking of assets for a relative lock time (ie. unlock after a specified number of blocks), this means that the assets must be staked for a predefined duration.

Fractal Voting leverages OP_CAT to remove the limitations of staking time. Users can now initiate the unstaking process whenever they decide.

Bitcoin Native Script and Security Backed by Bitcoin Miners

Built entirely with Bitcoin native script, with execution and verification by miner nodes on the Fractal network where one-third of the blocks are merge-mined by Bitcoin miners, Fractal Voting inherits the security of the Bitcoin blockchain.

Advantages of the UTXO Model

Each staking action by a user is recorded as a separate UTXO, which enables:

Independent Staking Transactions: The isolation of UTXOs significantly enhances security as attacks affecting one UTXO do not impact others.

Efficient Transaction Processing: Independent UTXOs simplifies the validation process, reduces the chances of errors, and increases the overall efficiency of the network.

Flexibility in Contract Execution: Users manage stakes and voting actions tied to specific UTXOs, free from the limitations of account-based models.

Building Fractal Voting with OP_CAT

OP_CAT is an operation introduced to Bitcoin’s scripting language that allows for the combination of different data elements, enabling the creation of more complex scripts than were previously impossible on Bitcoin.

The introduction of OP_CAT on Fractal network is key to Fractal Voting’s functionality:

  1. Verification and Control of Transactions: With OP_CAT, Bitcoin script can now read the content of its own transaction, a significant advancement that allows the script to verify and control the inputs and outputs of a transaction. For Fractal Voting, this means that the staking and voting contracts can enforce rules based on the actual transaction data, providing an additional layer of security and functionality.
  2. Merkle Tree Integration: OP_CAT allows the use and verification of Merkle trees within Bitcoin scripts. Merkle trees are a data structure that allows for efficient and secure verification of large sets of data. By integrating Merkle trees, Fractal Voting can manage and validate large-scale data, which enables Fractal Voting to scale its voting system while maintaining the integrity and decentralization of the process.

Fractal Voting represents a step forward in decentralized governance by leveraging Bitcoin’s UTXO model and OP_CAT, offering users secure, flexible, and transparent tools for staking and voting. As Fractal Voting continues to evolve, it aims to integrate more advanced features and capabilities that align wit its core mission of decentralization and community empowerment.


If you’re interested to find out more about the script implementation for Fractal Voting, please continue reading.

Fractal Voting empowers on-chain governance by allowing users to stake their Fractal Bitcoin, vote, and unlock their staked assets following a cooling period on the Fractal network. Here’s how the script works.

Locking

Locking Assets

Users stake by transferring FB into a Taproot contract. The contract code written in scrypt-ts(https://scrypt.io/) is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

import {
    assert,
    PubKey,
    Sig,
    SmartContract,
    prop,
    sha256,
    method,
    ByteString,
    toByteString,
    len,
    int2ByteString,
} from 'scrypt-ts'
import { SHPreimage, SigHashUtils } from '../utils/sigHashUtils'

export class Stake extends SmartContract {
    // a taproot output with a time lock script
    @prop()
    outputScript: ByteString

    @prop()
    xOnlyPubKey: ByteString

    // op_return with unstake data
    @prop()
    opReturnScript: ByteString

    constructor(outputScript: ByteString, xOnlyPubKey: ByteString, opReturnScript: ByteString) {
        super(...arguments)
        this.outputScript = outputScript
        this.xOnlyPubKey = xOnlyPubKey
        this.opReturnScript = opReturnScript
    }

    /**
     * Unstake user's assets
     * @param shPreimage - The preimage of tx 
     * @param sig - The schnorr signature
     * @param stakeOutputAddress - The taproot address of stake contract
     * @param remainningAmountBytes - The remaining satoshi bytes in stake contract
     * @param withdrawAmountByte - The withdraw satoshi bytes to user
     */
    @method()
    public unstake(
        shPreimage: SHPreimage,
        sig: Sig,
        stakeOutputAddress: ByteString,
        remainningAmountBytes: ByteString,
        withdrawAmountByte: ByteString
    ) {
        // check preimage
        const s = SigHashUtils.checkSHPreimage(shPreimage)
        assert(this.checkSig(s, SigHashUtils.Gx))

        // check sig
        assert(this.checkSig(sig, PubKey(this.xOnlyPubKey)), 'invalid signature')

        // verify stakeOutputAddress
        assert(sha256(int2ByteString(len(stakeOutputAddress)) + stakeOutputAddress) == shPreimage.hashSpentScripts)

        // build output
        const withdrawOutput =
            withdrawAmountByte +
            int2ByteString(len(this.outputScript)) +
            this.outputScript

        const opReturnOutput = toByteString('0000000000000000') + int2ByteString(len(this.opReturnScript)) + this.opReturnScript

        let stakeOutput = toByteString('')
        if (remainningAmountBytes !== toByteString('0000000000000000')) {
            stakeOutput =
                remainningAmountBytes + toByteString('22') + stakeOutputAddress
        }

        const hashOutputs = sha256(
            opReturnOutput + withdrawOutput + stakeOutput
        )
        assert(hashOutputs == shPreimage.hashOutputs, 'hashOutputs mismatch')
    }
}

This typescript code can be compiled into bitcoin script as follows:

1
2

32 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 OP_PUSHDATA1 128 0x7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 66 0xf40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a0310000 34 0x51206374674b66aba9f42169e03649198c4038449cd0616880df4aa0692feaaa55c3 32 0x181b2e3e19e4298b10ee71e443ef0bb72a599a55a158a023ae2061d76256164c 45 0x6a2b0300181b2e3e19e4298b10ee71e443ef0bb72a599a55a158a023ae2061d76256164c01004672616344616f 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x17 OP_PICK 1 0x11 OP_PICK OP_14 OP_PICK OP_CAT OP_13 OP_PICK OP_CAT OP_12 OP_PICK OP_CAT OP_11 OP_PICK OP_CAT OP_10 OP_PICK OP_CAT OP_9 OP_PICK OP_CAT OP_8 OP_PICK OP_CAT OP_7 OP_PICK OP_CAT OP_6 OP_PICK OP_CAT OP_5 OP_PICK OP_CAT OP_4 OP_PICK OP_CAT OP_3 OP_PICK OP_CAT OP_SHA256 1 0x13 OP_PICK OP_OVER OP_CAT OP_SHA256 OP_2 OP_PICK 1 0x7f OP_LESSTHAN OP_VERIFY OP_2 OP_PICK OP_0 OP_NUMEQUAL OP_IF 1 0x00 OP_ELSE OP_2 OP_PICK OP_ENDIF OP_OVER OP_5 OP_PICK OP_2 OP_PICK OP_CAT OP_EQUALVERIFY 1 0x16 OP_PICK OP_5 OP_PICK OP_CAT OP_4 OP_PICK OP_1ADD OP_CAT OP_TOALTSTACK OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_FROMALTSTACK OP_NIP OP_DUP OP_7 OP_PICK OP_CHECKSIGVERIFY OP_10 OP_PICK OP_3 OP_PICK OP_CHECKSIGVERIFY OP_9 OP_PICK OP_SIZE OP_NIP OP_10 OP_PICK OP_CAT OP_SHA256 1 0x15 OP_PICK OP_EQUALVERIFY OP_7 OP_PICK OP_4 OP_PICK OP_SIZE OP_NIP OP_CAT OP_4 OP_PICK OP_CAT 8 0x0000000000000000 OP_3 OP_PICK OP_SIZE OP_NIP OP_CAT OP_3 OP_PICK OP_CAT OP_0 OP_11 OP_PICK 8 0x0000000000000000 OP_EQUAL OP_NOTIF OP_11 OP_PICK 1 0x22 OP_CAT OP_13 OP_PICK OP_CAT OP_NIP OP_ENDIF OP_OVER OP_3 OP_PICK OP_CAT OP_OVER OP_CAT OP_SHA256 OP_DUP 1 0x17 OP_PICK OP_EQUAL OP_TOALTSTACK OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_FROMALTSTACK OP_NIP

As you can see, OP_CAT is used in many places in the script.

Changing the contract requires three parameters. The first is the outputScript. The second is the x only pubkey which is used to verify Schnorr signature, and the third is the script corresponding to Opreturn. The first outputScript is a taproot output that encodes a time-locking script in tap script. The specific content of the time-locking script is: typescript

1
WATING_TIME OP_CHECKSEQUENCEVERIFY OP_DROP <xOnlyPubKey> OP_CHECKSIG

Indexing Services for Staking UTXOs Search by Users: The staking script is only related to its address, so by giving an address, you can generate the corresponding p2sh, p2wsh, p2wpkh, p2tr output script for staking. User’s staked UTXO can be obtained by querying the UTXO of p2sh, p2wsh, p2tr. Global Search: To query all staking data, you need to index using the OP_RETURN in the staking transaction. The format of OP_RETURN is OP_RETURN + Dao operation data. Upon detecting this format of OP_RETURN, use the address to generate the corresponding staking script and compare output 0. If they are the same, it is a valid staking transaction. If the output 0 is spent, then remove this staking transaction.

Unlocking Assets

Unlocking Assets

When users unlock staked UTXOs in taproot, the checkSig used for signature verification employs the Schnorr signature algorithm. Therefore, when initially generating the pubKey, it’s necessary to convert the pubKey corresponding to legacy and native Segwit addresses into a x only pubKey. Moreover, when unlocking, the transaction signature must also use the Schnorr signature. Indexing User’s Unlocking Transaction Given a user’s address, you can generate the corresponding time lock output, then query the UTXOs of p2sh, p2wsh, and p2tr to obtain the user’s unlocking transactions. After the cooling period is met, the user can unlock the assets and convert them into normal UTXOs.

On-Chain Voting

During each vote, a snapshot of the current staking data is taken using the global indexer. Users voting power is determined by the the amount of staked assets in the snapshot. By writing the root of the Merkle tree into the voting contract, users would need to verify the Merkle path of the staking transaction, and the signature corresponding to the pubkey in the leaf node concurrently, ensuring that only the owner of the private key of the staked address can vote.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

import {
    assert,
    PubKey,
    Sig,
    SmartContract,
    prop,
    method,
    ByteString,
    FixedArray,
    hash160,
} from 'scrypt-ts'
import { SHPreimage, SigHashUtils } from '../utils/sigHashUtils'
import { MerkleTree, HEIGHT } from './merkleTree'

export type VoteLeaf = {
    addressType: ByteString // 1 byte
    pubKeyPrefix: ByteString // 1 byte
    xOnlyPubKey: ByteString // 32 byte
    stakeSatoshis: ByteString // 8 byte
}

export class Vote extends SmartContract {
    @prop()
    merkleRoot: ByteString

    @prop()
    proposalId: ByteString

    constructor(merkleRoot: ByteString) {
        super(...arguments)
        this.merkleRoot = merkleRoot
        this.proposalId = hash160(merkleRoot)
    }

    @method()
    public vote(
        shPreimage: SHPreimage,
        // input
        sig: Sig,
        choice: ByteString,
        // mekle tree leaf data
        leafData: VoteLeaf,
        neighbour: FixedArray<ByteString, typeof HEIGHT>,
        neighbourType: FixedArray<boolean, typeof HEIGHT>,
    ) {

        // check preimage
        const s = SigHashUtils.checkSHPreimage(shPreimage)
        assert(this.checkSig(s, SigHashUtils.Gx))

        // check sig
        assert(this.checkSig(sig, PubKey(leafData.xOnlyPubKey)), 'invalid signature')

        // verify merkle path
        const leafHash = hash160(leafData.addressType + leafData.pubKeyPrefix + leafData.xOnlyPubKey + leafData.stakeSatoshis)
        assert(MerkleTree.verifyLeaf(leafHash, neighbour, neighbourType, this.merkleRoot))
    }
}

This article is written by @ccoincash and @slothnvivi.

A big thank you for the contribution!

Fractal Builders
Oct. 2024

E-2411B (Lorenzo’s Library)
Creative Commons BY-NC-ND 4.0

Licensed under Creative Commons BY-NC-ND 4.0
Built with Hugo
Theme Stack designed by Jimmy