diff --git a/.gitignore b/.gitignore index 0fda7a0..9f11b75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .idea/ -**/README.md diff --git a/quadratic_vote/README.md b/quadratic_vote/README.md new file mode 100644 index 0000000..b243274 --- /dev/null +++ b/quadratic_vote/README.md @@ -0,0 +1,243 @@ +# Quadratic Vote + +## Summary + +`quadratic_vote` is a governance example that demonstrates Leo **library packages**. + +Two reusable libraries handle all the math: + +| Package | Kind | What it provides | +|---------|------|-----------------| +| `sqrt_math` | library | `isqrt` — integer square root via Newton's method | +| `vote_math` | library | QV weights, credit costs, voter profiles (depends on `sqrt_math`) | +| `governance.aleo` | program | Private vote records, proposal outcome checks | + +Under **Quadratic Voting (QV)**, a participant's vote weight equals ⌊√(token\_balance)⌋ and +casting *k* votes costs *k²* credits. This makes influence grow with the square root of +wealth rather than linearly — fairer to smaller holders. + +| Token balance | Vote weight | +|---------------|-------------| +| 1 | 1 | +| 100 | 10 | +| 225 | 15 | +| 1 000 | 31 | +| 10 000 | 100 (cap) | + +This example is inspired by the [Quadratic Voting](https://en.wikipedia.org/wiki/Quadratic_voting) +mechanism used in decentralised governance systems such as Gitcoin Grants. + +## Noteworthy Features + +**Library packages** — `sqrt_math` and `vote_math` are pure libraries: they have no +`.aleo` program name, are never deployed on their own, and only take effect when included +by a program package. + +**Library submodules** — each extra `.leo` file under `src/` becomes a named module. No +declaration is needed; the filename is the module name. Items are accessed as +`lib_name::module_name::item`. + +**Cross-submodule calls within a library** — `vote_math::cost::build_profile` calls +`vote_math::weight::compute` internally; `sqrt_math::checks::is_perfect_square` calls +`sqrt_math::newton::isqrt`. + +**Cross-library submodule calls** — `vote_math::weight::compute` reaches directly into +`sqrt_math::newton::isqrt`, demonstrating the full dependency chain +(`governance → vote_math::cost → vote_math::weight → sqrt_math::newton`). + +**Top-level library constants** — `vote_math::MAX_WEIGHT`, `vote_math::MIN_BALANCE`, and +`sqrt_math::ISQRT_MAX` are defined in each library's `lib.leo` and referenced by name from +within submodules. + +**Library struct export** — `vote_math::cost::VoterProfile` is defined in a submodule (field: +`weight: u32`) and used directly as a variable type inside `governance.aleo`. + +**Private vote receipt** — the `Vote` record is private; the token balance that drove the +weight computation is never revealed on-chain. + +## How to Build + +### Option A — clone this repo and build in place + +```bash +cd quadratic_vote + +cd sqrt_math && leo build && cd .. +cd vote_math && leo build && cd .. +cd governance && leo build && cd .. +``` + +### Option B — create from scratch with the Leo CLI + +```bash +# Create the two libraries and the program. +leo new --library sqrt_math +leo new --library vote_math +leo new governance + +# Wire up dependencies. +cd vote_math && leo add sqrt_math --local ../sqrt_math && cd .. +cd governance && leo add sqrt_math --local ../sqrt_math && cd .. +cd governance && leo add vote_math --local ../vote_math && cd .. + +# Copy source files, then build. +cd sqrt_math && leo build && cd .. +cd vote_math && leo build && cd .. +cd governance && leo build && cd .. +``` + +`leo new --library` creates a library package — `program.json` has no `.aleo` suffix and the +source lives in `src/lib.leo` instead of `src/main.leo`. `leo add --local` records the +dependency in `program.json` and lets the compiler resolve cross-package symbols. + +## How to Run + +All `leo run` commands are issued from inside the `governance/` directory. + +### Cast a vote + +A participant's token balance is kept private. The program derives their QV weight and +returns a private `Vote` receipt. + +``` +leo run cast_vote +``` + +**Alice (1 000 tokens, approval):** +```bash +leo run cast_vote 1field 1000u32 true +``` + +Output: +``` +{ + owner: aleo1...private, + proposal_id: 1field.private, + weight: 31u32.private, + approve: true.private, + _nonce: ...group.public, + _version: 1u8.public +} +``` + +**Bob (225 tokens, rejection):** +```bash +leo run cast_vote 1field 225u32 false +``` + +**Carol (10 000 tokens — hits the `MAX_WEIGHT` cap of 100):** +```bash +leo run cast_vote 1field 10000u32 true +``` + +### Query the credit cost of casting votes + +``` +leo run qv_cost +``` + +How much does it cost to cast 5 votes? + +```bash +leo run qv_cost 5u32 +# Output: 25u32 (= 5²) +``` + +### Check whether a number is a perfect square + +``` +leo run has_perfect_sqrt +``` + +This function bypasses `vote_math` entirely and calls `sqrt_math::checks::is_perfect_square` +directly, illustrating that a program can reach any library submodule independently. + +```bash +leo run has_perfect_sqrt 1024u32 # 1024 = 32² → true +leo run has_perfect_sqrt 1000u32 # 31² = 961, 32² = 1024 → false +``` + +### Check whether a proposal is passing + +``` +leo run is_passing +``` + +After tallying Alice (31) + Carol (100) = 131 approvals vs Bob's 15 rejections: + +```bash +leo run is_passing 131u32 15u32 +# Output: true +``` + +## Run the unit tests + +From inside `governance/`: + +```bash +leo test +``` + +18 tests covering `cast_vote`, `qv_cost`, `has_perfect_sqrt`, and `is_passing`. + +## Run the full demo + +```bash +bash run.sh +``` + +## Project structure + +Each library is split into **submodules** — separate `.leo` files under `src/` that are +automatically discovered by the compiler. No explicit `module` keyword is needed; the +filename becomes the module name. + +``` +quadratic_vote/ +├── sqrt_math/ # Library: integer square root +│ ├── program.json # "program": "sqrt_math" (no .aleo suffix) +│ └── src/ +│ ├── lib.leo # const ISQRT_MAX +│ ├── newton.leo # fn isqrt(n) — Newton's method [module: newton] +│ └── checks.leo # fn is_perfect_square(n) [module: checks] +│ +├── vote_math/ # Library: QV weight & cost formulas +│ ├── program.json # depends on sqrt_math +│ └── src/ +│ ├── lib.leo # const MAX_WEIGHT, MIN_BALANCE +│ ├── weight.leo # fn compute(balance), fn is_eligible(balance) [module: weight] +│ └── cost.leo # struct VoterProfile { weight }, [module: cost] +│ # fn credits_for_votes, fn build_profile(balance) +│ +├── governance/ # Program: private vote receipts +│ ├── program.json # depends on sqrt_math + vote_math +│ ├── inputs/governance.in +│ ├── src/main.leo # record Vote +│ │ # fn cast_vote — uses vote_math::weight + vote_math::cost +│ │ # fn qv_cost — uses vote_math::cost +│ │ # fn has_perfect_sqrt — uses sqrt_math::checks directly +│ │ # fn is_passing +│ └── tests/ +│ └── test_governance.leo # @test fns covering cast_vote, qv_cost, has_perfect_sqrt, +│ # is_passing (18 test cases) +│ +├── run.sh +└── README.md +``` + +### Call graph + +``` +governance.aleo +├── vote_math::weight::is_eligible(token_balance) +│ └── vote_math::MIN_BALANCE (top-level lib constant) +├── vote_math::cost::build_profile(token_balance) +│ └── vote_math::weight::compute(balance) (cross-submodule call within vote_math) +│ ├── sqrt_math::newton::isqrt(n) (cross-library submodule call) +│ │ └── sqrt_math::ISQRT_MAX (top-level lib constant, safety assert) +│ └── vote_math::MAX_WEIGHT (top-level lib constant) +├── vote_math::cost::credits_for_votes(votes) +└── sqrt_math::checks::is_perfect_square(n) (direct cross-library submodule call) + └── sqrt_math::newton::isqrt(n) (cross-submodule call within sqrt_math) + └── sqrt_math::ISQRT_MAX (top-level lib constant, safety assert) +``` diff --git a/quadratic_vote/governance/build/abi.json b/quadratic_vote/governance/build/abi.json new file mode 100644 index 0000000..07b3dc3 --- /dev/null +++ b/quadratic_vote/governance/build/abi.json @@ -0,0 +1,189 @@ +{ + "program": "governance.aleo", + "structs": [], + "records": [ + { + "path": [ + "Vote" + ], + "fields": [ + { + "name": "owner", + "ty": { + "Primitive": "Address" + }, + "mode": "None" + }, + { + "name": "proposal_id", + "ty": { + "Primitive": "Field" + }, + "mode": "None" + }, + { + "name": "weight", + "ty": { + "Primitive": { + "UInt": "U32" + } + }, + "mode": "None" + }, + { + "name": "approve", + "ty": { + "Primitive": "Boolean" + }, + "mode": "None" + } + ] + } + ], + "mappings": [], + "storage_variables": [], + "functions": [ + { + "name": "cast_vote", + "is_final": false, + "inputs": [ + { + "name": "proposal_id", + "ty": { + "Plaintext": { + "Primitive": "Field" + } + }, + "mode": "Public" + }, + { + "name": "token_balance", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + }, + { + "name": "approve", + "ty": { + "Plaintext": { + "Primitive": "Boolean" + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Record": { + "path": [ + "Vote" + ], + "program": "governance.aleo" + } + }, + "mode": "None" + } + ] + }, + { + "name": "qv_cost", + "is_final": false, + "inputs": [ + { + "name": "votes", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + } + ] + }, + { + "name": "has_perfect_sqrt", + "is_final": false, + "inputs": [ + { + "name": "token_balance", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": "Boolean" + } + }, + "mode": "None" + } + ] + }, + { + "name": "is_passing", + "is_final": false, + "inputs": [ + { + "name": "approvals", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "Public" + }, + { + "name": "rejections", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "Public" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": "Boolean" + } + }, + "mode": "None" + } + ] + } + ] +} \ No newline at end of file diff --git a/quadratic_vote/governance/build/imports/governance.aleo b/quadratic_vote/governance/build/imports/governance.aleo new file mode 100644 index 0000000..91fe277 --- /dev/null +++ b/quadratic_vote/governance/build/imports/governance.aleo @@ -0,0 +1,332 @@ +program governance.aleo; + +record Vote: + owner as address.private; + proposal_id as field.private; + weight as u32.private; + approve as boolean.private; + +struct VoterProfile__BuQNrNoHlO8: + weight as u32; + +function cast_vote: + input r0 as field.public; + input r1 as u32.private; + input r2 as boolean.private; + gte r1 1u32 into r3; + assert.eq r3 true; + div r1 2u32 into r4; + add r4 1u32 into r5; + is.eq r5 0u32 into r6; + ternary r6 1u32 r5 into r7; + div r1 r7 into r8; + add r5 r8 into r9; + div r9 2u32 into r10; + lt r10 r5 into r11; + ternary r11 r10 r5 into r12; + is.eq r12 0u32 into r13; + ternary r13 1u32 r12 into r14; + div r1 r14 into r15; + add r12 r15 into r16; + div r16 2u32 into r17; + lt r17 r12 into r18; + ternary r18 r17 r12 into r19; + is.eq r19 0u32 into r20; + ternary r20 1u32 r19 into r21; + div r1 r21 into r22; + add r19 r22 into r23; + div r23 2u32 into r24; + lt r24 r19 into r25; + ternary r25 r24 r19 into r26; + is.eq r26 0u32 into r27; + ternary r27 1u32 r26 into r28; + div r1 r28 into r29; + add r26 r29 into r30; + div r30 2u32 into r31; + lt r31 r26 into r32; + ternary r32 r31 r26 into r33; + is.eq r33 0u32 into r34; + ternary r34 1u32 r33 into r35; + div r1 r35 into r36; + add r33 r36 into r37; + div r37 2u32 into r38; + lt r38 r33 into r39; + ternary r39 r38 r33 into r40; + is.eq r40 0u32 into r41; + ternary r41 1u32 r40 into r42; + div r1 r42 into r43; + add r40 r43 into r44; + div r44 2u32 into r45; + lt r45 r40 into r46; + ternary r46 r45 r40 into r47; + is.eq r47 0u32 into r48; + ternary r48 1u32 r47 into r49; + div r1 r49 into r50; + add r47 r50 into r51; + div r51 2u32 into r52; + lt r52 r47 into r53; + ternary r53 r52 r47 into r54; + is.eq r54 0u32 into r55; + ternary r55 1u32 r54 into r56; + div r1 r56 into r57; + add r54 r57 into r58; + div r58 2u32 into r59; + lt r59 r54 into r60; + ternary r60 r59 r54 into r61; + is.eq r61 0u32 into r62; + ternary r62 1u32 r61 into r63; + div r1 r63 into r64; + add r61 r64 into r65; + div r65 2u32 into r66; + lt r66 r61 into r67; + ternary r67 r66 r61 into r68; + is.eq r68 0u32 into r69; + ternary r69 1u32 r68 into r70; + div r1 r70 into r71; + add r68 r71 into r72; + div r72 2u32 into r73; + lt r73 r68 into r74; + ternary r74 r73 r68 into r75; + is.eq r75 0u32 into r76; + ternary r76 1u32 r75 into r77; + div r1 r77 into r78; + add r75 r78 into r79; + div r79 2u32 into r80; + lt r80 r75 into r81; + ternary r81 r80 r75 into r82; + is.eq r82 0u32 into r83; + ternary r83 1u32 r82 into r84; + div r1 r84 into r85; + add r82 r85 into r86; + div r86 2u32 into r87; + lt r87 r82 into r88; + ternary r88 r87 r82 into r89; + is.eq r89 0u32 into r90; + ternary r90 1u32 r89 into r91; + div r1 r91 into r92; + add r89 r92 into r93; + div r93 2u32 into r94; + lt r94 r89 into r95; + ternary r95 r94 r89 into r96; + is.eq r96 0u32 into r97; + ternary r97 1u32 r96 into r98; + div r1 r98 into r99; + add r96 r99 into r100; + div r100 2u32 into r101; + lt r101 r96 into r102; + ternary r102 r101 r96 into r103; + is.eq r103 0u32 into r104; + ternary r104 1u32 r103 into r105; + div r1 r105 into r106; + add r103 r106 into r107; + div r107 2u32 into r108; + lt r108 r103 into r109; + ternary r109 r108 r103 into r110; + is.eq r110 0u32 into r111; + ternary r111 1u32 r110 into r112; + div r1 r112 into r113; + add r110 r113 into r114; + div r114 2u32 into r115; + lt r115 r110 into r116; + ternary r116 r115 r110 into r117; + is.eq r117 0u32 into r118; + ternary r118 1u32 r117 into r119; + div r1 r119 into r120; + add r117 r120 into r121; + div r121 2u32 into r122; + lt r122 r117 into r123; + ternary r123 r122 r117 into r124; + is.eq r124 0u32 into r125; + ternary r125 1u32 r124 into r126; + div r1 r126 into r127; + add r124 r127 into r128; + div r128 2u32 into r129; + lt r129 r124 into r130; + ternary r130 r129 r124 into r131; + is.eq r131 0u32 into r132; + ternary r132 1u32 r131 into r133; + div r1 r133 into r134; + add r131 r134 into r135; + div r135 2u32 into r136; + lt r136 r131 into r137; + ternary r137 r136 r131 into r138; + is.eq r138 0u32 into r139; + ternary r139 1u32 r138 into r140; + div r1 r140 into r141; + add r138 r141 into r142; + div r142 2u32 into r143; + lt r143 r138 into r144; + ternary r144 r143 r138 into r145; + lte r145 65535u32 into r146; + assert.eq r146 true; + lt r145 100u32 into r147; + ternary r147 r145 100u32 into r148; + cast r148 into r149 as VoterProfile__BuQNrNoHlO8; + cast self.signer r0 r149.weight r2 into r150 as Vote.record; + output r150 as Vote.record; + +function qv_cost: + input r0 as u32.private; + lte r0 65535u32 into r1; + assert.eq r1 true; + mul r0 r0 into r2; + output r2 as u32.private; + +function has_perfect_sqrt: + input r0 as u32.private; + div r0 2u32 into r1; + add r1 1u32 into r2; + is.eq r2 0u32 into r3; + ternary r3 1u32 r2 into r4; + div r0 r4 into r5; + add r2 r5 into r6; + div r6 2u32 into r7; + lt r7 r2 into r8; + ternary r8 r7 r2 into r9; + is.eq r9 0u32 into r10; + ternary r10 1u32 r9 into r11; + div r0 r11 into r12; + add r9 r12 into r13; + div r13 2u32 into r14; + lt r14 r9 into r15; + ternary r15 r14 r9 into r16; + is.eq r16 0u32 into r17; + ternary r17 1u32 r16 into r18; + div r0 r18 into r19; + add r16 r19 into r20; + div r20 2u32 into r21; + lt r21 r16 into r22; + ternary r22 r21 r16 into r23; + is.eq r23 0u32 into r24; + ternary r24 1u32 r23 into r25; + div r0 r25 into r26; + add r23 r26 into r27; + div r27 2u32 into r28; + lt r28 r23 into r29; + ternary r29 r28 r23 into r30; + is.eq r30 0u32 into r31; + ternary r31 1u32 r30 into r32; + div r0 r32 into r33; + add r30 r33 into r34; + div r34 2u32 into r35; + lt r35 r30 into r36; + ternary r36 r35 r30 into r37; + is.eq r37 0u32 into r38; + ternary r38 1u32 r37 into r39; + div r0 r39 into r40; + add r37 r40 into r41; + div r41 2u32 into r42; + lt r42 r37 into r43; + ternary r43 r42 r37 into r44; + is.eq r44 0u32 into r45; + ternary r45 1u32 r44 into r46; + div r0 r46 into r47; + add r44 r47 into r48; + div r48 2u32 into r49; + lt r49 r44 into r50; + ternary r50 r49 r44 into r51; + is.eq r51 0u32 into r52; + ternary r52 1u32 r51 into r53; + div r0 r53 into r54; + add r51 r54 into r55; + div r55 2u32 into r56; + lt r56 r51 into r57; + ternary r57 r56 r51 into r58; + is.eq r58 0u32 into r59; + ternary r59 1u32 r58 into r60; + div r0 r60 into r61; + add r58 r61 into r62; + div r62 2u32 into r63; + lt r63 r58 into r64; + ternary r64 r63 r58 into r65; + is.eq r65 0u32 into r66; + ternary r66 1u32 r65 into r67; + div r0 r67 into r68; + add r65 r68 into r69; + div r69 2u32 into r70; + lt r70 r65 into r71; + ternary r71 r70 r65 into r72; + is.eq r72 0u32 into r73; + ternary r73 1u32 r72 into r74; + div r0 r74 into r75; + add r72 r75 into r76; + div r76 2u32 into r77; + lt r77 r72 into r78; + ternary r78 r77 r72 into r79; + is.eq r79 0u32 into r80; + ternary r80 1u32 r79 into r81; + div r0 r81 into r82; + add r79 r82 into r83; + div r83 2u32 into r84; + lt r84 r79 into r85; + ternary r85 r84 r79 into r86; + is.eq r86 0u32 into r87; + ternary r87 1u32 r86 into r88; + div r0 r88 into r89; + add r86 r89 into r90; + div r90 2u32 into r91; + lt r91 r86 into r92; + ternary r92 r91 r86 into r93; + is.eq r93 0u32 into r94; + ternary r94 1u32 r93 into r95; + div r0 r95 into r96; + add r93 r96 into r97; + div r97 2u32 into r98; + lt r98 r93 into r99; + ternary r99 r98 r93 into r100; + is.eq r100 0u32 into r101; + ternary r101 1u32 r100 into r102; + div r0 r102 into r103; + add r100 r103 into r104; + div r104 2u32 into r105; + lt r105 r100 into r106; + ternary r106 r105 r100 into r107; + is.eq r107 0u32 into r108; + ternary r108 1u32 r107 into r109; + div r0 r109 into r110; + add r107 r110 into r111; + div r111 2u32 into r112; + lt r112 r107 into r113; + ternary r113 r112 r107 into r114; + is.eq r114 0u32 into r115; + ternary r115 1u32 r114 into r116; + div r0 r116 into r117; + add r114 r117 into r118; + div r118 2u32 into r119; + lt r119 r114 into r120; + ternary r120 r119 r114 into r121; + is.eq r121 0u32 into r122; + ternary r122 1u32 r121 into r123; + div r0 r123 into r124; + add r121 r124 into r125; + div r125 2u32 into r126; + lt r126 r121 into r127; + ternary r127 r126 r121 into r128; + is.eq r128 0u32 into r129; + ternary r129 1u32 r128 into r130; + div r0 r130 into r131; + add r128 r131 into r132; + div r132 2u32 into r133; + lt r133 r128 into r134; + ternary r134 r133 r128 into r135; + is.eq r135 0u32 into r136; + ternary r136 1u32 r135 into r137; + div r0 r137 into r138; + add r135 r138 into r139; + div r139 2u32 into r140; + lt r140 r135 into r141; + ternary r141 r140 r135 into r142; + lte r142 65535u32 into r143; + assert.eq r143 true; + mul r142 r142 into r144; + is.eq r144 r0 into r145; + output r145 as boolean.private; + +function is_passing: + input r0 as u32.public; + input r1 as u32.public; + gt r0 r1 into r2; + output r2 as boolean.private; + +constructor: + assert.eq edition 0u16; diff --git a/quadratic_vote/governance/build/imports/governance.aleo.abi.json b/quadratic_vote/governance/build/imports/governance.aleo.abi.json new file mode 100644 index 0000000..07b3dc3 --- /dev/null +++ b/quadratic_vote/governance/build/imports/governance.aleo.abi.json @@ -0,0 +1,189 @@ +{ + "program": "governance.aleo", + "structs": [], + "records": [ + { + "path": [ + "Vote" + ], + "fields": [ + { + "name": "owner", + "ty": { + "Primitive": "Address" + }, + "mode": "None" + }, + { + "name": "proposal_id", + "ty": { + "Primitive": "Field" + }, + "mode": "None" + }, + { + "name": "weight", + "ty": { + "Primitive": { + "UInt": "U32" + } + }, + "mode": "None" + }, + { + "name": "approve", + "ty": { + "Primitive": "Boolean" + }, + "mode": "None" + } + ] + } + ], + "mappings": [], + "storage_variables": [], + "functions": [ + { + "name": "cast_vote", + "is_final": false, + "inputs": [ + { + "name": "proposal_id", + "ty": { + "Plaintext": { + "Primitive": "Field" + } + }, + "mode": "Public" + }, + { + "name": "token_balance", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + }, + { + "name": "approve", + "ty": { + "Plaintext": { + "Primitive": "Boolean" + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Record": { + "path": [ + "Vote" + ], + "program": "governance.aleo" + } + }, + "mode": "None" + } + ] + }, + { + "name": "qv_cost", + "is_final": false, + "inputs": [ + { + "name": "votes", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + } + ] + }, + { + "name": "has_perfect_sqrt", + "is_final": false, + "inputs": [ + { + "name": "token_balance", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": "Boolean" + } + }, + "mode": "None" + } + ] + }, + { + "name": "is_passing", + "is_final": false, + "inputs": [ + { + "name": "approvals", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "Public" + }, + { + "name": "rejections", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U32" + } + } + }, + "mode": "Public" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": "Boolean" + } + }, + "mode": "None" + } + ] + } + ] +} \ No newline at end of file diff --git a/quadratic_vote/governance/build/imports/test_governance.aleo b/quadratic_vote/governance/build/imports/test_governance.aleo new file mode 100644 index 0000000..f33eeda --- /dev/null +++ b/quadratic_vote/governance/build/imports/test_governance.aleo @@ -0,0 +1,86 @@ +import governance.aleo; +program test_governance.aleo; + +struct VoterProfile__BuQNrNoHlO8: + weight as u32; + +function test_cast_vote_weight_10: + call governance.aleo/cast_vote 1field 100u32 true into r0; + assert.eq r0.weight 10u32; + assert.eq r0.approve true; + +function test_cast_vote_weight_31: + call governance.aleo/cast_vote 1field 1000u32 false into r0; + assert.eq r0.weight 31u32; + assert.eq r0.approve false; + +function test_cast_vote_cap: + call governance.aleo/cast_vote 1field 10000u32 true into r0; + assert.eq r0.weight 100u32; + +function test_cast_vote_min_balance: + call governance.aleo/cast_vote 2field 1u32 true into r0; + assert.eq r0.weight 1u32; + +function test_cast_vote_ineligible: + call governance.aleo/cast_vote 1field 0u32 true into r0; + +function test_qv_cost_5: + call governance.aleo/qv_cost 5u32 into r0; + assert.eq r0 25u32; + +function test_qv_cost_1: + call governance.aleo/qv_cost 1u32 into r0; + assert.eq r0 1u32; + +function test_qv_cost_10: + call governance.aleo/qv_cost 10u32 into r0; + assert.eq r0 100u32; + +function test_perfect_square_1024: + call governance.aleo/has_perfect_sqrt 1024u32 into r0; + assert.eq r0 true; + +function test_perfect_square_0: + call governance.aleo/has_perfect_sqrt 0u32 into r0; + assert.eq r0 true; + +function test_not_perfect_square_1000: + call governance.aleo/has_perfect_sqrt 1000u32 into r0; + not r0 into r1; + assert.eq r1 true; + +function test_not_perfect_square_2: + call governance.aleo/has_perfect_sqrt 2u32 into r0; + not r0 into r1; + assert.eq r1 true; + +function test_perfect_square_40000: + call governance.aleo/has_perfect_sqrt 40000u32 into r0; + assert.eq r0 true; + +function test_perfect_square_65536: + call governance.aleo/has_perfect_sqrt 65536u32 into r0; + assert.eq r0 true; + +function test_not_perfect_square_40001: + call governance.aleo/has_perfect_sqrt 40001u32 into r0; + not r0 into r1; + assert.eq r1 true; + +function test_is_passing_true: + call governance.aleo/is_passing 131u32 15u32 into r0; + assert.eq r0 true; + +function test_is_passing_tie: + call governance.aleo/is_passing 15u32 15u32 into r0; + not r0 into r1; + assert.eq r1 true; + +function test_is_passing_false: + call governance.aleo/is_passing 10u32 20u32 into r0; + not r0 into r1; + assert.eq r1 true; + +constructor: + assert.eq edition 0u16; diff --git a/quadratic_vote/governance/build/main.aleo b/quadratic_vote/governance/build/main.aleo new file mode 100644 index 0000000..91fe277 --- /dev/null +++ b/quadratic_vote/governance/build/main.aleo @@ -0,0 +1,332 @@ +program governance.aleo; + +record Vote: + owner as address.private; + proposal_id as field.private; + weight as u32.private; + approve as boolean.private; + +struct VoterProfile__BuQNrNoHlO8: + weight as u32; + +function cast_vote: + input r0 as field.public; + input r1 as u32.private; + input r2 as boolean.private; + gte r1 1u32 into r3; + assert.eq r3 true; + div r1 2u32 into r4; + add r4 1u32 into r5; + is.eq r5 0u32 into r6; + ternary r6 1u32 r5 into r7; + div r1 r7 into r8; + add r5 r8 into r9; + div r9 2u32 into r10; + lt r10 r5 into r11; + ternary r11 r10 r5 into r12; + is.eq r12 0u32 into r13; + ternary r13 1u32 r12 into r14; + div r1 r14 into r15; + add r12 r15 into r16; + div r16 2u32 into r17; + lt r17 r12 into r18; + ternary r18 r17 r12 into r19; + is.eq r19 0u32 into r20; + ternary r20 1u32 r19 into r21; + div r1 r21 into r22; + add r19 r22 into r23; + div r23 2u32 into r24; + lt r24 r19 into r25; + ternary r25 r24 r19 into r26; + is.eq r26 0u32 into r27; + ternary r27 1u32 r26 into r28; + div r1 r28 into r29; + add r26 r29 into r30; + div r30 2u32 into r31; + lt r31 r26 into r32; + ternary r32 r31 r26 into r33; + is.eq r33 0u32 into r34; + ternary r34 1u32 r33 into r35; + div r1 r35 into r36; + add r33 r36 into r37; + div r37 2u32 into r38; + lt r38 r33 into r39; + ternary r39 r38 r33 into r40; + is.eq r40 0u32 into r41; + ternary r41 1u32 r40 into r42; + div r1 r42 into r43; + add r40 r43 into r44; + div r44 2u32 into r45; + lt r45 r40 into r46; + ternary r46 r45 r40 into r47; + is.eq r47 0u32 into r48; + ternary r48 1u32 r47 into r49; + div r1 r49 into r50; + add r47 r50 into r51; + div r51 2u32 into r52; + lt r52 r47 into r53; + ternary r53 r52 r47 into r54; + is.eq r54 0u32 into r55; + ternary r55 1u32 r54 into r56; + div r1 r56 into r57; + add r54 r57 into r58; + div r58 2u32 into r59; + lt r59 r54 into r60; + ternary r60 r59 r54 into r61; + is.eq r61 0u32 into r62; + ternary r62 1u32 r61 into r63; + div r1 r63 into r64; + add r61 r64 into r65; + div r65 2u32 into r66; + lt r66 r61 into r67; + ternary r67 r66 r61 into r68; + is.eq r68 0u32 into r69; + ternary r69 1u32 r68 into r70; + div r1 r70 into r71; + add r68 r71 into r72; + div r72 2u32 into r73; + lt r73 r68 into r74; + ternary r74 r73 r68 into r75; + is.eq r75 0u32 into r76; + ternary r76 1u32 r75 into r77; + div r1 r77 into r78; + add r75 r78 into r79; + div r79 2u32 into r80; + lt r80 r75 into r81; + ternary r81 r80 r75 into r82; + is.eq r82 0u32 into r83; + ternary r83 1u32 r82 into r84; + div r1 r84 into r85; + add r82 r85 into r86; + div r86 2u32 into r87; + lt r87 r82 into r88; + ternary r88 r87 r82 into r89; + is.eq r89 0u32 into r90; + ternary r90 1u32 r89 into r91; + div r1 r91 into r92; + add r89 r92 into r93; + div r93 2u32 into r94; + lt r94 r89 into r95; + ternary r95 r94 r89 into r96; + is.eq r96 0u32 into r97; + ternary r97 1u32 r96 into r98; + div r1 r98 into r99; + add r96 r99 into r100; + div r100 2u32 into r101; + lt r101 r96 into r102; + ternary r102 r101 r96 into r103; + is.eq r103 0u32 into r104; + ternary r104 1u32 r103 into r105; + div r1 r105 into r106; + add r103 r106 into r107; + div r107 2u32 into r108; + lt r108 r103 into r109; + ternary r109 r108 r103 into r110; + is.eq r110 0u32 into r111; + ternary r111 1u32 r110 into r112; + div r1 r112 into r113; + add r110 r113 into r114; + div r114 2u32 into r115; + lt r115 r110 into r116; + ternary r116 r115 r110 into r117; + is.eq r117 0u32 into r118; + ternary r118 1u32 r117 into r119; + div r1 r119 into r120; + add r117 r120 into r121; + div r121 2u32 into r122; + lt r122 r117 into r123; + ternary r123 r122 r117 into r124; + is.eq r124 0u32 into r125; + ternary r125 1u32 r124 into r126; + div r1 r126 into r127; + add r124 r127 into r128; + div r128 2u32 into r129; + lt r129 r124 into r130; + ternary r130 r129 r124 into r131; + is.eq r131 0u32 into r132; + ternary r132 1u32 r131 into r133; + div r1 r133 into r134; + add r131 r134 into r135; + div r135 2u32 into r136; + lt r136 r131 into r137; + ternary r137 r136 r131 into r138; + is.eq r138 0u32 into r139; + ternary r139 1u32 r138 into r140; + div r1 r140 into r141; + add r138 r141 into r142; + div r142 2u32 into r143; + lt r143 r138 into r144; + ternary r144 r143 r138 into r145; + lte r145 65535u32 into r146; + assert.eq r146 true; + lt r145 100u32 into r147; + ternary r147 r145 100u32 into r148; + cast r148 into r149 as VoterProfile__BuQNrNoHlO8; + cast self.signer r0 r149.weight r2 into r150 as Vote.record; + output r150 as Vote.record; + +function qv_cost: + input r0 as u32.private; + lte r0 65535u32 into r1; + assert.eq r1 true; + mul r0 r0 into r2; + output r2 as u32.private; + +function has_perfect_sqrt: + input r0 as u32.private; + div r0 2u32 into r1; + add r1 1u32 into r2; + is.eq r2 0u32 into r3; + ternary r3 1u32 r2 into r4; + div r0 r4 into r5; + add r2 r5 into r6; + div r6 2u32 into r7; + lt r7 r2 into r8; + ternary r8 r7 r2 into r9; + is.eq r9 0u32 into r10; + ternary r10 1u32 r9 into r11; + div r0 r11 into r12; + add r9 r12 into r13; + div r13 2u32 into r14; + lt r14 r9 into r15; + ternary r15 r14 r9 into r16; + is.eq r16 0u32 into r17; + ternary r17 1u32 r16 into r18; + div r0 r18 into r19; + add r16 r19 into r20; + div r20 2u32 into r21; + lt r21 r16 into r22; + ternary r22 r21 r16 into r23; + is.eq r23 0u32 into r24; + ternary r24 1u32 r23 into r25; + div r0 r25 into r26; + add r23 r26 into r27; + div r27 2u32 into r28; + lt r28 r23 into r29; + ternary r29 r28 r23 into r30; + is.eq r30 0u32 into r31; + ternary r31 1u32 r30 into r32; + div r0 r32 into r33; + add r30 r33 into r34; + div r34 2u32 into r35; + lt r35 r30 into r36; + ternary r36 r35 r30 into r37; + is.eq r37 0u32 into r38; + ternary r38 1u32 r37 into r39; + div r0 r39 into r40; + add r37 r40 into r41; + div r41 2u32 into r42; + lt r42 r37 into r43; + ternary r43 r42 r37 into r44; + is.eq r44 0u32 into r45; + ternary r45 1u32 r44 into r46; + div r0 r46 into r47; + add r44 r47 into r48; + div r48 2u32 into r49; + lt r49 r44 into r50; + ternary r50 r49 r44 into r51; + is.eq r51 0u32 into r52; + ternary r52 1u32 r51 into r53; + div r0 r53 into r54; + add r51 r54 into r55; + div r55 2u32 into r56; + lt r56 r51 into r57; + ternary r57 r56 r51 into r58; + is.eq r58 0u32 into r59; + ternary r59 1u32 r58 into r60; + div r0 r60 into r61; + add r58 r61 into r62; + div r62 2u32 into r63; + lt r63 r58 into r64; + ternary r64 r63 r58 into r65; + is.eq r65 0u32 into r66; + ternary r66 1u32 r65 into r67; + div r0 r67 into r68; + add r65 r68 into r69; + div r69 2u32 into r70; + lt r70 r65 into r71; + ternary r71 r70 r65 into r72; + is.eq r72 0u32 into r73; + ternary r73 1u32 r72 into r74; + div r0 r74 into r75; + add r72 r75 into r76; + div r76 2u32 into r77; + lt r77 r72 into r78; + ternary r78 r77 r72 into r79; + is.eq r79 0u32 into r80; + ternary r80 1u32 r79 into r81; + div r0 r81 into r82; + add r79 r82 into r83; + div r83 2u32 into r84; + lt r84 r79 into r85; + ternary r85 r84 r79 into r86; + is.eq r86 0u32 into r87; + ternary r87 1u32 r86 into r88; + div r0 r88 into r89; + add r86 r89 into r90; + div r90 2u32 into r91; + lt r91 r86 into r92; + ternary r92 r91 r86 into r93; + is.eq r93 0u32 into r94; + ternary r94 1u32 r93 into r95; + div r0 r95 into r96; + add r93 r96 into r97; + div r97 2u32 into r98; + lt r98 r93 into r99; + ternary r99 r98 r93 into r100; + is.eq r100 0u32 into r101; + ternary r101 1u32 r100 into r102; + div r0 r102 into r103; + add r100 r103 into r104; + div r104 2u32 into r105; + lt r105 r100 into r106; + ternary r106 r105 r100 into r107; + is.eq r107 0u32 into r108; + ternary r108 1u32 r107 into r109; + div r0 r109 into r110; + add r107 r110 into r111; + div r111 2u32 into r112; + lt r112 r107 into r113; + ternary r113 r112 r107 into r114; + is.eq r114 0u32 into r115; + ternary r115 1u32 r114 into r116; + div r0 r116 into r117; + add r114 r117 into r118; + div r118 2u32 into r119; + lt r119 r114 into r120; + ternary r120 r119 r114 into r121; + is.eq r121 0u32 into r122; + ternary r122 1u32 r121 into r123; + div r0 r123 into r124; + add r121 r124 into r125; + div r125 2u32 into r126; + lt r126 r121 into r127; + ternary r127 r126 r121 into r128; + is.eq r128 0u32 into r129; + ternary r129 1u32 r128 into r130; + div r0 r130 into r131; + add r128 r131 into r132; + div r132 2u32 into r133; + lt r133 r128 into r134; + ternary r134 r133 r128 into r135; + is.eq r135 0u32 into r136; + ternary r136 1u32 r135 into r137; + div r0 r137 into r138; + add r135 r138 into r139; + div r139 2u32 into r140; + lt r140 r135 into r141; + ternary r141 r140 r135 into r142; + lte r142 65535u32 into r143; + assert.eq r143 true; + mul r142 r142 into r144; + is.eq r144 r0 into r145; + output r145 as boolean.private; + +function is_passing: + input r0 as u32.public; + input r1 as u32.public; + gt r0 r1 into r2; + output r2 as boolean.private; + +constructor: + assert.eq edition 0u16; diff --git a/quadratic_vote/governance/build/program.json b/quadratic_vote/governance/build/program.json new file mode 100644 index 0000000..a61840a --- /dev/null +++ b/quadratic_vote/governance/build/program.json @@ -0,0 +1,9 @@ +{ + "program": "governance.aleo", + "version": "0.1.0", + "description": "", + "license": "", + "leo": "4.0.1", + "dependencies": null, + "dev_dependencies": null +} diff --git a/quadratic_vote/governance/inputs/governance.in b/quadratic_vote/governance/inputs/governance.in new file mode 100644 index 0000000..b3e895d --- /dev/null +++ b/quadratic_vote/governance/inputs/governance.in @@ -0,0 +1,4 @@ +// cast_vote inputs: proposal_id (public field), token_balance (u32), approve (bool) +1field +1000u32 +true diff --git a/quadratic_vote/governance/program.json b/quadratic_vote/governance/program.json new file mode 100644 index 0000000..31e8fd0 --- /dev/null +++ b/quadratic_vote/governance/program.json @@ -0,0 +1,21 @@ +{ + "program": "governance.aleo", + "version": "0.1.0", + "description": "Quadratic-voting governance program.", + "license": "MIT", + "leo": "4.0.1", + "dependencies": [ + { + "name": "sqrt_math", + "location": "local", + "path": "../sqrt_math", + "edition": null + }, + { + "name": "vote_math", + "location": "local", + "path": "../vote_math", + "edition": null + } + ] +} diff --git a/quadratic_vote/governance/src/main.leo b/quadratic_vote/governance/src/main.leo new file mode 100644 index 0000000..c51b13b --- /dev/null +++ b/quadratic_vote/governance/src/main.leo @@ -0,0 +1,40 @@ +program governance.aleo { + // Private on-chain receipt proving a participant voted on a proposal. + record Vote { + owner: address, + proposal_id: field, + weight: u32, + approve: bool, + } + + // Cast a private vote on proposal `proposal_id`. + // + // `token_balance` is the caller's private holding; the quadratic voting + // weight is derived from it via vote_math::weight. Returns a Vote receipt + // that proves participation without revealing the token balance on-chain. + fn cast_vote(public proposal_id: field, token_balance: u32, approve: bool) -> Vote { + assert(vote_math::weight::is_eligible(token_balance)); + let profile: vote_math::cost::VoterProfile = vote_math::cost::build_profile(token_balance); + return Vote { owner: self.signer, proposal_id, weight: profile.weight, approve }; + } + + // Returns the credit cost of casting `votes` weighted votes under QV. + fn qv_cost(votes: u32) -> u32 { + return vote_math::cost::credits_for_votes(votes); + } + + // Returns true when `token_balance` is a perfect square. + // Delegates directly to sqrt_math::checks, bypassing vote_math entirely, + // to illustrate that a program can reach into any library submodule. + fn has_perfect_sqrt(token_balance: u32) -> bool { + return sqrt_math::checks::is_perfect_square(token_balance); + } + + // Returns true when weighted approvals exceed weighted rejections. + fn is_passing(public approvals: u32, public rejections: u32) -> bool { + return approvals > rejections; + } + + @noupgrade + constructor() {} +} diff --git a/quadratic_vote/governance/tests/test_governance.leo b/quadratic_vote/governance/tests/test_governance.leo new file mode 100644 index 0000000..0f6ee7d --- /dev/null +++ b/quadratic_vote/governance/tests/test_governance.leo @@ -0,0 +1,121 @@ +import governance.aleo; + +program test_governance.aleo { + // A voter with 100 tokens gets weight floor(sqrt(100)) = 10. + @test + fn test_cast_vote_weight_10() { + let v: governance.aleo::Vote = governance.aleo::cast_vote(1field, 100u32, true); + assert_eq(v.weight, 10u32); + assert_eq(v.approve, true); + } + + // A voter with 1 000 tokens gets weight floor(sqrt(1000)) = 31. + @test + fn test_cast_vote_weight_31() { + let v: governance.aleo::Vote = governance.aleo::cast_vote(1field, 1000u32, false); + assert_eq(v.weight, 31u32); + assert_eq(v.approve, false); + } + + // Weight is capped at MAX_WEIGHT (100) for very large balances. + @test + fn test_cast_vote_cap() { + let v: governance.aleo::Vote = governance.aleo::cast_vote(1field, 10000u32, true); + assert_eq(v.weight, 100u32); + } + + // A voter with 1 token (MIN_BALANCE) is eligible and gets weight 1. + @test + fn test_cast_vote_min_balance() { + let v: governance.aleo::Vote = governance.aleo::cast_vote(2field, 1u32, true); + assert_eq(v.weight, 1u32); + } + + // A voter with 0 tokens must be rejected (MIN_BALANCE = 1). + @test + @should_fail + fn test_cast_vote_ineligible() { + let v: governance.aleo::Vote = governance.aleo::cast_vote(1field, 0u32, true); + } + + // Casting 5 votes costs 5² = 25 credits. + @test + fn test_qv_cost_5() { + assert_eq(governance.aleo::qv_cost(5u32), 25u32); + } + + // Casting 1 vote costs 1² = 1 credit. + @test + fn test_qv_cost_1() { + assert_eq(governance.aleo::qv_cost(1u32), 1u32); + } + + // Casting 10 votes costs 10² = 100 credits. + @test + fn test_qv_cost_10() { + assert_eq(governance.aleo::qv_cost(10u32), 100u32); + } + + // 1 024 = 32² is a perfect square. + @test + fn test_perfect_square_1024() { + assert(governance.aleo::has_perfect_sqrt(1024u32)); + } + + // 0 = 0² is a perfect square. + @test + fn test_perfect_square_0() { + assert(governance.aleo::has_perfect_sqrt(0u32)); + } + + // 1 000 is not a perfect square (31² = 961, 32² = 1 024). + @test + fn test_not_perfect_square_1000() { + assert(!governance.aleo::has_perfect_sqrt(1000u32)); + } + + // 2 is not a perfect square. + @test + fn test_not_perfect_square_2() { + assert(!governance.aleo::has_perfect_sqrt(2u32)); + } + + // 40 000 = 200² is a perfect square (covers convergence for larger inputs). + @test + fn test_perfect_square_40000() { + assert(governance.aleo::has_perfect_sqrt(40000u32)); + } + + // 65 536 = 256² is a perfect square (covers convergence near the u16 boundary). + @test + fn test_perfect_square_65536() { + assert(governance.aleo::has_perfect_sqrt(65536u32)); + } + + // 40 001 is not a perfect square (200² = 40 000, 201² = 40 401). + @test + fn test_not_perfect_square_40001() { + assert(!governance.aleo::has_perfect_sqrt(40001u32)); + } + + // A proposal with more approvals than rejections is passing. + @test + fn test_is_passing_true() { + assert(governance.aleo::is_passing(131u32, 15u32)); + } + + // A tied proposal is not passing (strict majority required). + @test + fn test_is_passing_tie() { + assert(!governance.aleo::is_passing(15u32, 15u32)); + } + + // A proposal with fewer approvals than rejections is not passing. + @test + fn test_is_passing_false() { + assert(!governance.aleo::is_passing(10u32, 20u32)); + } + + @noupgrade + constructor() {} +} diff --git a/quadratic_vote/run.sh b/quadratic_vote/run.sh new file mode 100755 index 0000000..f680feb --- /dev/null +++ b/quadratic_vote/run.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Quadratic Vote — full walkthrough. +# Run from the quadratic_vote/ directory. + +cd governance || exit 1 + +echo "" +echo "=== Step 1: Alice (1 000 tokens) approves proposal 1 ===" +echo " weight = floor(sqrt(1000)) = 31" +leo run cast_vote 1field 1000u32 true + +echo "" +echo "=== Step 2: Bob (225 tokens) rejects proposal 1 ===" +echo " weight = floor(sqrt(225)) = 15" +leo run cast_vote 1field 225u32 false + +echo "" +echo "=== Step 3: Carol (10 000 tokens) approves proposal 1 ===" +echo " weight = floor(sqrt(10000)) = 100 (hits MAX_WEIGHT cap)" +leo run cast_vote 1field 10000u32 true + +echo "" +echo "=== Step 4: Credit cost of casting 5 votes (= 5² = 25) ===" +leo run qv_cost 5u32 + +echo "" +echo "=== Step 5: Does proposal 1 pass? (131 approvals vs 15 rejections) ===" +leo run is_passing 131u32 15u32 + +echo "" +echo "=== Step 6: Perfect-square check via sqrt_math::checks ===" +echo " 1024 = 32² → true" +leo run has_perfect_sqrt 1024u32 +echo " 1000 is not a perfect square → false" +leo run has_perfect_sqrt 1000u32 + +echo "" +echo "=== Step 7: Unit tests (leo test) ===" +leo test diff --git a/quadratic_vote/sqrt_math/program.json b/quadratic_vote/sqrt_math/program.json new file mode 100644 index 0000000..0e786b7 --- /dev/null +++ b/quadratic_vote/sqrt_math/program.json @@ -0,0 +1,7 @@ +{ + "program": "sqrt_math", + "version": "0.1.0", + "description": "Integer square root library.", + "license": "MIT", + "leo": "4.0.1" +} diff --git a/quadratic_vote/sqrt_math/src/checks.leo b/quadratic_vote/sqrt_math/src/checks.leo new file mode 100644 index 0000000..2435163 --- /dev/null +++ b/quadratic_vote/sqrt_math/src/checks.leo @@ -0,0 +1,5 @@ +// Returns true when n is a perfect square (i.e., floor(sqrt(n))^2 == n). +fn is_perfect_square(n: u32) -> bool { + let s: u32 = sqrt_math::newton::isqrt(n); + return s * s == n; +} diff --git a/quadratic_vote/sqrt_math/src/lib.leo b/quadratic_vote/sqrt_math/src/lib.leo new file mode 100644 index 0000000..65e7a6d --- /dev/null +++ b/quadratic_vote/sqrt_math/src/lib.leo @@ -0,0 +1,2 @@ +// Upper bound on floor(sqrt(n)) for any n: u32 (since sqrt(2^32 - 1) = 65535). +const ISQRT_MAX: u32 = 65_535u32; diff --git a/quadratic_vote/sqrt_math/src/newton.leo b/quadratic_vote/sqrt_math/src/newton.leo new file mode 100644 index 0000000..4b93b4d --- /dev/null +++ b/quadratic_vote/sqrt_math/src/newton.leo @@ -0,0 +1,19 @@ +// Returns floor(sqrt(n)) for any n: u32, including 0 and 1. +// Result is always in [0, 65_535] since sqrt(2^32 − 1) = 65_535. +// +// Uses twenty iterations of Newton's method: x ← (x + n/x) / 2. +// Starting from n/2 + 1 guarantees x ≥ 1 initially; twenty iterations +// are sufficient to converge for all n: u32. +// Because Aleo executes all loop iterations in circuit mode (no early exit), +// the divisor must be guarded so that x = 0 (reachable only when n = 0) +// never causes integer division by zero. +fn isqrt(n: u32) -> u32 { + let x: u32 = n / 2u32 + 1u32; + for i: u8 in 0u8..20u8 { + let divisor: u32 = x == 0u32 ? 1u32 : x; + let next: u32 = (x + n / divisor) / 2u32; + x = next < x ? next : x; + } + assert(x <= sqrt_math::ISQRT_MAX); + return x; +} diff --git a/quadratic_vote/vote_math/program.json b/quadratic_vote/vote_math/program.json new file mode 100644 index 0000000..1fdd1a2 --- /dev/null +++ b/quadratic_vote/vote_math/program.json @@ -0,0 +1,15 @@ +{ + "program": "vote_math", + "version": "0.1.0", + "description": "Quadratic-voting weight and credit-cost library.", + "license": "MIT", + "leo": "4.0.1", + "dependencies": [ + { + "name": "sqrt_math", + "location": "local", + "path": "../sqrt_math", + "edition": null + } + ] +} diff --git a/quadratic_vote/vote_math/src/cost.leo b/quadratic_vote/vote_math/src/cost.leo new file mode 100644 index 0000000..6e13dab --- /dev/null +++ b/quadratic_vote/vote_math/src/cost.leo @@ -0,0 +1,19 @@ +// Computed voting parameters for a participant. +struct VoterProfile { + weight: u32, +} + +// Returns the credit cost of casting `votes` weighted votes under QV. +// Cost equals votes², so each additional vote is progressively more expensive. +// votes must be ≤ 65_535 to avoid u32 overflow (65_535² = 4_294_836_225 < 2^32). +fn credits_for_votes(votes: u32) -> u32 { + assert(votes <= 65535u32); + return votes * votes; +} + +// Builds a VoterProfile for `token_balance`. +// Uses vote_math::weight::compute for the capped QV weight. +fn build_profile(token_balance: u32) -> VoterProfile { + let w: u32 = vote_math::weight::compute(token_balance); + return VoterProfile { weight: w }; +} diff --git a/quadratic_vote/vote_math/src/lib.leo b/quadratic_vote/vote_math/src/lib.leo new file mode 100644 index 0000000..0a7f9ef --- /dev/null +++ b/quadratic_vote/vote_math/src/lib.leo @@ -0,0 +1,4 @@ +// Maximum vote weight any single participant may hold. +const MAX_WEIGHT: u32 = 100u32; +// Minimum token balance required to participate in a vote. +const MIN_BALANCE: u32 = 1u32; diff --git a/quadratic_vote/vote_math/src/weight.leo b/quadratic_vote/vote_math/src/weight.leo new file mode 100644 index 0000000..dfb2d7d --- /dev/null +++ b/quadratic_vote/vote_math/src/weight.leo @@ -0,0 +1,11 @@ +// Returns the quadratic-voting weight for `token_balance`. +// Weight equals floor(sqrt(token_balance)), capped at vote_math::MAX_WEIGHT. +fn compute(token_balance: u32) -> u32 { + let raw: u32 = sqrt_math::newton::isqrt(token_balance); + return raw < vote_math::MAX_WEIGHT ? raw : vote_math::MAX_WEIGHT; +} + +// Returns true when `token_balance` meets the minimum participation threshold. +fn is_eligible(token_balance: u32) -> bool { + return token_balance >= vote_math::MIN_BALANCE; +}