├── tests ├── testcases │ ├── __init__.py │ ├── rlp │ │ ├── __init__.py │ │ ├── integer.py │ │ ├── empty_account.py │ │ └── merkle_patricia_trie_leaf.py │ ├── divide.py │ ├── concat.py │ ├── array.py │ ├── public_commitment.py │ ├── poseidon.py │ ├── burn_address.py │ ├── shift.py │ ├── spend.py │ ├── proof_of_burn.py │ ├── assertion.py │ ├── keccak.py │ ├── selector.py │ ├── substring_check.py │ ├── proof_of_work.py │ └── convert.py ├── test_spend_input.json ├── constants.py ├── requirements.txt ├── main.py ├── test.py └── test_pob_input.json ├── circomkit.json ├── .gitmodules ├── .gitignore ├── circuits ├── main_spend.circom ├── utils │ ├── constants.circom │ ├── divide.circom │ ├── public_commitment.circom │ ├── assert.circom │ ├── shift.circom │ ├── concat.circom │ ├── array.circom │ ├── proof_of_work.circom │ ├── selector.circom │ ├── convert.circom │ ├── rlp │ │ ├── integer.circom │ │ ├── empty_account.circom │ │ └── merkle_patricia_trie_leaf.circom │ ├── substring_check.circom │ ├── burn_address.circom │ └── keccak.circom ├── main_proof_of_burn.circom ├── spend.circom └── proof_of_burn.circom ├── Dockerfile ├── Makefile ├── .github └── workflows │ ├── test.yml │ └── circuitscan.yml ├── shell.nix ├── LICENSE └── README.md /tests/testcases/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testcases/rlp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /circomkit.json: -------------------------------------------------------------------------------- 1 | {"cWitness": true, "wasmWitness": false} 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "circuits/circomlib"] 2 | path = circuits/circomlib 3 | url = https://github.com/iden3/circomlib 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | target 3 | Prover.toml 4 | node_modules 5 | yarn.lock 6 | yarn-error.log 7 | inp.json 8 | *_cpp 9 | __pycache__ 10 | circuits/test.circom 11 | -------------------------------------------------------------------------------- /circuits/main_spend.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "./spend.circom"; 4 | 5 | // Maximum 31 bytes for amounts to avoid field overflows 6 | component main = Spend(31); -------------------------------------------------------------------------------- /tests/test_spend_input.json: -------------------------------------------------------------------------------- 1 | {"burnKey": "7118903102388801870291760559945404783171971840824336792073800140342461819984", "withdrawnBalance": "321", "balance": "999999999999999766", "extraCommitment": 999} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87-bullseye 2 | RUN apt update && apt install -y curl git python3 python3-pip nlohmann-json3-dev nasm libgmp-dev 3 | RUN pip3 install web3 4 | RUN curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y 5 | RUN git clone https://github.com/iden3/circom.git && cd circom && cargo install --path circom 6 | WORKDIR /app 7 | COPY . . -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | cd circuits && circom -c main_spend.circom --O0 && cd main_spend_cpp && make -B 3 | cd circuits && circom -c main_proof_of_burn.circom --O0 && cd main_proof_of_burn_cpp && make -B 4 | python3 -m tests.main 5 | cd circuits/main_proof_of_burn_cpp && ./main_proof_of_burn input.json witness.wtns 6 | cd circuits/main_spend_cpp && ./main_spend input.json witness.wtns -------------------------------------------------------------------------------- /tests/testcases/divide.py: -------------------------------------------------------------------------------- 1 | test_divide = ( 2 | "Divide(16)", 3 | [ 4 | ({"a": [10], "b": 1}, [10, 0]), 5 | ({"a": [10], "b": 2}, [5, 0]), 6 | ({"a": [10], "b": 3}, [3, 1]), 7 | ({"a": [11], "b": 3}, [3, 2]), 8 | ({"a": [12], "b": 3}, [4, 0]), 9 | ({"a": [0], "b": 1}, [0, 0]), 10 | ({"a": [0], "b": 10}, [0, 0]), 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | from .poseidon import Field 2 | 3 | # int.from_bytes(web3.Web3.keccak(b"EIP-7503"), byteorder='big') % P 4 | POSEIDON_PREFIX = ( 5 | 5265656504298861414514317065875120428884240036965045859626767452974705356670 6 | ) 7 | POSEIDON_BURN_ADDRESS_PREFIX = Field(POSEIDON_PREFIX + 0) 8 | POSEIDON_NULLIFIER_PREFIX = Field(POSEIDON_PREFIX + 1) 9 | POSEIDON_COIN_PREFIX = Field(POSEIDON_PREFIX + 2) 10 | -------------------------------------------------------------------------------- /circuits/utils/constants.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | function POSEIDON_PREFIX() { 4 | // int.from_bytes(web3.Web3.keccak(b"EIP-7503"), byteorder='big') % P 5 | return 5265656504298861414514317065875120428884240036965045859626767452974705356670; 6 | } 7 | function POSEIDON_BURN_ADDRESS_PREFIX() { 8 | return POSEIDON_PREFIX() + 0; 9 | } 10 | function POSEIDON_NULLIFIER_PREFIX() { 11 | return POSEIDON_PREFIX() + 1; 12 | } 13 | function POSEIDON_COIN_PREFIX() { 14 | return POSEIDON_PREFIX() + 2; 15 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test circuits 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: 'recursive' 18 | 19 | - name: Build Docker image 20 | run: | 21 | docker build -t my-app . 22 | 23 | - name: Run test script in Docker container 24 | run: | 25 | docker run --rm my-app python3 -m tests.test -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | let 4 | python-with-packages = pkgs.python3.withPackages (ps: with ps; [ pip virtualenv ]); 5 | in 6 | 7 | pkgs.mkShell { 8 | buildInputs = [ 9 | pkgs.gcc 10 | pkgs.nlohmann_json 11 | pkgs.gmp 12 | pkgs.rustc 13 | pkgs.circom 14 | pkgs.gnumake 15 | pkgs.foundry 16 | pkgs.nasm 17 | python-with-packages 18 | ]; 19 | 20 | shellHook = '' 21 | if [ ! -d ".venv" ]; then 22 | echo "Creating Python virtual environment..." 23 | python -m venv .venv 24 | fi 25 | source .venv/bin/activate 26 | if [ -f tests/requirements.txt ]; then 27 | echo "Installing Python dependencies..." 28 | pip install -r tests/requirements.txt 29 | fi 30 | ''; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /circuits/utils/divide.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "./assert.circom"; 4 | 5 | // Computes the quotient and remainder for the division of a by b: 6 | // a === out * b + rem 7 | // 8 | // Example: 9 | // a: 10 10 | // b: 3 11 | // out: 3 12 | // rem: 1 13 | // 14 | // Reviewers: 15 | // Keyvan: OK 16 | // 17 | template Divide(N) { 18 | signal input a; 19 | signal input b; 20 | signal output out; 21 | signal output rem; 22 | 23 | out <-- a \ b; 24 | rem <-- a % b; 25 | 26 | // Check if `rem` and `b` are at most N-bits long and `rem < b` 27 | AssertLessThan(N)(rem, b); 28 | 29 | // Check if `out` and `a` are at most N-bits long and `out <= a` 30 | AssertLessEqThan(N)(out, a); 31 | 32 | out * b + rem === a; 33 | } -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs==2.6.1 2 | aiohttp==3.12.13 3 | aiosignal==1.3.2 4 | annotated-types==0.7.0 5 | async-timeout==5.0.1 6 | attrs==25.3.0 7 | bitarray==3.4.3 8 | certifi==2025.6.15 9 | charset-normalizer==3.4.2 10 | ckzg==2.1.1 11 | cytoolz==1.0.1 12 | eth-account==0.13.7 13 | eth-hash==0.7.1 14 | eth-keyfile==0.8.1 15 | eth-keys==0.7.0 16 | eth-rlp==2.2.0 17 | eth-typing==5.2.1 18 | eth-utils==5.3.0 19 | eth_abi==5.2.0 20 | frozenlist==1.7.0 21 | hexbytes==1.3.1 22 | idna==3.10 23 | multidict==6.5.1 24 | parsimonious==0.10.0 25 | propcache==0.3.2 26 | pycryptodome==3.23.0 27 | pydantic==2.11.7 28 | pydantic_core==2.33.2 29 | pyunormalize==16.0.0 30 | regex==2024.11.6 31 | requests==2.32.4 32 | rlp==4.1.0 33 | toolz==1.0.0 34 | types-requests==2.32.4.20250611 35 | typing-inspection==0.4.1 36 | typing_extensions==4.14.0 37 | urllib3==2.5.0 38 | web3==7.12.0 39 | websockets==15.0.1 40 | yarl==1.20.1 41 | -------------------------------------------------------------------------------- /tests/testcases/concat.py: -------------------------------------------------------------------------------- 1 | test_mask = ( 2 | "Mask(5)", 3 | [ 4 | ({"in": [1, 2, 3, 4, 5], "count": 0}, [0, 0, 0, 0, 0]), 5 | ({"in": [1, 2, 3, 4, 5], "count": 2}, [1, 2, 0, 0, 0]), 6 | ({"in": [1, 2, 3, 4, 5], "count": 4}, [1, 2, 3, 4, 0]), 7 | ({"in": [1, 2, 3, 4, 5], "count": 5}, [1, 2, 3, 4, 5]), 8 | ({"in": [1, 2, 3, 4, 5], "count": 10}, [1, 2, 3, 4, 5]), 9 | ({"in": [1, 2, 3, 4, 5], "count": 1000}, [1, 2, 3, 4, 5]), 10 | ], 11 | ) 12 | 13 | test_concat = ( 14 | "Concat(5,5)", 15 | [ 16 | ( 17 | { 18 | "a": [1, 2, 3, 4, 5], 19 | "aLen": 5, 20 | "b": [10, 20, 30, 40, 50], 21 | "bLen": 2, 22 | }, 23 | [1, 2, 3, 4, 5, 10, 20, 0, 0, 0, 7], 24 | ), 25 | ( 26 | { 27 | "a": [1, 2, 3, 4, 5], 28 | "aLen": 6, 29 | "b": [10, 20, 30, 40, 50], 30 | "bLen": 2, 31 | }, 32 | None, 33 | ), 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /tests/testcases/array.py: -------------------------------------------------------------------------------- 1 | test_filter = ( 2 | "Filter(5)", 3 | [ 4 | ({"in": 0}, [0, 0, 0, 0, 0]), 5 | ({"in": 1}, [1, 0, 0, 0, 0]), 6 | ({"in": 3}, [1, 1, 1, 0, 0]), 7 | ({"in": 5}, [1, 1, 1, 1, 1]), 8 | ({"in": 10}, [1, 1, 1, 1, 1]), 9 | ], 10 | ) 11 | 12 | test_reshape = ( 13 | "Reshape(3, 4)", 14 | [ 15 | ( 16 | {"in": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]}, 17 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 18 | ), 19 | ], 20 | ) 21 | 22 | test_reverse = ( 23 | "Reverse(3)", 24 | [ 25 | ({"in": [1, 2, 3]}, [3, 2, 1]), 26 | ({"in": [123, 234, 56]}, [56, 234, 123]), 27 | ], 28 | ) 29 | 30 | test_fit_1 = ( 31 | "Fit(5, 3)", 32 | [ 33 | ({"in": [1, 2, 3, 4, 5]}, [1, 2, 3]), 34 | ], 35 | ) 36 | 37 | test_fit_2 = ( 38 | "Fit(3, 5)", 39 | [ 40 | ({"in": [1, 2, 3]}, [1, 2, 3, 0, 0]), 41 | ], 42 | ) 43 | 44 | test_flatten = ( 45 | "Flatten(2, 3)", 46 | [ 47 | ({"in": [[1, 2, 3], [4, 5, 6]]}, [1, 2, 3, 4, 5, 6]), 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 worm-privacy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/circuitscan.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on Circuitscan 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | deploy: 8 | environment: Circuitscan 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | with: 14 | submodules: 'recursive' 15 | 16 | - name: Install Node 17 | uses: actions/setup-node@v4 18 | 19 | - name: Install CircuitScan 20 | run: | 21 | npm install -g circuitscan 22 | 23 | - name: Login to Circuitscan 24 | run: | 25 | circuitscan login ${{ secrets.CIRCUITSCAN_API_KEY }} 26 | 27 | - name: Deploy Spend circuit 28 | run: | 29 | circuitscan deploy:circom circuits/main_spend.circom sepolia -v v2.2.2 -i 256 30 | env: 31 | DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }} 32 | ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} 33 | 34 | - name: Deploy Proof-Of-Burn circuit 35 | run: | 36 | circuitscan deploy:circom circuits/main_proof_of_burn.circom sepolia -v v2.2.2 -i 256 37 | env: 38 | DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }} 39 | ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} 40 | -------------------------------------------------------------------------------- /tests/testcases/public_commitment.py: -------------------------------------------------------------------------------- 1 | from eth_abi import packed 2 | import web3 3 | 4 | 5 | def expected_commitment(vals): 6 | concat_bytes = [] 7 | for v in vals: 8 | concat_bytes.extend(int.to_bytes(v, 32, "big")) 9 | 10 | expected = int.from_bytes( 11 | web3.Web3.keccak(packed.encode_packed(["uint256"] * len(vals), vals))[:31], 12 | "big", 13 | ) 14 | return ( 15 | {"in": concat_bytes}, 16 | [expected], 17 | ) 18 | 19 | 20 | test_public_commitment_1 = ( 21 | "PublicCommitment(1)", 22 | [ 23 | expected_commitment([0]), 24 | expected_commitment([123456]), 25 | expected_commitment([2**256 - 1]), 26 | ], 27 | ) 28 | 29 | test_public_commitment_2 = ( 30 | "PublicCommitment(2)", 31 | [ 32 | expected_commitment([0, 1]), 33 | expected_commitment([123456, 2345678]), 34 | expected_commitment([987654321, 2**256 - 1]), 35 | expected_commitment([2**256 - 1, 2**256 - 1]), 36 | ], 37 | ) 38 | 39 | test_public_commitment_6 = ( 40 | "PublicCommitment(6)", 41 | [ 42 | expected_commitment([0, 1, 2, 3, 4, 5]), 43 | expected_commitment([v * 3**100 for v in [0, 1, 2, 3, 4, 5]]), 44 | expected_commitment([2**256 - 1] * 6), 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /circuits/main_proof_of_burn.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "./proof_of_burn.circom"; 4 | 5 | // 16 -> maxNumLayers (Maximum number of Merkle-Patricia-Trie proof nodes supported) 6 | // Number of MPT nodes in account proofs of 100 richest addresses as of July 2nd 2025: Min: 8 Max: 10 Avg: 8.69 7 | // 8 | // 4 -> maxNodeBlocks (Keccak blocks are 136 bytes. Merkle-Patricia-Trie nodes are maximum 532 bytes ~ 3.91 blocks) 9 | // Length of MPT nodes in accounts proofs of 100 richest addresses as of July 7th 2025: Min: 35 Max: 532 Avg: 432.23 10 | // Maximum lengths are for branch nodes with 16 non-empty slots: len(rlp.encode([keccak(...)] * 16 + [0])) == 532 11 | // 12 | // 16 -> maxHeaderBlocks (Average header len of the last 100 blocks as of July 2nd 2025 is 643 bytes ~ 4.72 blocks) 13 | // 14 | // 50 -> minLeafAddressNibbles (4 * 50 = 200 bits of security (?!)) 15 | // Number of address-hash nibbles present in leaf among 100 richest addresses: Min: 54 Max: 60 Avg: 56.08 16 | // Bitcoin's world record of lowest block-hash has only 23 zero nibbles 17 | // 18 | // 31 -> amountBytes (248-bits to disallow field overflows) 19 | // 20 | // 2 -> powMinimumZeroBytes (Adds 8 * powMinimumZeroBytes extra bits of security) 21 | // This is to make it harder to find address-hash collisions 22 | // 23 | // 10 ETH -> maxIntendedBalance (To reduce the incentive to prove large amounts of ETH by performing address-hash collision attack) 24 | // 25 | // 100 ETH -> maxActualBalance (Allows the burn address to be dusted by attackers up to a limited amount, while still keeping the burn address balance appearing realistic) 26 | // 27 | component main = ProofOfBurn(16, 4, 16, 50, 31, 2, 10 ** 19, 10 ** 20); 28 | -------------------------------------------------------------------------------- /tests/testcases/poseidon.py: -------------------------------------------------------------------------------- 1 | from ..poseidon import poseidon2, poseidon3, poseidon4, Field 2 | 3 | test_poseidon_2 = ( 4 | "Poseidon(2)", 5 | [ 6 | ({"inputs": [1, 2]}, [poseidon2(Field(1), Field(2)).val]), 7 | ({"inputs": [1, 3]}, [poseidon2(Field(1), Field(3)).val]), 8 | ({"inputs": [2, 3]}, [poseidon2(Field(2), Field(3)).val]), 9 | ( 10 | {"inputs": [str(3**150), str(7**40)]}, 11 | [poseidon2(Field(3**150), Field(7**40)).val], 12 | ), 13 | ], 14 | ) 15 | test_poseidon_3 = ( 16 | "Poseidon(3)", 17 | [ 18 | ({"inputs": [1, 2, 3]}, [poseidon3(Field(1), Field(2), Field(3)).val]), 19 | ({"inputs": [1, 3, 4]}, [poseidon3(Field(1), Field(3), Field(4)).val]), 20 | ({"inputs": [2, 3, 5]}, [poseidon3(Field(2), Field(3), Field(5)).val]), 21 | ( 22 | {"inputs": [str(3**150), str(7**40), str(6**50)]}, 23 | [poseidon3(Field(3**150), Field(7**40), Field(6**50)).val], 24 | ), 25 | ], 26 | ) 27 | test_poseidon_4 = ( 28 | "Poseidon(4)", 29 | [ 30 | ( 31 | {"inputs": [1, 2, 4, 5]}, 32 | [poseidon4(Field(1), Field(2), Field(4), Field(5)).val], 33 | ), 34 | ( 35 | {"inputs": [1, 3, 2, 4]}, 36 | [poseidon4(Field(1), Field(3), Field(2), Field(4)).val], 37 | ), 38 | ( 39 | {"inputs": [2, 3, 5, 1]}, 40 | [poseidon4(Field(2), Field(3), Field(5), Field(1)).val], 41 | ), 42 | ( 43 | {"inputs": [str(3**150), str(7**40), str(6**20), str(7**35)]}, 44 | [ 45 | poseidon4( 46 | Field(3**150), Field(7**40), Field(6**20), Field(7**35) 47 | ).val 48 | ], 49 | ), 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /circuits/utils/public_commitment.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../circomlib/circuits/bitify.circom"; 4 | include "./keccak.circom"; 5 | include "./array.circom"; 6 | include "./assert.circom"; 7 | 8 | // Calculate keccak(abi.encodePacked(in[0], in[1], ..., in[N-1])) 9 | // Where inputs are 32-byte data 10 | // The last byte of output is truncated in order to make the result fit in a field element 11 | // 12 | // Reviewers: 13 | // Keyvan: OK 14 | // Shahriar: OK 15 | // - Comment: Since it is being used indirectly by the spend.circom, there is no need to check that `in[n][32]` values are indeed `bytes`. This must always be eforced by the wrapper circuit that calls this. 16 | // - Keyvan's response: Added AssertByteString to ensure all inputs are bytes, just in case :) 17 | // 18 | template PublicCommitment(N) { 19 | signal input in[N][32]; 20 | signal output out; 21 | 22 | // Check if all inputs are byte-strings 23 | for(var i = 0; i < N; i++) { 24 | AssertByteString(32)(in[i]); 25 | } 26 | 27 | // Number of keccak-blocks needed to store N 32-byte elements 28 | // numBlocks = Ceil(N * 32 / 136) 29 | var numBlocks = N * 32 \ 136 + (N * 32 % 136 != 0); 30 | 31 | assert(numBlocks * 136 - N * 32 >= 1); // Reserve at least one byte for padding! 32 | 33 | // Fit the 32-byte numbers in the keccak blocks and calculate the hash 34 | signal flattenIn[N * 32] <== Flatten(N, 32)(in); 35 | signal block[numBlocks * 136] <== Fit(N * 32, numBlocks * 136)(flattenIn); 36 | signal hash[32] <== KeccakBytes(numBlocks)(block, N * 32); 37 | 38 | // Ignore the least-significant byte while converting keccak to field element 39 | // In Solidity: keccak(abi.encodePacked(...)) >> 8 40 | signal reducedHash[31] <== Fit(32, 31)(hash); 41 | out <== BigEndianBytes2Num(31)(reducedHash); 42 | } -------------------------------------------------------------------------------- /circuits/utils/assert.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../circomlib/circuits/comparators.circom"; 4 | include "../circomlib/circuits/bitify.circom"; 5 | 6 | // Assert the input number is less than 2^B 7 | // 8 | // Reviewers: 9 | // Keyvan: OK 10 | // Shahriar: OK 11 | // Sarah: OK 12 | // 13 | template AssertBits(B) { 14 | signal input in; 15 | assert(B < 254); // For bit-length of 254 Num2Bits_strict() should be used 16 | signal bits[B] <== Num2Bits(B)(in); 17 | } 18 | 19 | // Assert all N inputs are bytes 20 | // 21 | // Reviewers: 22 | // Keyvan: OK 23 | // Shahriar: OK 24 | // Sarah: OK 25 | // 26 | template AssertByteString(N) { 27 | signal input in[N]; 28 | for(var i = 0; i < N; i++) { 29 | AssertBits(8)(in[i]); 30 | } 31 | } 32 | 33 | // Assert a < b (Where a and b are at most B bits long) 34 | // 35 | // Reviewers: 36 | // Keyvan: OK 37 | // Shahriar: OK 38 | // Sarah: OK 39 | // 40 | template AssertLessThan(B) { 41 | signal input a; 42 | signal input b; 43 | AssertBits(B)(a); 44 | AssertBits(B)(b); 45 | signal out <== LessThan(B)([a, b]); 46 | out === 1; 47 | } 48 | 49 | // Assert a <= b (Where a and b are at most B bits long) 50 | // 51 | // Reviewers: 52 | // Keyvan: OK 53 | // Shahriar: OK 54 | // Sarah: OK 55 | // 56 | template AssertLessEqThan(B) { 57 | signal input a; 58 | signal input b; 59 | AssertBits(B)(a); 60 | AssertBits(B)(b); 61 | signal out <== LessEqThan(B)([a, b]); 62 | out === 1; 63 | } 64 | 65 | // Assert a >= b (Where a and b are at most B bits long) 66 | // 67 | // Reviewers: 68 | // Keyvan: OK 69 | // Shahriar: OK 70 | // Sarah: OK 71 | // 72 | template AssertGreaterEqThan(B) { 73 | signal input a; 74 | signal input b; 75 | AssertBits(B)(a); 76 | AssertBits(B)(b); 77 | signal out <== GreaterEqThan(B)([a, b]); 78 | out === 1; 79 | } 80 | 81 | -------------------------------------------------------------------------------- /tests/testcases/burn_address.py: -------------------------------------------------------------------------------- 1 | from ..poseidon import poseidon4, Field 2 | from ..constants import POSEIDON_BURN_ADDRESS_PREFIX 3 | import web3 4 | 5 | 6 | def burn_addr_calc(burn_key, reveal_amount, burn_addr_commit): 7 | return int.to_bytes( 8 | poseidon4( 9 | POSEIDON_BURN_ADDRESS_PREFIX, 10 | Field(burn_key), 11 | Field(reveal_amount), 12 | Field(burn_addr_commit), 13 | ).val, 14 | 32, 15 | "big", 16 | )[:20] 17 | 18 | 19 | def burn_addr_hash_calc(burn_key, reveal_amount, burn_addr_commit): 20 | res = web3.Web3.keccak( 21 | burn_addr_calc(burn_key, reveal_amount, burn_addr_commit) 22 | ).hex() 23 | return [int(ch, base=16) for ch in res] 24 | 25 | 26 | test_burn_address = ( 27 | "BurnAddress()", 28 | [ 29 | ( 30 | { 31 | "burnKey": 123, 32 | "revealAmount": 98765, 33 | "burnExtraCommitment": 5678, 34 | }, 35 | list(burn_addr_calc(123, 98765, 5678)), 36 | ), 37 | ( 38 | { 39 | "burnKey": str(7**40), 40 | "revealAmount": str(9**41), 41 | "burnExtraCommitment": str(6**41), 42 | }, 43 | list(burn_addr_calc(7**40, 9**41, 6**41)), 44 | ), 45 | ], 46 | ) 47 | 48 | test_burn_address_hash = ( 49 | "BurnAddressHash()", 50 | [ 51 | ( 52 | { 53 | "burnKey": 123, 54 | "revealAmount": 98765, 55 | "burnExtraCommitment": 5678, 56 | }, 57 | burn_addr_hash_calc(123, 98765, 5678), 58 | ), 59 | ( 60 | { 61 | "burnKey": str(7**40), 62 | "revealAmount": str(9**41), 63 | "burnExtraCommitment": str(6**41), 64 | }, 65 | burn_addr_hash_calc(7**40, 9**41, 6**41), 66 | ), 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /tests/testcases/shift.py: -------------------------------------------------------------------------------- 1 | test_shift_right = ( 2 | "ShiftRight(8, 3)", 3 | [ 4 | ( 5 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 0}, 6 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0], 7 | ), 8 | ( 9 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 1}, 10 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0], 11 | ), 12 | ( 13 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 2}, 14 | [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0], 15 | ), 16 | ( 17 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 3}, 18 | [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8], 19 | ), 20 | ( 21 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 4}, 22 | None, 23 | ), 24 | ( 25 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 100}, 26 | None, 27 | ), 28 | ], 29 | ) 30 | 31 | 32 | test_shift_left = ( 33 | "ShiftLeft(8)", 34 | [ 35 | ( 36 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 0}, 37 | [1, 2, 3, 4, 5, 6, 7, 8], 38 | ), 39 | ( 40 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 1}, 41 | [2, 3, 4, 5, 6, 7, 8, 0], 42 | ), 43 | ( 44 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 4}, 45 | [5, 6, 7, 8, 0, 0, 0, 0], 46 | ), 47 | ( 48 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 7}, 49 | [8, 0, 0, 0, 0, 0, 0, 0], 50 | ), 51 | ( 52 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 8}, 53 | [0, 0, 0, 0, 0, 0, 0, 0], 54 | ), 55 | ( 56 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 9}, 57 | None, 58 | ), 59 | ( 60 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 10}, 61 | None, 62 | ), 63 | ( 64 | {"in": [1, 2, 3, 4, 5, 6, 7, 8], "count": 100}, 65 | None, 66 | ), 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /tests/testcases/rlp/integer.py: -------------------------------------------------------------------------------- 1 | test_rlp_integer_1 = ( 2 | "RlpInteger(3)", 3 | [ 4 | ({"in": 0}, [0x80, 0, 0, 0, 1]), 5 | ({"in": 1}, [1, 0, 0, 0, 1]), 6 | ({"in": 3}, [3, 0, 0, 0, 1]), 7 | ({"in": 10}, [10, 0, 0, 0, 1]), 8 | ({"in": 127}, [127, 0, 0, 0, 1]), 9 | ({"in": 128}, [0x81, 128, 0, 0, 2]), 10 | ({"in": 0xFF}, [0x81, 0xFF, 0, 0, 2]), 11 | ({"in": 0x100}, [0x82, 1, 0, 0, 3]), 12 | ({"in": 0xFFFF}, [0x82, 0xFF, 0xFF, 0, 3]), 13 | ({"in": 0xFFFF}, [0x82, 0xFF, 0xFF, 0, 3]), 14 | ({"in": 0x10000}, [0x83, 1, 0, 0, 4]), 15 | ({"in": 0xFFFFFF}, [0x83, 0xFF, 0xFF, 0xFF, 4]), 16 | ({"in": 0x1000000}, None), 17 | ], 18 | ) 19 | 20 | test_rlp_integer_2 = ( 21 | "RlpInteger(3)", 22 | [ 23 | ({"in": 0}, [0x80, 0, 0, 0, 1]), 24 | ({"in": 1}, [1, 0, 0, 0, 1]), 25 | ({"in": 3}, [3, 0, 0, 0, 1]), 26 | ({"in": 10}, [10, 0, 0, 0, 1]), 27 | ({"in": 127}, [127, 0, 0, 0, 1]), 28 | ({"in": 128}, [0x81, 128, 0, 0, 2]), 29 | ({"in": 0xFF}, [0x81, 0xFF, 0, 0, 2]), 30 | ({"in": 0x100}, [0x82, 1, 0, 0, 3]), 31 | ({"in": 0xFFFF}, [0x82, 0xFF, 0xFF, 0, 3]), 32 | ({"in": 0xFFFF}, [0x82, 0xFF, 0xFF, 0, 3]), 33 | ({"in": 0x10000}, [0x83, 1, 0, 0, 4]), 34 | ({"in": 0xFFFFFF}, [0x83, 0xFF, 0xFF, 0xFF, 4]), 35 | ({"in": 0x1000000}, None), 36 | ], 37 | ) 38 | 39 | test_count_bytes = ( 40 | "CountBytes(4)", 41 | [ 42 | ({"bytes": [0, 0, 0, 0]}, [0]), 43 | ({"bytes": [5, 0, 0, 0]}, [4]), 44 | ({"bytes": [0, 6, 0, 0]}, [3]), 45 | ({"bytes": [0, 0, 7, 0]}, [2]), 46 | ({"bytes": [0, 0, 0, 8]}, [1]), 47 | ({"bytes": [10, 0, 0, 9]}, [4]), 48 | ({"bytes": [11, 12, 0, 13]}, [4]), 49 | ({"bytes": [15, 14, 0, 0]}, [4]), 50 | ({"bytes": [0, 14, 16, 0]}, [3]), 51 | ({"bytes": [0, 14, 10000, 0]}, [3]), 52 | ({"bytes": [0, 0, 10000, 0]}, [2]), 53 | ({"bytes": [0, 0, 10000, 543]}, [2]), 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /tests/testcases/spend.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | from ..constants import ( 4 | POSEIDON_COIN_PREFIX, 5 | ) 6 | from ..poseidon import poseidon3, Field 7 | from .public_commitment import expected_commitment 8 | import copy 9 | 10 | 11 | with io.open("tests/test_spend_input.json") as f: 12 | spend_inp = json.load(f) 13 | 14 | burn_key = int(spend_inp["burnKey"]) 15 | 16 | spend_expected_commitment = expected_commitment( 17 | [ 18 | poseidon3( 19 | POSEIDON_COIN_PREFIX, 20 | Field(burn_key), 21 | Field(999999999999999766), 22 | ).val, # coin 23 | 321, # withdrawnBalance 24 | poseidon3( 25 | POSEIDON_COIN_PREFIX, 26 | Field(burn_key), 27 | Field(999999999999999766 - 321), 28 | ).val, # remainingCoin (coin - withdrawnBalance) 29 | 999, # extraCommitment 30 | ] 31 | )[1][0] 32 | 33 | spend_broken_1 = copy.deepcopy(spend_inp) 34 | spend_broken_1["withdrawnBalance"] = "999999999999999767" 35 | 36 | spend_broken_2 = copy.deepcopy(spend_inp) 37 | spend_broken_2["withdrawnBalance"] = str(2**240 - 1) 38 | 39 | spend_all = copy.deepcopy(spend_inp) 40 | spend_all["withdrawnBalance"] = "999999999999999766" 41 | spend_all_expected_commitment = expected_commitment( 42 | [ 43 | poseidon3( 44 | POSEIDON_COIN_PREFIX, 45 | Field(burn_key), 46 | Field(999999999999999766), 47 | ).val, # coin 48 | 999999999999999766, # withdrawnBalance 49 | poseidon3( 50 | POSEIDON_COIN_PREFIX, 51 | Field(burn_key), 52 | Field(0), 53 | ).val, # remainingCoin (coin - withdrawnBalance) 54 | 999, # extraCommitment 55 | ] 56 | )[1][0] 57 | 58 | test_spend = ( 59 | "Spend(31)", 60 | [ 61 | ( 62 | spend_inp, 63 | [spend_expected_commitment], 64 | ), 65 | (spend_broken_1, None), 66 | (spend_broken_2, None), 67 | ( 68 | spend_all, 69 | [spend_all_expected_commitment], 70 | ), 71 | ], 72 | ) 73 | -------------------------------------------------------------------------------- /circuits/spend.circom: -------------------------------------------------------------------------------- 1 | // __ _____ ____ __ __ 2 | // \ \ / / _ \| _ \| \/ | 3 | // \ \ /\ / / | | | |_) | |\/| | 4 | // \ V V /| |_| | _ <| | | | 5 | // \_/\_/ \___/|_| \_\_| |_| 6 | // 7 | 8 | pragma circom 2.2.2; 9 | 10 | include "./circomlib/circuits/poseidon.circom"; 11 | include "./utils/assert.circom"; 12 | include "./utils/convert.circom"; 13 | include "./utils/public_commitment.circom"; 14 | include "./utils/constants.circom"; 15 | 16 | // Computes the encrypted balance (coin) using the Poseidon3 hash function 17 | // with the given `balance` and `burnKey`. Verifies that `withdrawnBalance` plus 18 | // `remainingCoin` equals the encrypted balance, and includes an `extraCommitment` 19 | // to enforce how the `withdrawnBalance` should be distributed by the verifier 20 | // contract. 21 | // 22 | // Example: 23 | // balance: 1000 24 | // withdrawnBalance: 200 25 | // burnKey: 123456 26 | // coin: Poseidon3(POSEIDON_COIN_PREFIX, 123456, 1000) 27 | // remainingCoin: Poseidon3(POSEIDON_COIN_PREFIX, 123456, 800) 28 | // 29 | // Reviewers: 30 | // Keyvan: Ok 31 | // 32 | template Spend(maxAmountBytes) { 33 | signal input burnKey; 34 | signal input balance; 35 | signal input withdrawnBalance; 36 | signal input extraCommitment; 37 | 38 | signal output commitment; 39 | 40 | assert(maxAmountBytes <= 31); // To avoid field overflows 41 | AssertGreaterEqThan(maxAmountBytes * 8)(balance, withdrawnBalance); 42 | 43 | signal coin <== Poseidon(3)([POSEIDON_COIN_PREFIX(), burnKey, balance]); 44 | signal remainingCoin <== Poseidon(3)([POSEIDON_COIN_PREFIX(), burnKey, balance - withdrawnBalance]); 45 | 46 | signal coinBytes[32] <== Num2BigEndianBytes(32)(coin); 47 | signal withdrawnBalanceBytes[32] <== Num2BigEndianBytes(32)(withdrawnBalance); 48 | signal remainingCoinBytes[32] <== Num2BigEndianBytes(32)(remainingCoin); 49 | signal extraCommmitmentBytes[32] <== Num2BigEndianBytes(32)(extraCommitment); 50 | commitment <== PublicCommitment(4)( 51 | [coinBytes, withdrawnBalanceBytes, remainingCoinBytes, extraCommmitmentBytes] 52 | ); 53 | } -------------------------------------------------------------------------------- /circuits/utils/shift.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../circomlib/circuits/comparators.circom"; 4 | include "./assert.circom"; 5 | 6 | // Shifts the `in` array to the left by the given `count` times, filling the end with zeros. 7 | // 8 | // Example: 9 | // in: [1, 2, 3, 4, 5] 10 | // count: 2 11 | // out: [3, 4, 5, 0, 0] 12 | // 13 | // Reviewers: 14 | // Keyvan: OK 15 | // Shahriar: OK 16 | // 17 | template ShiftLeft(n) { 18 | signal input in[n]; 19 | signal input count; 20 | signal output out[n]; 21 | 22 | AssertLessEqThan(16)(count, n); 23 | 24 | var outVars[n]; 25 | signal isEq[n][n]; 26 | signal temp[n][n]; 27 | // out[i] <== sum_j(in[j] * (i == j - count)) 28 | for(var i = 0; i < n; i++) { 29 | for(var j = 0; j < n; j++) { 30 | isEq[i][j] <== IsEqual()([i, j - count]) ; 31 | temp[i][j] <== isEq[i][j] * in[j]; 32 | outVars[i] += temp[i][j]; 33 | } 34 | out[i] <== outVars[i]; 35 | } 36 | } 37 | 38 | // Shifts the input array `in` to the right by `count` positions, 39 | // filling the leftmost `count` positions with zeros. 40 | // 41 | // Example: 42 | // maxShift: 5 43 | // count: 3 44 | // in: [1, 2, 3, 4, 5, 6, 7, 8] 45 | // output: [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0] 46 | // 47 | // Reviewers: 48 | // Keyvan: OK 49 | // Sarah: OK 50 | // 51 | template ShiftRight(n, maxShift) { 52 | signal input in[n]; 53 | signal input count; 54 | signal output out[n + maxShift]; 55 | 56 | AssertLessEqThan(16)(count, maxShift); 57 | 58 | var outVars[n + maxShift]; 59 | 60 | // Shift by `i` only when `i == count` 61 | // out[i + j] <== (i == count) * in[j] 62 | // I.e out[i + j] <== in[j] when `i == count` 63 | signal isEq[maxShift + 1]; 64 | signal temps[maxShift + 1][n]; 65 | for(var i = 0; i <= maxShift; i++) { 66 | isEq[i] <== IsEqual()([i, count]); 67 | for(var j = 0; j < n; j++) { 68 | temps[i][j] <== isEq[i] * in[j]; 69 | outVars[i + j] += temps[i][j]; 70 | } 71 | } 72 | 73 | for(var i = 0; i < n + maxShift; i++) { 74 | out[i] <== outVars[i]; 75 | } 76 | } -------------------------------------------------------------------------------- /tests/testcases/rlp/empty_account.py: -------------------------------------------------------------------------------- 1 | import rlp 2 | import web3 3 | 4 | 5 | def rlp_empty_account(balance, max_balance_bytes): 6 | predict = list( 7 | rlp.encode( 8 | [ 9 | 0, 10 | balance, 11 | web3.Web3.to_bytes( 12 | hexstr="0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" 13 | ), 14 | web3.Web3.to_bytes( 15 | hexstr="0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" 16 | ), 17 | ] 18 | ) 19 | ) 20 | predict_len = len(predict) 21 | predict = predict + [0] * (70 + max_balance_bytes - predict_len) + [predict_len] 22 | return predict 23 | 24 | 25 | test_rlp_empty_account_1 = ( 26 | "RlpEmptyAccount(3)", 27 | [ 28 | ({"balance": 0}, rlp_empty_account(0, 3)), 29 | ({"balance": 1}, rlp_empty_account(1, 3)), 30 | ({"balance": 10}, rlp_empty_account(10, 3)), 31 | ({"balance": 255}, rlp_empty_account(255, 3)), 32 | ({"balance": 256}, rlp_empty_account(256, 3)), 33 | ({"balance": 257}, rlp_empty_account(257, 3)), 34 | ({"balance": 0xFFFF}, rlp_empty_account(0xFFFF, 3)), 35 | ({"balance": 0x10000}, rlp_empty_account(0x10000, 3)), 36 | ({"balance": 0xFFFFFF}, rlp_empty_account(0xFFFFFF, 3)), 37 | ({"balance": 0x1000000}, None), 38 | ], 39 | ) 40 | 41 | test_rlp_empty_account_2 = ( 42 | "RlpEmptyAccount(10)", 43 | [ 44 | ({"balance": str(256**7 - 1234)}, rlp_empty_account(256**7 - 1234, 10)), 45 | ({"balance": str(256**10 - 1)}, rlp_empty_account(256**10 - 1, 10)), 46 | ({"balance": str(256**10)}, None), 47 | ], 48 | ) 49 | 50 | test_rlp_empty_account_3 = ( 51 | "RlpEmptyAccount(31)", 52 | [ 53 | ( 54 | {"balance": str(256**7 - 192837465)}, 55 | rlp_empty_account(256**7 - 192837465, 31), 56 | ), 57 | ( 58 | {"balance": str(256**10 - 987654321)}, 59 | rlp_empty_account(256**10 - 987654321, 31), 60 | ), 61 | ({"balance": str(256**31 - 1)}, rlp_empty_account(256**31 - 1, 31)), 62 | ({"balance": str(256**31)}, None), 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /circuits/utils/concat.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "./array.circom"; 4 | include "./shift.circom"; 5 | 6 | // Outputs an array where only the first `ind` elements of `in` are kept, 7 | // and the rest are zeroed out. 8 | // 9 | // Example: 10 | // in: [1, 2, 3, 4, 5], ind: 3 11 | // out: [1, 2, 3, 0, 0] 12 | // 13 | // Reviewers: 14 | // Keyvan: OK 15 | // Shahriar: OK 16 | // Sarah: OK 17 | // 18 | template Mask(n) { 19 | signal input in[n]; 20 | signal input count; 21 | signal output out[n]; 22 | 23 | // Generate filter: [1, 1, ..., 1, 1, 0, 0, ..., 0, 0] 24 | signal filter[n] <== Filter(n)(count); 25 | 26 | // Apply filter 27 | for(var i = 0; i < n; i++) { 28 | out[i] <== in[i] * filter[i]; 29 | } 30 | } 31 | 32 | 33 | // Concatenates arrays `a` and `b` up to lengths `aLen` and `bLen` 34 | // Output is length `outLen` 35 | // Elements beyond the concatenated length are zero-padded 36 | // 37 | // Example: 38 | // a: [1, 2, 3, 4, 5], aLen: 3 39 | // b: [10, 20, 30, 40, 50], bLen: 2 40 | // outLen: 5 41 | // out: [1, 2, 3, 10, 20, 0, 0, 0, 0, 0] 42 | // 43 | // Reviewers: 44 | // Keyvan: OK 45 | // Sarah: OK 46 | // 47 | template Concat(maxLenA, maxLenB) { 48 | signal input a[maxLenA]; 49 | signal input aLen; 50 | 51 | signal input b[maxLenB]; 52 | signal input bLen; 53 | 54 | signal output out[maxLenA + maxLenB]; 55 | signal output outLen; 56 | 57 | AssertLessEqThan(16)(aLen, maxLenA); 58 | AssertLessEqThan(16)(bLen, maxLenB); 59 | 60 | // Example: 61 | // a: [1, 2, 3, 4, 5] aLen: 3 62 | // b: [10, 20, 30, 40, 50] bLen: 2 63 | 64 | // maskedA: [1, 2, 3, 0, 0] 65 | signal maskedA[maxLenA] <== Mask(maxLenA)(a, aLen); 66 | 67 | // maskedB: [10, 20, 0, 0, 0] 68 | signal maskedB[maxLenB] <== Mask(maxLenB)(b, bLen); 69 | 70 | // shiftedB: [0, 0, 0, 10, 20, 0, 0, 0, 0, 0] 71 | signal shiftedB[maxLenA + maxLenB] <== ShiftRight(maxLenB, maxLenA)(maskedB, aLen); 72 | 73 | // out = maskedA + shiftedB 74 | // out: [1, 2, 3, 10, 20, 0, 0, 0, 0, 0] 75 | for(var i = 0; i < maxLenA + maxLenB; i++) { 76 | if(i < maxLenA) { 77 | out[i] <== maskedA[i] + shiftedB[i]; 78 | } else { 79 | out[i] <== shiftedB[i]; 80 | } 81 | } 82 | 83 | outLen <== aLen + bLen; 84 | } -------------------------------------------------------------------------------- /circuits/utils/array.circom: -------------------------------------------------------------------------------- 1 | 2 | pragma circom 2.2.2; 3 | 4 | include "../circomlib/circuits/comparators.circom"; 5 | include "./assert.circom"; 6 | 7 | // Generates an array where the first `in` elements are 1 and the rest are 0 8 | // 9 | // Example Filter(5): 10 | // in: 0 11 | // out: [0, 0, 0, 0, 0] 12 | // 13 | // in: 1 14 | // out: [1, 0, 0, 0, 0] 15 | // 16 | // in: 3 17 | // out: [1, 1, 1, 0, 0] 18 | // 19 | // in: 10 20 | // out: [1, 1, 1, 1, 1] 21 | // 22 | // Reviewers: 23 | // Keyvan: OK 24 | // Shahriar: OK 25 | // 26 | template Filter(N) { 27 | signal input in; 28 | signal output out[N]; 29 | 30 | signal isEq[N]; 31 | for(var i = 0; i < N; i++) { 32 | isEq[i] <== IsEqual()([i, in]); 33 | if(i > 0) { 34 | out[i] <== out[i - 1] * (1 - isEq[i]); 35 | } 36 | else { 37 | out[i] <== (1 - isEq[i]); 38 | } 39 | } 40 | } 41 | 42 | // Fits an M-element array in an N-element block 43 | // 44 | // Reviewers: 45 | // Keyvan: OK 46 | // 47 | template Fit(M, N) { 48 | signal input in[M]; 49 | signal output out[N]; 50 | for(var i = 0; i < N; i++) { 51 | if(i < M) { 52 | out[i] <== in[i]; 53 | } else { 54 | out[i] <== 0; 55 | } 56 | } 57 | } 58 | 59 | // Flattens a 2D array into a 1D array 60 | // 61 | // Reviewers: 62 | // Keyvan: OK 63 | // 64 | template Flatten(M, N) { 65 | signal input in[M][N]; 66 | signal output out[M * N]; 67 | for(var i = 0; i < M; i++) { 68 | for(var j = 0; j < N; j++) { 69 | out[i * N + j] <== in[i][j]; 70 | } 71 | } 72 | } 73 | 74 | // Reshapes a 1D array into a 2D array 75 | // 76 | // Reviewers: 77 | // Keyvan: OK 78 | // 79 | template Reshape(M, N) { 80 | signal input in[M * N]; 81 | signal output out[M][N]; 82 | for(var i = 0; i < M; i++) { 83 | for(var j = 0; j < N; j++) { 84 | out[i][j] <== in[i * N + j]; 85 | } 86 | } 87 | } 88 | 89 | // Reverses the input array 90 | // 91 | // Reviewers: 92 | // Keyvan: OK 93 | // 94 | template Reverse(N) { 95 | signal input in[N]; 96 | signal output out[N]; 97 | for(var i = 0; i < N; i++) { 98 | out[i] <== in[N - 1 - i]; 99 | } 100 | } -------------------------------------------------------------------------------- /tests/testcases/proof_of_burn.py: -------------------------------------------------------------------------------- 1 | import web3 2 | import io 3 | import json 4 | from ..constants import ( 5 | POSEIDON_COIN_PREFIX, 6 | POSEIDON_NULLIFIER_PREFIX, 7 | ) 8 | from ..poseidon import poseidon2, poseidon3, Field 9 | from .public_commitment import expected_commitment 10 | 11 | 12 | with io.open("tests/test_pob_input.json") as f: 13 | proof_of_burn_inp = json.load(f) 14 | 15 | burn_key = int(proof_of_burn_inp["burnKey"]) 16 | burn_extra_commit = int(proof_of_burn_inp["burnExtraCommitment"]) 17 | 18 | pob_expected_commitment = expected_commitment( 19 | [ 20 | int.from_bytes( 21 | bytes.fromhex( 22 | "e36499b50da290131c3fa32d4f60717c8c529ae1bc3a216f32d05c05fe80368d" 23 | ), 24 | "big", 25 | ), # Block root 26 | poseidon2(POSEIDON_NULLIFIER_PREFIX, Field(burn_key)).val, # Nullifier, 27 | poseidon3( 28 | POSEIDON_COIN_PREFIX, 29 | Field(burn_key), 30 | Field(1000000000000000000 - 234), 31 | ).val, # Encrypted balance 32 | 234, # Spend 33 | burn_extra_commit, 34 | 0, # Extra commitment 35 | ] 36 | )[1][0] 37 | 38 | import copy 39 | 40 | proof_of_burn_corrupted_layer_0 = copy.deepcopy(proof_of_burn_inp) 41 | proof_of_burn_corrupted_layer_0["layers"][0][0] += 1 42 | 43 | proof_of_burn_corrupted_layer_1 = copy.deepcopy(proof_of_burn_inp) 44 | proof_of_burn_corrupted_layer_1["layers"][1][0] += 1 45 | 46 | proof_of_burn_corrupted_layer_2 = copy.deepcopy(proof_of_burn_inp) 47 | proof_of_burn_corrupted_layer_2["layers"][2][0] += 1 48 | 49 | proof_of_burn_corrupted_layer_3 = copy.deepcopy(proof_of_burn_inp) 50 | proof_of_burn_corrupted_layer_3["layers"][3][0] += 1 51 | 52 | test_proof_of_burn = ( 53 | "ProofOfBurn(4, 4, 5, 20, 31, 2, 10 ** 18, 10 ** 19)", 54 | [ 55 | ( 56 | proof_of_burn_inp, 57 | [pob_expected_commitment], 58 | ), 59 | ( 60 | proof_of_burn_corrupted_layer_0, 61 | None, 62 | ), 63 | ( 64 | proof_of_burn_corrupted_layer_1, 65 | None, 66 | ), 67 | ( 68 | proof_of_burn_corrupted_layer_2, 69 | [pob_expected_commitment], # layer[2] is unused so doesn't matter! 70 | ), 71 | ( 72 | proof_of_burn_corrupted_layer_3, 73 | [pob_expected_commitment], # layer[3] is unused so doesn't matter! 74 | ), 75 | ], 76 | ) 77 | -------------------------------------------------------------------------------- /circuits/utils/proof_of_work.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "./keccak.circom"; 4 | include "./assert.circom"; 5 | 6 | // The "EIP-7503" string 7 | // 8 | // Reviewers: 9 | // Keyvan: OK 10 | // 11 | template EIP7503() { 12 | signal output out[8]; 13 | out[0] <== 69; // 'E' 14 | out[1] <== 73; // 'I' 15 | out[2] <== 80; // 'P' 16 | out[3] <== 45; // '-' 17 | out[4] <== 55; // '7' 18 | out[5] <== 53; // '5' 19 | out[6] <== 48; // '0' 20 | out[7] <== 51; // '3' 21 | } 22 | 23 | // Concat 4 fixed-size strings 24 | // 25 | // Reviewers: 26 | // Keyvan: OK 27 | // 28 | template ConcatFixed4(A, B, C, D) { 29 | signal input a[A]; 30 | signal input b[B]; 31 | signal input c[C]; 32 | signal input d[D]; 33 | signal output out[A + B + C + D]; 34 | 35 | for(var i = 0; i < A; i++) { 36 | out[i] <== a[i]; 37 | } 38 | for(var i = 0; i < B; i++) { 39 | out[i + A] <== b[i]; 40 | } 41 | for(var i = 0; i < C; i++) { 42 | out[i + A + B] <== c[i]; 43 | } 44 | for(var i = 0; i < D; i++) { 45 | out[i + A + B + C] <== d[i]; 46 | } 47 | } 48 | 49 | // Proof-of-Work: Assert keccak(burnKey | revealAmount | burnExtraCommitment | 'EIP-7503') < 2 ^ (256 - 8 * minimumZeroBytes) 50 | // 51 | // Reviewers: 52 | // Keyvan: OK 53 | // 54 | template ProofOfWorkChecker() { 55 | signal input burnKey; 56 | signal input revealAmount; 57 | signal input burnExtraCommitment; 58 | signal input minimumZeroBytes; 59 | 60 | signal burnKeyBytes[32] <== Num2BigEndianBytes(32)(burnKey); 61 | signal revealAmountBytes[32] <== Num2BigEndianBytes(32)(revealAmount); 62 | signal burnExtraCommitmentBytes[32] <== Num2BigEndianBytes(32)(burnExtraCommitment); 63 | signal eip7503[8] <== EIP7503()(); 64 | 65 | var hasherInputLen = 32 + 32 + 32 + 8; 66 | signal hasherInput[hasherInputLen] <== ConcatFixed4(32, 32, 32, 8)( 67 | burnKeyBytes, revealAmountBytes, burnExtraCommitmentBytes, eip7503 68 | ); 69 | 70 | signal burnKeyBlock[136] <== Fit(hasherInputLen, 136)(hasherInput); 71 | signal burnKeyKeccak[32] <== KeccakBytes(1)(burnKeyBlock, hasherInputLen); 72 | 73 | signal shouldBeZero[32] <== Filter(32)(minimumZeroBytes); 74 | 75 | // Assert the first powMinimumZeroBytes bytes of keccak is zero 76 | for(var i = 0; i < 32; i++) { 77 | // If shouldBeZero[i] is 1, then burnKeyKeccak[i] should be zero 78 | // Otherwise it can obtain any value 79 | burnKeyKeccak[i] * shouldBeZero[i] === 0; 80 | } 81 | } -------------------------------------------------------------------------------- /tests/testcases/assertion.py: -------------------------------------------------------------------------------- 1 | test_assert_less_than = ( 2 | "AssertLessThan(3)", 3 | [ 4 | ({"a": 0, "b": 1}, []), 5 | ({"a": 1, "b": 0}, None), 6 | ({"a": 3, "b": 6}, []), 7 | ({"a": 6, "b": 3}, None), 8 | ({"a": 4, "b": 5}, []), 9 | ({"a": 5, "b": 4}, None), 10 | ({"a": 6, "b": 7}, []), 11 | ({"a": 7, "b": 6}, None), 12 | ({"a": 0, "b": 0}, None), 13 | ({"a": 1, "b": 1}, None), 14 | ({"a": 3, "b": 3}, None), 15 | ({"a": 7, "b": 7}, None), 16 | ({"a": 6, "b": 8}, None), 17 | ({"a": 8, "b": 6}, None), 18 | ], 19 | ) 20 | 21 | test_assert_less_eq_than = ( 22 | "AssertLessEqThan(3)", 23 | [ 24 | ({"a": 0, "b": 1}, []), 25 | ({"a": 1, "b": 0}, None), 26 | ({"a": 3, "b": 6}, []), 27 | ({"a": 6, "b": 3}, None), 28 | ({"a": 4, "b": 5}, []), 29 | ({"a": 5, "b": 4}, None), 30 | ({"a": 6, "b": 7}, []), 31 | ({"a": 7, "b": 6}, None), 32 | ({"a": 0, "b": 0}, []), 33 | ({"a": 1, "b": 1}, []), 34 | ({"a": 3, "b": 3}, []), 35 | ({"a": 7, "b": 7}, []), 36 | ({"a": 6, "b": 8}, None), 37 | ({"a": 8, "b": 6}, None), 38 | ], 39 | ) 40 | 41 | test_assert_greater_eq_than = ( 42 | "AssertGreaterEqThan(3)", 43 | [ 44 | ({"a": 0, "b": 1}, None), 45 | ({"a": 1, "b": 0}, []), 46 | ({"a": 3, "b": 6}, None), 47 | ({"a": 6, "b": 3}, []), 48 | ({"a": 4, "b": 5}, None), 49 | ({"a": 5, "b": 4}, []), 50 | ({"a": 6, "b": 7}, None), 51 | ({"a": 7, "b": 6}, []), 52 | ({"a": 0, "b": 0}, []), 53 | ({"a": 1, "b": 1}, []), 54 | ({"a": 3, "b": 3}, []), 55 | ({"a": 7, "b": 7}, []), 56 | ({"a": 6, "b": 8}, None), 57 | ({"a": 8, "b": 6}, None), 58 | ], 59 | ) 60 | 61 | test_assert_byte_string = ( 62 | "AssertByteString(3)", 63 | [ 64 | ({"in": [100, 200, 250]}, []), 65 | ({"in": [250, 0, 1]}, []), 66 | ({"in": [0, 1, 1]}, []), 67 | ({"in": [123, 255, 255]}, []), 68 | ({"in": [255, 255, 255]}, []), 69 | ({"in": [1, 256, 1]}, None), 70 | ], 71 | ) 72 | 73 | test_assert_bits = ( 74 | "AssertBits(3)", 75 | [ 76 | ({"in": 0}, []), 77 | ({"in": 1}, []), 78 | ({"in": 2}, []), 79 | ({"in": 3}, []), 80 | ({"in": 4}, []), 81 | ({"in": 5}, []), 82 | ({"in": 6}, []), 83 | ({"in": 7}, []), 84 | ({"in": 8}, None), 85 | ({"in": 9}, None), 86 | ({"in": 20}, None), 87 | ({"in": 2**100}, None), 88 | ], 89 | ) 90 | -------------------------------------------------------------------------------- /circuits/utils/selector.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "./assert.circom"; 4 | 5 | // Selects the element at the given index `select` from the input array `vals`. 6 | // 7 | // Example: 8 | // vals: [10, 20, 30, 40] 9 | // select: 2 10 | // out: 30 11 | // 12 | // Reviewers: 13 | // Keyvan: OK 14 | // Shahriar: The circuit is OK but why not use Decoder() from `multiplexer` in circomlib? I mean, for the sake of fewer lines of code. Also, why not use `var sum` instead of [n+1] signals? 15 | // - Keyvan's response: Can't accumulate them inside a `var sum` because it makes it non-quadratic. Also I thought the current version is easier to understand than using Decoder gadgets. 16 | // 17 | // Update 26 July 2025: The AssertLessThan(select, n) check is removed and a better check is placed. 18 | // Reviewers: 19 | // Keyvan: OK 20 | // 21 | template Selector(n) { 22 | signal input vals[n]; 23 | signal input select; 24 | signal output out; 25 | 26 | // isEq is the filter: [0, ..., 0, 1, 0, ..., 0] 27 | // Where the ith index is 1 and the rest are 0 28 | signal isEq[n]; 29 | 30 | var sumIsEq = 0; 31 | 32 | signal sum[n + 1]; 33 | sum[0] <== 0; 34 | for(var i = 0; i < n; i++) { 35 | isEq[i] <== IsEqual()([select, i]); 36 | 37 | sumIsEq += isEq[i]; 38 | 39 | // Keep the vals[i] only when i == select 40 | sum[i + 1] <== sum[i] + isEq[i] * vals[i]; 41 | } 42 | 43 | sumIsEq === 1; // Ensure at least one element is selected! 44 | 45 | out <== sum[n]; 46 | } 47 | 48 | 49 | // Selects the 1D-array at the given index `select` from an NxP array of 1D-arrays `arrays`. 50 | // 51 | // Example: 52 | // arrays: [[11, 21, 31, 41], 53 | // [12, 22, 32, 42], 54 | // [13, 23, 33, 43], 55 | // [14, 24, 34, 44]] 56 | // select: 2 57 | // out: [13, 23, 33, 43] 58 | // 59 | // Reviewers: 60 | // Keyvan: OK 61 | // 62 | template SelectorArray1D(n, p) { 63 | signal input arrays[n][p]; 64 | signal input select; 65 | signal output out[p]; 66 | 67 | signal arraysT[p][n]; // Transposed 68 | for(var i = 0; i < n; i++) { 69 | for(var j = 0; j < p; j++) { 70 | arraysT[j][i] <== arrays[i][j]; 71 | } 72 | } 73 | 74 | for(var i = 0; i < p; i++) { 75 | out[i] <== Selector(n)(arraysT[i], select); 76 | } 77 | } 78 | 79 | // Selects the 2D-array at the given index `select` from an NxPxQ array of 2D-arrays `arrays`. 80 | // 81 | // Example: 82 | // arrays: [[[1, 2],[3, 4]], 83 | // [[2, 4],[6, 8]], 84 | // [[3, 6],[9, 12]]] 85 | // select: 1 86 | // out: [[2, 4],[6, 8]] 87 | // 88 | // Reviewers: 89 | // Keyvan: OK 90 | // 91 | template SelectorArray2D(n, p, q) { 92 | signal input arrays[n][p][q]; 93 | signal input select; 94 | signal output out[p][q]; 95 | 96 | signal arraysT[p][q][n]; // Transposed 97 | for(var i = 0; i < n; i++) { 98 | for(var j = 0; j < p; j++) { 99 | for(var k = 0; k < q; k++) { 100 | arraysT[j][k][i] <== arrays[i][j][k]; 101 | } 102 | } 103 | } 104 | 105 | for(var i = 0; i < p; i++) { 106 | for(var j = 0; j < q; j++) { 107 | out[i][j] <== Selector(n)(arraysT[i][j], select); 108 | } 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /tests/testcases/keccak.py: -------------------------------------------------------------------------------- 1 | import web3 2 | 3 | test_pad = ( 4 | "Pad(3, 4)", 5 | [ 6 | ( 7 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 0}, 8 | [0x1, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 1], 9 | ), 10 | ( 11 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 1}, 12 | [5, 0x1, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 1], 13 | ), 14 | ( 15 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 2}, 16 | [5, 5, 0x1, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 1], 17 | ), 18 | ( 19 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 3}, 20 | [5, 5, 5, 0x81, 0, 0, 0, 0, 0, 0, 0, 0, 1], 21 | ), 22 | ( 23 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 4}, 24 | [5, 5, 5, 5, 0x1, 0, 0, 0x80, 0, 0, 0, 0, 2], 25 | ), 26 | ( 27 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 5}, 28 | [5, 5, 5, 5, 5, 0x1, 0, 0x80, 0, 0, 0, 0, 2], 29 | ), 30 | ( 31 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 6}, 32 | [5, 5, 5, 5, 5, 5, 0x1, 0x80, 0, 0, 0, 0, 2], 33 | ), 34 | ( 35 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 7}, 36 | [5, 5, 5, 5, 5, 5, 5, 0x81, 0, 0, 0, 0, 2], 37 | ), 38 | ( 39 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 8}, 40 | [5, 5, 5, 5, 5, 5, 5, 5, 0x1, 0, 0, 0x80, 3], 41 | ), 42 | ( 43 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 9}, 44 | [5, 5, 5, 5, 5, 5, 5, 5, 5, 0x1, 0, 0x80, 3], 45 | ), 46 | ( 47 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 10}, 48 | [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0x1, 0x80, 3], 49 | ), 50 | ( 51 | {"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 11}, 52 | [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0x81, 3], 53 | ), 54 | ({"in": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], "inLen": 12}, None), 55 | ], 56 | ) 57 | 58 | 59 | def keccak(inp): 60 | return list(web3.Web3.keccak(inp)) 61 | 62 | 63 | def blockify(inp, blks): 64 | return list(inp) + [0] * (blks * 136 - len(inp)) 65 | 66 | 67 | test_keccak_1 = ( 68 | "KeccakBytes(1)", 69 | [ 70 | ({"in": blockify(b"", 1), "inLen": 0}, keccak(b"")), 71 | ({"in": blockify(b"salam", 1), "inLen": 5}, keccak(b"salam")), 72 | ({"in": blockify(b"salam", 1), "inLen": 4}, keccak(b"sala")), 73 | ({"in": blockify(b"a" * 135, 1), "inLen": 135}, keccak(b"a" * 135)), 74 | ({"in": blockify(b"a" * 136, 1), "inLen": 136}, None), 75 | ], 76 | ) 77 | 78 | test_keccak_2 = ( 79 | "KeccakBytes(2)", 80 | [ 81 | ({"in": blockify(b"", 2), "inLen": 0}, keccak(b"")), 82 | ({"in": blockify(b"salam", 2), "inLen": 5}, keccak(b"salam")), 83 | ({"in": blockify(b"a" * 135, 2), "inLen": 135}, keccak(b"a" * 135)), 84 | ({"in": blockify(b"a" * 136, 2), "inLen": 136}, keccak(b"a" * 136)), 85 | ({"in": blockify(b"a" * 136, 2), "inLen": 130}, keccak(b"a" * 130)), 86 | ({"in": blockify(b"a" * 137, 2), "inLen": 137}, keccak(b"a" * 137)), 87 | ({"in": blockify(b"a" * 271, 2), "inLen": 271}, keccak(b"a" * 271)), 88 | ({"in": blockify(b"a" * 272, 2), "inLen": 272}, None), 89 | ], 90 | ) 91 | -------------------------------------------------------------------------------- /tests/testcases/selector.py: -------------------------------------------------------------------------------- 1 | test_selector = ( 2 | "Selector(5)", 3 | [ 4 | ({"vals": [0, 10, 20, 30, 40], "select": 2}, [20]), 5 | ({"vals": [0, 10, 20, 30, 40], "select": 0}, [0]), 6 | ({"vals": [0, 10, 20, 30, 40], "select": 4}, [40]), 7 | ({"vals": [0, 10, 20, 30, 40], "select": 5}, None), 8 | ({"vals": [0, 10, 20, 30, 40], "select": 100}, None), 9 | ], 10 | ) 11 | 12 | 13 | test_selector_array_1d = ( 14 | "SelectorArray1D(4, 5)", 15 | [ 16 | ( 17 | { 18 | "arrays": [ 19 | [11, 21, 31, 41, 51], 20 | [12, 22, 32, 42, 52], 21 | [13, 23, 33, 43, 53], 22 | [14, 24, 34, 44, 54], 23 | ], 24 | "select": 0, 25 | }, 26 | [11, 21, 31, 41, 51], 27 | ), 28 | ( 29 | { 30 | "arrays": [ 31 | [11, 21, 31, 41, 51], 32 | [12, 22, 32, 42, 52], 33 | [13, 23, 33, 43, 53], 34 | [14, 24, 34, 44, 54], 35 | ], 36 | "select": 1, 37 | }, 38 | [12, 22, 32, 42, 52], 39 | ), 40 | ( 41 | { 42 | "arrays": [ 43 | [11, 21, 31, 41, 51], 44 | [12, 22, 32, 42, 52], 45 | [13, 23, 33, 43, 53], 46 | [14, 24, 34, 44, 54], 47 | ], 48 | "select": 2, 49 | }, 50 | [13, 23, 33, 43, 53], 51 | ), 52 | ( 53 | { 54 | "arrays": [ 55 | [11, 21, 31, 41, 51], 56 | [12, 22, 32, 42, 52], 57 | [13, 23, 33, 43, 53], 58 | [14, 24, 34, 44, 54], 59 | ], 60 | "select": 3, 61 | }, 62 | [14, 24, 34, 44, 54], 63 | ), 64 | ( 65 | { 66 | "arrays": [ 67 | [11, 21, 31, 41, 51], 68 | [12, 22, 32, 42, 52], 69 | [13, 23, 33, 43, 53], 70 | [14, 24, 34, 44, 54], 71 | ], 72 | "select": 4, 73 | }, 74 | None, 75 | ), 76 | ], 77 | ) 78 | 79 | test_selector_array_2d = ( 80 | "SelectorArray2D(3, 2, 2)", 81 | [ 82 | ( 83 | { 84 | "arrays": [[[1, 2], [3, 4]], [[2, 4], [6, 8]], [[3, 6], [9, 12]]], 85 | "select": 0, 86 | }, 87 | [1, 2, 3, 4], 88 | ), 89 | ( 90 | { 91 | "arrays": [[[1, 2], [3, 4]], [[2, 4], [6, 8]], [[3, 6], [9, 12]]], 92 | "select": 1, 93 | }, 94 | [2, 4, 6, 8], 95 | ), 96 | ( 97 | { 98 | "arrays": [[[1, 2], [3, 4]], [[2, 4], [6, 8]], [[3, 6], [9, 12]]], 99 | "select": 2, 100 | }, 101 | [3, 6, 9, 12], 102 | ), 103 | ( 104 | { 105 | "arrays": [[[1, 2], [3, 4]], [[2, 4], [6, 8]], [[3, 6], [9, 12]]], 106 | "select": 3, 107 | }, 108 | None, 109 | ), 110 | ], 111 | ) 112 | -------------------------------------------------------------------------------- /tests/testcases/substring_check.py: -------------------------------------------------------------------------------- 1 | test_substring_check = ( 2 | "SubstringCheck(10, 3)", 3 | [ 4 | ( 5 | { 6 | "mainInput": [1, 123, 256, 1, 1, 0, 0, 0, 1, 0], 7 | "mainLen": 3, 8 | "subInput": [1, 123, 256], 9 | }, 10 | None, 11 | ), 12 | ( 13 | { 14 | "mainInput": [1, 123, 255, 1, 1, 0, 0, 0, 1, 0], 15 | "mainLen": 3, 16 | "subInput": [1, 123, 255], 17 | }, 18 | [1], 19 | ), 20 | ( 21 | { 22 | "mainInput": [1, 0, 3, 1, 1, 0, 0, 0, 1, 0], 23 | "mainLen": 3, 24 | "subInput": [1, 0, 3], 25 | }, 26 | [1], 27 | ), 28 | ( 29 | { 30 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 31 | "mainLen": 3, 32 | "subInput": [1, 0, 3], 33 | }, 34 | [0], 35 | ), 36 | ( 37 | { 38 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 39 | "mainLen": 3, 40 | "subInput": [1, 0, 256], 41 | }, 42 | None, 43 | ), 44 | ( 45 | { 46 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 47 | "mainLen": 0, 48 | "subInput": [1, 0, 1], 49 | }, 50 | None, 51 | ), 52 | ( 53 | { 54 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 55 | "mainLen": 2, 56 | "subInput": [1, 0, 1], 57 | }, 58 | None, 59 | ), 60 | ( 61 | { 62 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 63 | "mainLen": 3, 64 | "subInput": [1, 0, 1], 65 | }, 66 | [1], 67 | ), 68 | ( 69 | { 70 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 71 | "mainLen": 3, 72 | "subInput": [0, 1, 0], 73 | }, 74 | [0], 75 | ), 76 | ( 77 | { 78 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 79 | "mainLen": 4, 80 | "subInput": [1, 1, 1], 81 | }, 82 | [0], 83 | ), 84 | ( 85 | { 86 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 87 | "mainLen": 5, 88 | "subInput": [1, 1, 1], 89 | }, 90 | [1], 91 | ), 92 | ( 93 | { 94 | "mainInput": [1, 0, 1, 1, 1, 0, 1, 0, 0, 0], 95 | "mainLen": 9, 96 | "subInput": [0, 0, 0], 97 | }, 98 | [0], 99 | ), 100 | ( 101 | { 102 | "mainInput": [1, 0, 1, 1, 1, 0, 1, 0, 0, 0], 103 | "mainLen": 10, 104 | "subInput": [0, 0, 0], 105 | }, 106 | [1], 107 | ), 108 | ( 109 | { 110 | "mainInput": [1, 0, 1, 1, 1, 0, 0, 0, 1, 0], 111 | "mainLen": 11, 112 | "subInput": [0, 1, 0], 113 | }, 114 | None, 115 | ), 116 | ], 117 | ) 118 | -------------------------------------------------------------------------------- /tests/testcases/proof_of_work.py: -------------------------------------------------------------------------------- 1 | test_pow_eip7503_postfix = ( 2 | "EIP7503()", 3 | [ 4 | ( 5 | {}, 6 | list(b"EIP-7503"), 7 | ), 8 | ], 9 | ) 10 | 11 | test_concat_fixed_4 = ( 12 | "ConcatFixed4(1,2,3,4)", 13 | [ 14 | ( 15 | { 16 | "a": [1], 17 | "b": [2, 3], 18 | "c": [4, 5, 6], 19 | "d": [7, 8, 9, 10], 20 | }, 21 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 22 | ), 23 | ], 24 | ) 25 | 26 | test_proof_of_work = ( 27 | "ProofOfWorkChecker()", 28 | [ 29 | ( 30 | { 31 | "burnKey": 123, 32 | "revealAmount": 234, 33 | "burnExtraCommitment": 345, 34 | "minimumZeroBytes": 0, 35 | }, 36 | [], 37 | ), 38 | ( 39 | { 40 | "burnKey": 811, 41 | "revealAmount": 234, 42 | "burnExtraCommitment": 345, 43 | "minimumZeroBytes": 1, 44 | }, 45 | None, 46 | ), 47 | ( 48 | { 49 | "burnKey": 812, # 1 zero bytes 50 | "revealAmount": 234, 51 | "burnExtraCommitment": 345, 52 | "minimumZeroBytes": 1, 53 | }, 54 | [], 55 | ), 56 | ( 57 | { 58 | "burnKey": 813, 59 | "revealAmount": 234, 60 | "burnExtraCommitment": 345, 61 | "minimumZeroBytes": 1, 62 | }, 63 | None, 64 | ), 65 | ( 66 | { 67 | "burnKey": 47109, # 2 zero bytes 68 | "revealAmount": 234, 69 | "burnExtraCommitment": 345, 70 | "minimumZeroBytes": 1, 71 | }, 72 | [], 73 | ), 74 | ( 75 | { 76 | "burnKey": 811, 77 | "revealAmount": 234, 78 | "burnExtraCommitment": 345, 79 | "minimumZeroBytes": 2, 80 | }, 81 | None, 82 | ), 83 | ( 84 | { 85 | "burnKey": 812, # 1 zero bytes 86 | "revealAmount": 234, 87 | "burnExtraCommitment": 345, 88 | "minimumZeroBytes": 2, 89 | }, 90 | None, 91 | ), 92 | ( 93 | { 94 | "burnKey": 813, 95 | "revealAmount": 234, 96 | "burnExtraCommitment": 345, 97 | "minimumZeroBytes": 2, 98 | }, 99 | None, 100 | ), 101 | ( 102 | { 103 | "burnKey": 47108, 104 | "revealAmount": 234, 105 | "burnExtraCommitment": 345, 106 | "minimumZeroBytes": 2, 107 | }, 108 | None, 109 | ), 110 | ( 111 | { 112 | "burnKey": 47109, # 2 zero bytes 113 | "revealAmount": 234, 114 | "burnExtraCommitment": 345, 115 | "minimumZeroBytes": 2, 116 | }, 117 | [], 118 | ), 119 | ( 120 | { 121 | "burnKey": 47110, 122 | "revealAmount": 234, 123 | "burnExtraCommitment": 345, 124 | "minimumZeroBytes": 2, 125 | }, 126 | None, 127 | ), 128 | ], 129 | ) 130 | -------------------------------------------------------------------------------- /circuits/utils/convert.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../circomlib/circuits/bitify.circom"; 4 | include "./array.circom"; 5 | include "./assert.circom"; 6 | 7 | // Converts little-endian bytes to num 8 | // 9 | // Reviewers: 10 | // Keyvan: OK 11 | // 12 | template LittleEndianBytes2Num(N) { 13 | signal input in[N]; 14 | signal output out; 15 | 16 | assert(N <= 31); // Avoid overflows 17 | 18 | AssertByteString(N)(in); 19 | 20 | var lc = 0; 21 | for(var i = 0; i < N; i++) { 22 | lc += (256 ** i) * in[i]; 23 | } 24 | 25 | out <== lc; 26 | } 27 | 28 | // Converts big-endian bytes to num 29 | // 30 | // Reviewers: 31 | // Keyvan: OK 32 | // 33 | template BigEndianBytes2Num(N) { 34 | signal input in[N]; 35 | signal output out; 36 | 37 | signal inReversed[N] <== Reverse(N)(in); 38 | out <== LittleEndianBytes2Num(N)(inReversed); 39 | } 40 | 41 | // Decompose the input number into arbitrary number of bits. Uses Num2Bits_strict when necessary. 42 | // 43 | // Reviewers: 44 | // Keyvan: OK 45 | // 46 | template Num2BitsSafe(N) { 47 | signal input in; 48 | signal output out[N]; 49 | 50 | if(N >= 254) { 51 | signal bitsStrict[254] <== Num2Bits_strict()(in); 52 | out <== Fit(254, N)(bitsStrict); // Set the remaining bytes to zero 53 | } else { 54 | out <== Num2Bits(N)(in); 55 | } 56 | } 57 | 58 | // Decomposes an input number `num` into an array of `N` little-endian bytes. 59 | // Each byte represents 8 bits of the number starting from the least significant byte. 60 | // 61 | // Example: 62 | // num: 66051 63 | // N: 8 64 | // bytes: [3, 2, 1, 0, 0, 0, 0, 0] 65 | // 66 | // Reviewers: 67 | // Keyvan: OK 68 | // 69 | template Num2LittleEndianBytes(N) { 70 | signal input in; 71 | signal output out[N]; 72 | 73 | assert(N <= 32); // Avoid overflows 74 | 75 | // Decompose into bits and arrange them into 8-bit chunks 76 | signal bits[N * 8] <== Num2BitsSafe(N * 8)(in); 77 | signal byteArrays[N][8] <== Reshape(N, 8)(bits); 78 | 79 | // Convert 8-bit chunks to bytes 80 | for (var i = 0; i < N; i++) { 81 | out[i] <== Bits2Num(8)(byteArrays[i]); 82 | } 83 | } 84 | 85 | // Convert a field element to 32 big-endian bytes 86 | // 87 | // Reviewers: 88 | // Keyvan: OK 89 | // 90 | template Num2BigEndianBytes(N) { 91 | signal input in; 92 | signal output out[N]; 93 | 94 | signal littleEndian[N] <== Num2LittleEndianBytes(N)(in); 95 | out <== Reverse(N)(littleEndian); 96 | } 97 | 98 | // Accepts N bytes and outputs 2xN nibbles (As a list of 4-bit numbers) 99 | // 100 | // Reviewers: 101 | // Keyvan: OK 102 | // 103 | template Bytes2Nibbles(N) { 104 | signal input in[N]; 105 | signal output out[2 * N]; // Each byte is 2 nibbles 106 | 107 | signal inDecomposed[N][8]; 108 | 109 | for(var i = 0; i < N; i++) { 110 | inDecomposed[i] <== Num2Bits(8)(in[i]); // Also asserts if in[i] is a byte 111 | var higher = 0; 112 | var lower = 0; 113 | for(var j = 0; j < 4; j++) { 114 | lower += inDecomposed[i][j] * (2 ** j); 115 | higher += inDecomposed[i][j + 4] * (2 ** j); 116 | } 117 | out[2 * i] <== higher; 118 | out[2 * i + 1] <== lower; 119 | } 120 | } 121 | 122 | // Converts an array of nibbles (4-bit values) into an array of bytes (8-bit values). 123 | // Each byte is formed by combining two nibbles (4 bits each). 124 | // 125 | // Example: 126 | // nibbles: [0x1, 0x2, 0x3, 0x4, 0x5, 0x6] 127 | // bytes: [0x12, 0x34, 0x56] 128 | // 129 | // Reviewers: 130 | // Keyvan: OK 131 | // 132 | template Nibbles2Bytes(n) { 133 | signal input nibbles[2 * n]; 134 | signal output bytes[n]; 135 | for(var i = 0; i < n; i++) { 136 | // Check if all nibbles are maximum 4-bit long 137 | AssertBits(4)(nibbles[2 * i]); 138 | AssertBits(4)(nibbles[2 * i + 1]); 139 | 140 | bytes[i] <== nibbles[2 * i] * 16 + nibbles[2 * i + 1]; 141 | } 142 | } -------------------------------------------------------------------------------- /tests/testcases/convert.py: -------------------------------------------------------------------------------- 1 | from ..poseidon import FIELD_SIZE, Field 2 | 3 | 4 | # Number to 256-bit little-endian list 5 | def field_to_be_bytes(elem): 6 | return list(int.to_bytes(elem, 32, "big")) 7 | 8 | 9 | def field_to_le_bits(elem, length): 10 | binary = bin(elem)[2:] 11 | binary = "0" * (length - len(binary)) + binary 12 | return [1 if b == "1" else 0 for b in reversed(list(binary))] 13 | 14 | 15 | test_num_2_bits_safe_32 = ( 16 | "Num2BitsSafe(32)", 17 | [ 18 | ({"in": 0}, field_to_le_bits(0, 32)), 19 | ({"in": 123}, field_to_le_bits(123, 32)), 20 | ({"in": 7**10}, field_to_le_bits(7**10, 32)), 21 | ({"in": 2**32 - 1}, field_to_le_bits(2**32 - 1, 32)), 22 | ({"in": 2**32}, None), 23 | ({"in": 2**32 + 1}, None), 24 | ], 25 | ) 26 | 27 | test_num_2_bits_safe_254 = ( 28 | "Num2BitsSafe(254)", 29 | [ 30 | ({"in": 0}, field_to_le_bits(0, 254)), 31 | ({"in": 123}, field_to_le_bits(123, 254)), 32 | ({"in": 7**10}, field_to_le_bits(7**10, 254)), 33 | ({"in": 2**32 - 1}, field_to_le_bits(2**32 - 1, 254)), 34 | ({"in": 2**32}, field_to_le_bits(2**32, 254)), 35 | ({"in": 2**32 + 1}, field_to_le_bits(2**32 + 1, 254)), 36 | ({"in": str(FIELD_SIZE - 1)}, field_to_le_bits(FIELD_SIZE - 1, 254)), 37 | ], 38 | ) 39 | 40 | 41 | test_num_2_bits_safe_256 = ( 42 | "Num2BitsSafe(256)", 43 | [ 44 | ({"in": 0}, field_to_le_bits(0, 256)), 45 | ({"in": 123}, field_to_le_bits(123, 256)), 46 | ({"in": 7**10}, field_to_le_bits(7**10, 256)), 47 | ({"in": str(FIELD_SIZE - 1)}, field_to_le_bits(FIELD_SIZE - 1, 256)), 48 | ], 49 | ) 50 | 51 | test_num_2_big_endian_bytes = ( 52 | "Num2BigEndianBytes(32)", 53 | [ 54 | ({"in": 123}, field_to_be_bytes(123)), 55 | ({"in": 0}, field_to_be_bytes(0)), 56 | ({"in": 1}, field_to_be_bytes(1)), 57 | ({"in": str(3**150)}, field_to_be_bytes(3**150)), 58 | ({"in": str(FIELD_SIZE - 10)}, field_to_be_bytes(FIELD_SIZE - 10)), 59 | ({"in": str(FIELD_SIZE - 1)}, field_to_be_bytes(FIELD_SIZE - 1)), 60 | ], 61 | ) 62 | 63 | test_bytes_2_nibbles = ( 64 | "Bytes2Nibbles(3)", 65 | [ 66 | ({"in": [0xAB, 0x12, 0xF5]}, [0xA, 0xB, 0x1, 0x2, 0xF, 0x5]), 67 | ], 68 | ) 69 | 70 | test_num_2_little_endian_bytes = ( 71 | "Num2LittleEndianBytes(4)", 72 | [ 73 | ({"in": [0x00]}, [0, 0, 0, 0]), 74 | ({"in": [0xDE]}, [0xDE, 0, 0, 0]), 75 | ({"in": [0xDEAD]}, [0xAD, 0xDE, 0, 0]), 76 | ({"in": [0xDEADBE]}, [0xBE, 0xAD, 0xDE, 0]), 77 | ({"in": [0xDEADBEEF]}, [0xEF, 0xBE, 0xAD, 0xDE]), 78 | ({"in": [0xFF]}, [0xFF, 0, 0, 0]), 79 | ({"in": [0xFFFFFFFF]}, [0xFF, 0xFF, 0xFF, 0xFF]), 80 | ({"in": [0xFFFFFFFF + 1]}, None), 81 | ], 82 | ) 83 | 84 | 85 | test_little_endian_bytes_2_num = ( 86 | "LittleEndianBytes2Num(2)", 87 | [ 88 | ({"in": [0, 0]}, [0]), 89 | ({"in": [0, 1]}, [256]), 90 | ({"in": [1, 0]}, [1]), 91 | ({"in": [1, 1]}, [257]), 92 | ({"in": [0, 128]}, [128 * 256]), 93 | ({"in": [128, 0]}, [128]), 94 | ({"in": [128, 128]}, [128 * 256 + 128]), 95 | ({"in": [12345, 23456]}, None), 96 | ], 97 | ) 98 | 99 | test_big_endian_bytes_2_num = ( 100 | "BigEndianBytes2Num(2)", 101 | [ 102 | ({"in": [0, 0]}, [0]), 103 | ({"in": [0, 1]}, [1]), 104 | ({"in": [1, 0]}, [256]), 105 | ({"in": [1, 1]}, [257]), 106 | ({"in": [0, 128]}, [128]), 107 | ({"in": [128, 0]}, [128 * 256]), 108 | ({"in": [128, 128]}, [128 * 256 + 128]), 109 | ({"in": [12345, 23456]}, None), 110 | ], 111 | ) 112 | 113 | test_nibbles_2_bytes = ( 114 | "Nibbles2Bytes(2)", 115 | [ 116 | ({"nibbles": [1, 2, 3, 4]}, [0x12, 0x34]), 117 | ({"nibbles": [15, 2, 3, 4]}, [0xF2, 0x34]), 118 | ({"nibbles": [16, 2, 3, 4]}, None), 119 | ({"nibbles": [1, 16, 3, 4]}, None), 120 | ], 121 | ) 122 | -------------------------------------------------------------------------------- /circuits/utils/rlp/integer.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../../circomlib/circuits/comparators.circom"; 4 | include "../convert.circom"; 5 | 6 | // Counts the number of bytes required to store the big-endian number (i.e., ignores leading zeros). 7 | // 8 | // Example: 9 | // bytes: [0, 0, 0, 3, 0, 1, 4, 2] (3 leading zeros) 10 | // len: 5 11 | // 12 | // Reviewers: 13 | // Keyvan: OK 14 | // Shahriar: OK 15 | // 16 | template CountBytes(N) { 17 | signal input bytes[N]; 18 | signal output len; 19 | 20 | // Example: 21 | // bytes: [0, 0, 0, 3, 0, 1, 4, 0] 22 | 23 | // Checking zeroness of each byte 24 | // isZero: [1, 1, 1, 0, 1, 0, 0, 1] 25 | signal isZero[N]; 26 | for (var i = 0; i < N; i++) { 27 | isZero[i] <== IsZero()(bytes[i]); 28 | } 29 | 30 | // Accumulating 1s until we reach a zero 31 | // stillZero: [1, 1, 1, 0, 0, 0, 0, 0] 32 | signal stillZero[N]; 33 | for (var i = 0; i < N; i++) { 34 | if(i == 0) { 35 | stillZero[i] <== isZero[i]; 36 | } else { 37 | stillZero[i] <== isZero[i] * stillZero[i - 1]; 38 | } 39 | } 40 | 41 | // Number of leading zeros = Sum of bits in stillZero 42 | var leadingZeros = 0; 43 | for (var j = 0; j < N; j++) { 44 | leadingZeros = leadingZeros + stillZero[j]; 45 | } 46 | 47 | // Number of effective bytes = N - number of leading zeros 48 | len <== N - leadingZeros; 49 | } 50 | 51 | // Returns RLP of an integer up to 31 bytes 52 | // 53 | // Read: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ 54 | // 55 | // Example: 56 | // in: [0], out: [0x80] 57 | // in: [1], out: [0x01] 58 | // in: [10], out: [0x0a] 59 | // in: [127], out: [0x7f] 60 | // in: [128], out: [0x81,0x80] 61 | // in: [255], out: [0x81,0xff] 62 | // in: [256], out: [0x82,0x01,0x00] 63 | // 64 | // Reviewers: 65 | // Keyvan: OK 66 | // 67 | template RlpInteger(N) { 68 | signal input in; 69 | signal output out[N + 1]; 70 | signal output outLen; 71 | 72 | // The assertion is for avoiding overflows. 73 | // Also, the RLP of any number whose value is greater than or equal to 0x80 and whose 74 | // byte-length is less than or equal to 55 starts with: 75 | // [0x80 + num_value_bytes] 76 | // Instead of: 77 | // [0xb7 + num_len_bytes] (Which is the case where length is above 55 bytes) 78 | // (Since the amounts are never more than 31 bytes we'll never need this scenario) 79 | // Read: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ 80 | assert(N <= 31); 81 | 82 | // Calculate the big-endian version of the balance without the leading zeros 83 | signal bytes[N] <== Num2BigEndianBytes(N)(in); 84 | signal length <== CountBytes(N)(bytes); 85 | signal bigEndian[N] <== ShiftLeft(N)(bytes, N - length); 86 | 87 | // If the number is below 128, then the first byte is the number itself 88 | // Except when the number is zero, in that case the first byte is 0x80 89 | // If the number is greater than or equal 128, then the first byte is 90 | // (0x80 + number_of_bytes) 91 | signal isSingleByte <== LessThan(N * 8)([in, 128]); 92 | signal isZero <== IsZero()(in); 93 | signal firstRlpByte <== Mux1()([0x80 + length, in], isSingleByte); 94 | 95 | // If zero: 0 + 0x80 = 0x80 (Correct) 96 | // If below 128: num + 0 = num (Correct) 97 | // If greater than or equal 128: 0x80 + length (Correct) 98 | out[0] <== firstRlpByte + isZero * 0x80; 99 | 100 | // If the number is greater than or equal 128, then comes the rest of 101 | // the big-endian representation of bytes. Otherwise, everything is 0. 102 | for (var i = 1; i < N + 1; i++) { 103 | out[i] <== (1 - isSingleByte) * bigEndian[i - 1]; 104 | } 105 | 106 | // If zero: (1 - 1) + 0 + 1 = 1 (Correct) [0x80] 107 | // If below 128: (1 - 1) + 1 + 0 = 1 (Correct) [num] 108 | // If greater than or equal 128: (1 - 0) + length + 0 = 1 + length (Correct) [0x80 + length, ...] 109 | outLen <== (1 - isSingleByte) + length + isZero; 110 | } -------------------------------------------------------------------------------- /circuits/utils/substring_check.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../circomlib/circuits/comparators.circom"; 4 | include "./convert.circom"; 5 | include "./assert.circom"; 6 | 7 | // Checks whether the byte-string `subInput` is a contiguous substring of `mainInput`. 8 | // 9 | // Example: 10 | // mainInput: [12, 23, 34, 45, 56, 67, 78, 89, 98, 87] 11 | // mainLen: 6 12 | // subInput: [56, 67, 78] 13 | // out: 0 14 | // 15 | // Example: 16 | // mainInput: [12, 23, 34, 45, 56, 67, 78, 89, 98, 87] 17 | // mainLen: 7 18 | // subInput: [56, 67, 78] 19 | // out: 1 20 | // 21 | // Reviewers: 22 | // Keyvan: OK 23 | // 24 | template SubstringCheck(maxMainLen, subLen) { 25 | signal input mainInput[maxMainLen]; 26 | signal input mainLen; 27 | signal input subInput[subLen]; 28 | signal output out; 29 | 30 | assert(subLen <= 31); // So that subInput fits in a field element 31 | 32 | // Substring-checker works with byte-string inputs 33 | AssertByteString(subLen)(subInput); 34 | AssertByteString(maxMainLen)(mainInput); 35 | 36 | AssertLessEqThan(16)(mainLen, maxMainLen); 37 | AssertLessEqThan(16)(subLen, mainLen); 38 | 39 | // Convert the sub-input into a field-element 40 | signal subInputNum <== LittleEndianBytes2Num(subLen)(subInput); 41 | 42 | // M[i] = Number representation of the first i bytes 43 | // If i = 0 --> M[i] = 0 44 | // If i > 0 --> M[i] = 256^0*mainInput[0] + 256^1*mainInput[1] + ... + 256^(i-1)*mainInput[i-1] 45 | signal M[maxMainLen + 1]; 46 | M[0] <== 0; 47 | for (var i = 0; i < maxMainLen; i++) { 48 | M[i + 1] <== mainInput[i] * (256 ** i) + M[i]; 49 | } 50 | // M[i + N] - M[i] = 256^i.mainInput[i] + ... + 256^(i+N-1).mainInput[i+N-1] 51 | // = 256^i.(256^0.mainInput[i] + ... + 256^(N-1).mainInput[i+N-1]) 52 | // = 256^i.(Number representation of M[i..i + N]) 53 | // 54 | // Substring-ness Equation: Substring exists if there is `i` where: 55 | // (256 ^ i) * subInputNum == M[i + subLen] - M[i] 56 | // 57 | // Reasons this is safe: 58 | // 1. If this holds: 59 | // (256 ^ i) * subInputNum == M[i + subLen] - M[i] 60 | // Then: 61 | // (256 ^ i) * subInputNum == 256^i.(256^0.mainInput[i] + ... + 256^(N-1).mainInput[i+N-1]) 62 | // Which means: 63 | // subInputNum == 256^0.mainInput[i] + ... + 256^(N-1).mainInput[i+N-1] 64 | // That's because in a prime-field, when `ab == ac`, then we can conclude that `b == c` 65 | // Thus, all the bytes in M[i..i + subLen] has to be equal with subInput 66 | // 2. Also subInput's length is limited to 31 bytes, so subInputNum will not overflow 67 | // and we won't have unexpected/fancy bugs here. 68 | 69 | 70 | // Existence flags. When exists[i] is 1 it means that: 71 | // mainInput[i..i + subLen] == subInput 72 | signal exists[maxMainLen - subLen + 1]; 73 | 74 | // Used for creating an `allowed` filter: [1, 1, ..., 1, 1, 0, 0, ..., 0, 0] 75 | // Where the first `mainLen - subLen` elements are 1, indicating the existence 76 | // flags that should be considered. 77 | signal isLastIndex[maxMainLen - subLen + 1]; 78 | signal allowed[maxMainLen - subLen + 2]; 79 | allowed[0] <== 1; 80 | 81 | // For summing up all the *allowed* existence flags. 82 | signal sums[maxMainLen - subLen + 2]; 83 | sums[0] <== 0; 84 | 85 | for (var i = 0; i < maxMainLen - subLen + 1; i++) { 86 | // Building the `allowed` filter 87 | isLastIndex[i] <== IsEqual()([i, mainLen - subLen + 1]); 88 | allowed[i + 1] <== allowed[i] * (1 - isLastIndex[i]); 89 | 90 | // Existence check 91 | exists[i] <== IsEqual()([subInputNum * (256 ** i), M[i + subLen] - M[i]]); 92 | 93 | // Existence flag is accumulated in the sum only when we are in the allowed region 94 | sums[i + 1] <== sums[i] + allowed[i + 1] * exists[i]; 95 | } 96 | 97 | // Substring exists only when there has been a 1 while summing up the existence flags 98 | signal doesNotExist <== IsZero()(sums[maxMainLen - subLen + 1]); 99 | out <== 1 - doesNotExist; 100 | } -------------------------------------------------------------------------------- /circuits/utils/burn_address.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../circomlib/circuits/poseidon.circom"; 4 | include "./convert.circom"; 5 | include "./array.circom"; 6 | include "./constants.circom"; 7 | 8 | // The burn-address is the first 20 bytes of: 9 | // Poseidon4(POSEIDON_BURN_ADDRESS_PREFIX, burnKey, revealAmount, burnExtraCommitment) 10 | // 11 | // The burn-address is bound to: 12 | // 1. burnKey: A random secret from which the burn-address and the nullifier are derived 13 | // 2. revealAmount: The amount of BETH to be revealed and minted upon proof submission 14 | // 3. burnExtraCommitment: Extra commitment on burn-address, useful to enforce how the revealed amount 15 | // should be distributed by the contract 16 | // The commitment itself is hash of several values: 17 | // * receiverAddress: The address authorized to collect the revealed BETH coins 18 | // * proverFeeAmount: The amount of BETH that can be collected by the proof generator 19 | // * broadcasterFeeAmount: The amount of BETH that can be collected by the tx broadcaster 20 | // * sellAmount: The amount of BETH that can be sold in exchange of ETH (By the relayer) 21 | // (NOTE: The remaining BETH amount is revealed as an encrypted coin, 22 | // which can be partially revealed later through the Spend circuit) 23 | // 24 | // The bounds ensure that the burner can simply send their ETH to a derived address and 25 | // delegate the responsibility of generating a proof and submitting it to the blockchain 26 | // to the relayer, without the risk of the relayer taking all the BETH tokens for 27 | // themselves or charging more fees than specified by the burner. However, it is better 28 | // for the burner to generate the proof themselves; otherwise, the burner's identity will 29 | // be leaked to the relayer. 30 | // 31 | // Attack scenarios when revealing the burnKey to a relayer and delegating proof creation: 32 | // 33 | // - If we do not commit to the feeAmount: The proof generator may collect all tokens by 34 | // arbitrarily setting the feeAmount to the maximum possible value. 35 | // - If we do not commit to the revealAmount: The proof generator may set the revealAmount 36 | // to 0, then reveal and spend the encrypted coin themselves through the Spend circuit, 37 | // since they possess the burnKey. 38 | // (WARNING: When proof creation is delegated to someone else, always set the revealAmount 39 | // to the maximum intended amount. Otherwise, the proof generator will be able to spend 40 | // the remaining amount for themselves, as they hold the burnKey!) 41 | // 42 | // Reviewers: 43 | // Keyvan: OK 44 | // (UPDATE 21st September 2025: Also committing to the reveal amount to prevent a potential attack scenario.) 45 | // (UPDATE 3rd November 2025: Generalize commitment through a burnExtraCommitment signal.) 46 | // 47 | template BurnAddress() { 48 | signal input burnKey; 49 | signal input revealAmount; 50 | signal input burnExtraCommitment; 51 | signal output addressBytes[20]; 52 | 53 | // Take the first 20-bytes of 54 | // Poseidon4(POSEIDON_BURN_ADDRESS_PREFIX, burnKey, revealAmount, burnExtraCommitment) as a burn-address 55 | signal hash <== Poseidon(4)([POSEIDON_BURN_ADDRESS_PREFIX(), burnKey, revealAmount, burnExtraCommitment]); 56 | signal hashBytes[32] <== Num2BigEndianBytes(32)(hash); 57 | addressBytes <== Fit(32, 20)(hashBytes); 58 | } 59 | 60 | // Returns Keccak of a burn-address as 64 4-bit nibbles. 61 | // Ethereum state-trie maps address-hashes to accounts, that's why we return the 62 | // Keccak of address instead of the address itself. 63 | // 64 | // Reviewers: 65 | // Keyvan: OK 66 | // 67 | template BurnAddressHash() { 68 | signal input burnKey; 69 | signal input revealAmount; 70 | signal input burnExtraCommitment; 71 | signal output addressHashNibbles[64]; 72 | 73 | // Calculate the address to which the burnt coins are sent 74 | signal addressBytes[20] <== BurnAddress()(burnKey, revealAmount, burnExtraCommitment); 75 | 76 | // Feed the address-bytes in the big-endian form to keccak in order to take the 77 | // address-hash which will be used as the key of the MPT leaf 78 | signal addressBytesBlock[136] <== Fit(20, 136)(addressBytes); 79 | signal addressHash[32] <== KeccakBytes(1)(addressBytesBlock, 20); 80 | 81 | // Convert the burn-address-hash to 64 4-bit nibbles 82 | addressHashNibbles <== Bytes2Nibbles(32)(addressHash); 83 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥 Private-Proof-of-Burn circuits in Circom 🔥 2 | 3 | [![Test circuits](https://github.com/worm-privacy/proof-of-burn/actions/workflows/test.yml/badge.svg)](https://github.com/worm-privacy/proof-of-burn/actions/workflows/test.yml) 4 | [![Discord](https://img.shields.io/discord/1213528108796350484)](https://discord.gg/EIP7503) 5 | [![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/EIP7503)](https://x.com/EIP7503) 6 | 7 | ***(The circuits are audited by yAudit: https://reports.yaudit.dev/2025-11-Worm)*** 8 | 9 | The circuit calculates the account-rlp of a burn address and then generate the leaf-trie-node accordingly. 10 | 11 | It will then iterate through trie nodes and check whether `keccak(layer[i])` is within `keccak(layer[i+1])`. 12 | 13 | As a public input, it will expect a single public commitment which itself is Keccak hash of multiple values (In order to optimize gas usage): 14 | 15 | 1. The `blockRoot`: the state root of the block being referenced, passed by a Solidity contract. 16 | 2. A `nullifier`: `Poseidon2(POSEIDON_NULLIFIER_PREFIX, burnKey)`, used to prevent revealing the same burn address more than once. 17 | 3. An encrypted representation of the remaining balance: `Poseidon3(POSEIDON_COIN_PREFIX, burnKey, intendedBalance - revealAmount)`. 18 | 4. A `revealAmount`: an amount from the minted balance that is directly revealed upon submission of the proof. 19 | 5. A `burnExtraCommitment`: commits to the way the revealed amount should be distributed by the contract. 20 | 6. A `_proofExtraCommitment`: to glue information to the proof that aren't necessarily processed in the circuit. 21 | 22 | ## Burn-key 23 | 24 | Burn-key is a number you generate in order to start the burn/mint process. It somehow is your "private-key" to the world of EIP-7503. 25 | 26 | - Burn-address: `Truncate160(Poseidon4(POSEIDON_BURN_ADDRESS_PREFIX, burnKey, revealAmount, burnExtraCommitment))` 27 | - Is the 160 first bits of the Poseidon4 hash of a random-number `burnKey`, and 2 other values we are commiting to: 28 | - A `revealAmount` which specifies part of the BETH that will be minted upon submission of the proof. 29 | - A `burnExtraCommitment` which commits on the way the minted amount should be distributed by the contract after being minted. 30 | - PoW: `Keccak(burnKey | revealAmount | burnExtraCommitment | 'EIP-7503') < THRESHOLD` 31 | Only burn-keys which fit in the equation can be used. This is in order to increase the bit-security of the protocol. 32 | - Nullifier: `Poseidon2(POSEIDON_NULLIFIER_PREFIX, burnKey)` 33 | Nullifier prevents us from using the burn-key again. 34 | - Coin: `Poseidon3(POSEIDON_COIN_PREFIX, burnKey, amount)` 35 | A "coin" is an encrypted amount which can be partially withdrawn, resulting in a new coin. 36 | 37 | The burn-address hash, which is present in the Merkle-Patricia-Trie leaf key for which we provide a proof, is calculated using the following formula: 38 | `f₄(f₃(f₂(f₁(burnKey, receiverAddress, burnExtraCommitment))))`, where the inputs are all 254-bit numbers (finite-field elements), resulting in a 254 × 3 bit input space. The function `f₁` is Poseidon2 with a 254-bit output space. The output is then passed to `f₂(x)`, which selects the first 160 bits to produce an Ethereum address, yielding a 160-bit output space. This is followed by `f₃`, which is Keccak with a 256-bit output space, and finally `f₄`, which truncates the result to at least 50 nibbles (200 bits), giving a 200-bit output space. 39 | 40 | Since the smallest output space among these functions is 160 bits (due to `f₂`), the overall security of this scheme is limited by that step. 41 | 42 | Thus, we consider the Merkle-Patricia-Trie leaves in this scheme to provide 160-bit preimage resistance, which corresponds to 160-bit security against such brute-force attacks. 43 | 44 | While the Merkle-Patricia-Trie leaf key construction offers 160-bit preimage resistance due to the truncation to a 160-bit Ethereum address, this may not be sufficient for long-term or high-assurance applications. To strengthen the scheme, we add an additional constraint: the Keccak256 hash of `burnKey | revealAmount | burnExtraCommitment | 'EIP-7503'` must begin with two zero bytes. Since each zero byte contributes 8 bits of difficulty, this adds 16 bits of security, raising the effective brute-force cost from 2^160 to 2^176. This constraint filters out the vast majority of candidate inputs, ensuring that only those satisfying both the original hash chain and the prefix condition are considered valid, thereby increasing the overall security of the scheme. 45 | 46 | ## Test Locally 47 | 48 | 1. Install Rust toolkit 49 | - `curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh` 50 | 2. Install Circom 51 | - `git clone https://github.com/iden3/circom.git` 52 | - `cd circom && cargo install --path circom` 53 | 3. Clone this repo 54 | - `git clone --recurse-submodules https://github.com/worm-privacy/proof-of-burn` 55 | - `cd proof-of-burn` 56 | 4. Install Python dependencies 57 | - `python -m venv .venv` 58 | - `source .venv/bin/activate` 59 | - `pip install -r tests/requirements.txt` 60 | 5. Start Ganache *or* Anvil (Foundry) server 61 | - `ganache -d` 62 | - `anvil --mnemonic "myth like bonus scare over problem client lizard pioneer submit female collect"` 63 | 6. Run the Makefile 64 | - `make` 65 | 7. Run the tests 66 | - `python -m tests.test` 67 | 68 | After running `make`, the `main.py` script will first initiate a transfer to a burn-address and will then generate an input file for the circuit. Then it will try to generate a witness file through the Circom-generated C program. 69 | 70 | ## License 71 | 72 | This project is licensed under the MIT License. 73 | 74 | The Circom implementation of the Keccak hash function is a refactored version of [`keccak256-circom`](https://github.com/vocdoni/keccak256-circom) developed by [@vocdoni](https://github.com/vocdoni), which is licensed under the GNU GPL v3. 75 | -------------------------------------------------------------------------------- /tests/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import web3 3 | import rlp 4 | from hexbytes.main import HexBytes 5 | from .poseidon import poseidon4, Field, FIELD_SIZE 6 | from .constants import * 7 | 8 | MAX_HEADER_BYTES = 5 * 136 9 | MAX_LAYER_BYTES = 4 * 136 10 | MAX_NUM_LAYERS = 4 11 | POW_MIN_ZERO_BYTES = 2 12 | 13 | w3 = web3.Web3(provider=web3.Web3.HTTPProvider("http://127.0.0.1:8545")) 14 | 15 | 16 | def burn(burn_key, burn_extra_commit, reveal): 17 | recv = web3.Web3.to_int(hexstr=receiver) 18 | account_1 = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 19 | private_key = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" 20 | nonce = w3.eth.get_transaction_count(account_1) 21 | hashed = w3.to_bytes( 22 | poseidon4( 23 | POSEIDON_BURN_ADDRESS_PREFIX, 24 | Field(burn_key), 25 | Field(reveal), 26 | Field(burn_extra_commit), 27 | ).val 28 | ) 29 | addr = list(hashed[:20]) 30 | burn_addr = w3.to_checksum_address(bytes(addr)) 31 | tx = { 32 | "nonce": nonce, 33 | "to": burn_addr, 34 | "value": w3.to_wei(1, "ether"), 35 | "gas": 2000000, 36 | "gasPrice": w3.to_wei(50, "gwei"), 37 | } 38 | signed_tx = w3.eth.account.sign_transaction(tx, private_key) 39 | tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) 40 | assert w3.eth.wait_for_transaction_receipt(tx_hash).status == 1 41 | return burn_addr 42 | 43 | 44 | import random 45 | 46 | 47 | def find_burn_key(burn_extra_commit, reveal, min_zero_bytes): 48 | postfix = ( 49 | int.to_bytes(reveal, 32, "big") 50 | + int.to_bytes(burn_extra_commit, 32, "big") 51 | + b"EIP-7503" 52 | ) 53 | burn_key = random.randint(0, FIELD_SIZE - 1) 54 | while any(w3.keccak(int.to_bytes(burn_key, 32, "big") + postfix)[:min_zero_bytes]): 55 | burn_key += 1 56 | return burn_key 57 | 58 | 59 | burn_extra_commit = 43287974328 60 | spend = 234 61 | receiver = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 62 | burn_key = find_burn_key(burn_extra_commit, spend, POW_MIN_ZERO_BYTES) 63 | addr = burn(burn_key, burn_extra_commit, spend) 64 | 65 | blknum = w3.eth.block_number 66 | proof = w3.eth.get_proof(addr, [], blknum) 67 | block = w3.eth.get_block(blknum) 68 | 69 | addr_hash = w3.keccak(hexstr=addr) 70 | leaf = proof.accountProof[-1] 71 | (term, account_rlp) = tuple(rlp.decode(leaf)) 72 | if term[0] & 0xF0 == 0x20: 73 | addr_term_len = 2 * len(term) - 2 74 | elif term[0] & 0xF0 == 0x30: 75 | addr_term_len = 2 * len(term) - 1 76 | else: 77 | raise Exception("Invalid") 78 | # print(term.hex(), addr_term_len) 79 | # exit(0) 80 | MAX_TERM_LEN = 64 81 | term_len = len(term) 82 | term = list(term) + [0] * (MAX_TERM_LEN - term_len) 83 | 84 | hashes = [ 85 | block.parentHash.hex(), 86 | block.sha3Uncles.hex(), 87 | block.miner, 88 | block.stateRoot.hex(), 89 | block.transactionsRoot.hex(), 90 | block.receiptsRoot.hex(), 91 | block.logsBloom.hex(), 92 | hex(block.difficulty), 93 | hex(block.number), 94 | hex(block.gasLimit), 95 | hex(block.gasUsed), 96 | hex(block.timestamp), 97 | block.extraData.hex(), 98 | block.mixHash.hex(), 99 | block.nonce.hex(), 100 | ] 101 | 102 | optional_headers = [ 103 | "baseFeePerGas", 104 | "withdrawalsRoot", 105 | "blobGasUsed", 106 | "excessBlobGas", 107 | "parentBeaconBlockRoot", 108 | "requestsHash", 109 | ] 110 | 111 | for header in optional_headers: 112 | if hasattr(block, header): 113 | v = getattr(block, header) 114 | if isinstance(v, HexBytes): 115 | hashes.append(v.hex()) 116 | elif isinstance(v, int): 117 | hashes.append(hex(v)) 118 | else: 119 | hashes.append(v) 120 | 121 | hashes = ["0x" if h == "0x0" else h for h in hashes] 122 | header = rlp.encode([w3.to_bytes(hexstr=h) for h in hashes]) 123 | 124 | 125 | # def bytes_to_bits(bytes): 126 | # out = [] 127 | # for byte in bytes: 128 | # lst = [int(a) for a in list(reversed(bin(byte)[2:]))] 129 | # for i in range(8): 130 | # out.append(lst[i] if i < len(lst) else 0) 131 | # return out 132 | 133 | 134 | header_bytes = list(header) 135 | header_bytes_len = len(header_bytes) 136 | header_bytes = header_bytes + [0] * (MAX_HEADER_BYTES - header_bytes_len) 137 | 138 | layers = [] 139 | layer_lens = [] 140 | for layer_bytes in list(proof.accountProof): 141 | layer = list(layer_bytes) 142 | layer_len = len(layer) 143 | layer += [0] * (MAX_LAYER_BYTES - layer_len) 144 | layers.append(layer) 145 | layer_lens.append(layer_len) 146 | 147 | num_layers = len(layers) 148 | while len(layers) < MAX_NUM_LAYERS: 149 | layers.append([0] * MAX_LAYER_BYTES) 150 | layer_lens.append(256) 151 | import io 152 | 153 | with io.open("details.json", "w") as f: 154 | json.dump( 155 | [ 156 | block.hash.hex(), 157 | ], 158 | f, 159 | ) 160 | with io.open("circuits/main_proof_of_burn_cpp/input.json", "w") as f: 161 | json.dump( 162 | { 163 | "numLeafAddressNibbles": str(addr_term_len), 164 | "burnKey": str(burn_key), 165 | "burnExtraCommitment": burn_extra_commit, 166 | "actualBalance": str(proof.balance), 167 | "intendedBalance": str(proof.balance), 168 | "revealAmount": str(spend), 169 | "numLayers": num_layers, 170 | "layers": layers, 171 | "layerLens": layer_lens, 172 | "blockHeader": header_bytes, 173 | "blockHeaderLen": header_bytes_len, 174 | "byteSecurityRelax": 0, 175 | "_proofExtraCommitment": 0, 176 | }, 177 | f, 178 | ) 179 | 180 | spend_withdraw = 321 181 | spend_broadcaster_fee = 12 182 | with io.open("circuits/main_spend_cpp/input.json", "w") as f: 183 | json.dump( 184 | { 185 | "burnKey": str(burn_key), 186 | "withdrawnBalance": str(spend_withdraw), 187 | "balance": str(proof.balance - spend), 188 | "broadcasterFee": str(spend_broadcaster_fee), 189 | "receiverAddress": str(int(str(receiver)[2:], 16)), 190 | }, 191 | f, 192 | ) 193 | -------------------------------------------------------------------------------- /circuits/utils/rlp/empty_account.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../concat.circom"; 4 | include "./integer.circom"; 5 | 6 | // Returns RLP([NONCE, balance, STORAGE_HASH, CODE_HASH]) 7 | // Where: 8 | // NONCE = 0 9 | // STORAGE_HASH = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 10 | // CODE_HASH = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 11 | // 12 | // The result is the RLP of a "list" of "bytes" where the total length of the bytes is 13 | // always this range: 68 <= length <= 99, which is more than 55, thus RLP prefix of [0xf7 + len] 14 | // and less than 256, which allows it to be represented using a single byte) 15 | // Read: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ 16 | // 17 | // Reviewers: 18 | // Keyvan: OK 19 | // 20 | template RlpEmptyAccount(maxBalanceBytes) { 21 | signal input balance; 22 | 23 | assert(maxBalanceBytes <= 31); // Avoid overflows 24 | 25 | // Minimum RLP length is 1 (NONCE) + 1 (balance) + 33 (STORAGE_HASH) + 33 (CODE_HASH) = 68 (> 55 bytes) 26 | // Maximum RLP length is 1 (NONCE) + 32 (balance) + 33 (STORAGE_HASH) + 33 (CODE_HASH) = 99 (< 256, less than a byte) 27 | // So, the two-byte prefix is always [0xf7 + 1, TOTAL_BYTES_LEN] (Given the range: 55 < byte < 256) 28 | // (When the number of bytes in a list is less-than-equal 55, then the prefix is [0xc0 + len]) 29 | // Read: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ 30 | 31 | // [0xf7 + 1, TOTAL_BYTES_LEN, 0x80 (Nonce: 0), BALANCE_BYTES_PREFIX] 32 | // + BALANCE_BYTES + [0x80 + 32] + STORAGE_HASH + [0x80 + 32] + CODE_HASH 33 | signal output out[4 + maxBalanceBytes + 66]; 34 | signal output outLen; 35 | 36 | // 4 prefix bytes: [0xf8, TOTAL_BYTES_LEN, 0x80 (Nonce: 0), BALANCE_BYTES_PREFIX] 37 | signal prefixedNonceAndBalanceRlp[4 + maxBalanceBytes]; 38 | signal prefixedNonceAndBalanceRlpLen; 39 | 40 | prefixedNonceAndBalanceRlp[2] <== 0x80; // Nonce of a burn-address is always zero (RLP: 0x80) 41 | signal (balanceRlp[maxBalanceBytes + 1], balanceRlpLen) <== RlpInteger(maxBalanceBytes)(balance); 42 | for(var i = 0; i < maxBalanceBytes + 1; i++) { 43 | prefixedNonceAndBalanceRlp[i + 3] <== balanceRlp[i]; 44 | } 45 | 46 | signal nonceAndBalanceRlpLen; // Without the [0xf8, TOTAL_BYTES_LEN] prefix 47 | nonceAndBalanceRlpLen <== 1 + balanceRlpLen; // Nonce (Len: 1) and then the RLP of balance (Len: balanceRlpLen) 48 | prefixedNonceAndBalanceRlpLen <== 2 + nonceAndBalanceRlpLen; // [0xf8, TOTAL_BYTES_LEN] is the prefix 49 | 50 | // Concatenated RLP of storage-hash and code-hash of an empty account 51 | // Storage-hash: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 52 | // Code-hash: 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 53 | var storageAndCodeHashRlpLen = 66; // 1 + 32 + 1 + 32 (32-byte chunks are prefixed) 54 | signal storageAndCodeHashRlp[storageAndCodeHashRlpLen]; 55 | storageAndCodeHashRlp[0] <== 160; // Prefix: 0x80 + 32 56 | storageAndCodeHashRlp[1] <== 86; 57 | storageAndCodeHashRlp[2] <== 232; 58 | storageAndCodeHashRlp[3] <== 31; 59 | storageAndCodeHashRlp[4] <== 23; 60 | storageAndCodeHashRlp[5] <== 27; 61 | storageAndCodeHashRlp[6] <== 204; 62 | storageAndCodeHashRlp[7] <== 85; 63 | storageAndCodeHashRlp[8] <== 166; 64 | storageAndCodeHashRlp[9] <== 255; 65 | storageAndCodeHashRlp[10] <== 131; 66 | storageAndCodeHashRlp[11] <== 69; 67 | storageAndCodeHashRlp[12] <== 230; 68 | storageAndCodeHashRlp[13] <== 146; 69 | storageAndCodeHashRlp[14] <== 192; 70 | storageAndCodeHashRlp[15] <== 248; 71 | storageAndCodeHashRlp[16] <== 110; 72 | storageAndCodeHashRlp[17] <== 91; 73 | storageAndCodeHashRlp[18] <== 72; 74 | storageAndCodeHashRlp[19] <== 224; 75 | storageAndCodeHashRlp[20] <== 27; 76 | storageAndCodeHashRlp[21] <== 153; 77 | storageAndCodeHashRlp[22] <== 108; 78 | storageAndCodeHashRlp[23] <== 173; 79 | storageAndCodeHashRlp[24] <== 192; 80 | storageAndCodeHashRlp[25] <== 1; 81 | storageAndCodeHashRlp[26] <== 98; 82 | storageAndCodeHashRlp[27] <== 47; 83 | storageAndCodeHashRlp[28] <== 181; 84 | storageAndCodeHashRlp[29] <== 227; 85 | storageAndCodeHashRlp[30] <== 99; 86 | storageAndCodeHashRlp[31] <== 180; 87 | storageAndCodeHashRlp[32] <== 33; 88 | storageAndCodeHashRlp[33] <== 160; // Prefix: 0x80 + 32 89 | storageAndCodeHashRlp[34] <== 197; 90 | storageAndCodeHashRlp[35] <== 210; 91 | storageAndCodeHashRlp[36] <== 70; 92 | storageAndCodeHashRlp[37] <== 1; 93 | storageAndCodeHashRlp[38] <== 134; 94 | storageAndCodeHashRlp[39] <== 247; 95 | storageAndCodeHashRlp[40] <== 35; 96 | storageAndCodeHashRlp[41] <== 60; 97 | storageAndCodeHashRlp[42] <== 146; 98 | storageAndCodeHashRlp[43] <== 126; 99 | storageAndCodeHashRlp[44] <== 125; 100 | storageAndCodeHashRlp[45] <== 178; 101 | storageAndCodeHashRlp[46] <== 220; 102 | storageAndCodeHashRlp[47] <== 199; 103 | storageAndCodeHashRlp[48] <== 3; 104 | storageAndCodeHashRlp[49] <== 192; 105 | storageAndCodeHashRlp[50] <== 229; 106 | storageAndCodeHashRlp[51] <== 0; 107 | storageAndCodeHashRlp[52] <== 182; 108 | storageAndCodeHashRlp[53] <== 83; 109 | storageAndCodeHashRlp[54] <== 202; 110 | storageAndCodeHashRlp[55] <== 130; 111 | storageAndCodeHashRlp[56] <== 39; 112 | storageAndCodeHashRlp[57] <== 59; 113 | storageAndCodeHashRlp[58] <== 123; 114 | storageAndCodeHashRlp[59] <== 250; 115 | storageAndCodeHashRlp[60] <== 216; 116 | storageAndCodeHashRlp[61] <== 4; 117 | storageAndCodeHashRlp[62] <== 93; 118 | storageAndCodeHashRlp[63] <== 133; 119 | storageAndCodeHashRlp[64] <== 164; 120 | storageAndCodeHashRlp[65] <== 112; 121 | 122 | prefixedNonceAndBalanceRlp[0] <== 0xf7 + 1; // + 1, because the next byte is the number of total bytes 123 | prefixedNonceAndBalanceRlp[1] <== nonceAndBalanceRlpLen + storageAndCodeHashRlpLen; 124 | 125 | // Final result: RLP(0, balance, storageHash, codeHash) 126 | component concat = Concat(4 + maxBalanceBytes, 66); 127 | concat.a <== prefixedNonceAndBalanceRlp; 128 | concat.aLen <== prefixedNonceAndBalanceRlpLen; 129 | concat.b <== storageAndCodeHashRlp; 130 | concat.bLen <== storageAndCodeHashRlpLen; 131 | 132 | out <== concat.out; 133 | outLen <== concat.outLen; 134 | } -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import io 2 | import subprocess 3 | import json 4 | 5 | 6 | def run(main, test_cases): 7 | print() 8 | print(f"Testing {main}") 9 | print("=" * 20) 10 | with io.open("circuits/test.circom", "w") as f: 11 | imports = """ 12 | pragma circom 2.2.2; 13 | 14 | include "./circomlib/circuits/poseidon.circom"; 15 | include "utils/shift.circom"; 16 | include "utils/public_commitment.circom"; 17 | include "utils/concat.circom"; 18 | include "utils/rlp/integer.circom"; 19 | include "utils/rlp/empty_account.circom"; 20 | include "utils/rlp/merkle_patricia_trie_leaf.circom"; 21 | include "utils/selector.circom"; 22 | include "utils/substring_check.circom"; 23 | include "utils/array.circom"; 24 | include "utils/divide.circom"; 25 | include "utils/convert.circom"; 26 | include "utils/keccak.circom"; 27 | include "proof_of_burn.circom"; 28 | include "spend.circom"; 29 | 30 | """ 31 | f.write(imports + f"component main = {main};") 32 | subprocess.run(["circom", "-c", "circuits/test.circom", "--O0"]) 33 | with io.open("test_cpp/main.cpp") as f: 34 | test_cpp = f.read() 35 | test_cpp = test_cpp.replace( 36 | "fclose(write_ptr);", 37 | """fclose(write_ptr); 38 | std::ofstream out("output.json",std::ios::binary | std::ios::out); 39 | out<<"["<getWitness(i + 1, &v); 43 | out<<"\\\""<= 2 bytes) 147 | ), 148 | ( 149 | { 150 | "addressHashNibbles": [0xA, 0xB, 0xC, 0xD, 0xE, 0xF], 151 | "addressHashNibblesLen": 1, # 0x3a 152 | "balance": 123, 153 | }, 154 | None, # (Assertion fails since keyLen should always be >= 2 bytes) 155 | ), 156 | ( 157 | { 158 | "addressHashNibbles": [0xA, 0xB, 0xC, 0xD, 0xE, 0xF], 159 | "addressHashNibblesLen": 2, # 0x20 0xef 160 | "balance": 123, 161 | }, 162 | [0xF8, 75, 0x82, 0x20, 0xEF, 0xB8, empty_len] + empty + [0, 0, 0, 0, 0, 77], 163 | ), 164 | ( 165 | { 166 | "addressHashNibbles": [0xA, 0xB, 0xC, 0xD, 0xE, 0xF], 167 | "addressHashNibblesLen": 3, # 0x3d 0xef 168 | "balance": 123, 169 | }, 170 | [0xF8, 75, 0x82, 0x3D, 0xEF, 0xB8, empty_len] + empty + [0, 0, 0, 0, 0, 77], 171 | ), 172 | ( 173 | { 174 | "addressHashNibbles": [0xA, 0xB, 0xC, 0xD, 0xE, 0xF], 175 | "addressHashNibblesLen": 4, # 0x20 0xcd 0xef 176 | "balance": 123, 177 | }, 178 | [0xF8, 76, 0x83, 0x20, 0xCD, 0xEF, 0xB8, empty_len] 179 | + empty 180 | + [0, 0, 0, 0, 78], 181 | ), 182 | ( 183 | { 184 | "addressHashNibbles": [0xA, 0xB, 0xC, 0xD, 0xE, 0xF], 185 | "addressHashNibblesLen": 6, # 0x20 0xab 0xcd 0xef 186 | "balance": 123, 187 | }, 188 | [0xF8, 77, 0x84, 0x20, 0xAB, 0xCD, 0xEF, 0xB8, empty_len] 189 | + empty 190 | + [0, 0, 0, 79], 191 | ), 192 | ], 193 | ) 194 | -------------------------------------------------------------------------------- /tests/test_pob_input.json: -------------------------------------------------------------------------------- 1 | {"numLeafAddressNibbles": "63", "burnKey": "1671628602202555361902489448752361352669123470013386773134699973022354581491", "burnExtraCommitment": 43287974328, "actualBalance": "1000000000000000000", "intendedBalance": "1000000000000000000", "revealAmount": "234", "numLayers": 2, "layers": [[249, 1, 49, 160, 219, 218, 251, 12, 175, 99, 150, 224, 178, 166, 239, 66, 222, 213, 123, 228, 84, 199, 250, 143, 165, 52, 54, 239, 17, 75, 15, 156, 247, 46, 5, 27, 128, 128, 128, 160, 135, 13, 218, 106, 54, 119, 1, 216, 183, 251, 157, 147, 80, 94, 171, 98, 196, 150, 92, 215, 177, 196, 109, 99, 157, 247, 164, 136, 155, 120, 77, 236, 160, 74, 113, 52, 175, 239, 202, 52, 236, 98, 21, 185, 203, 49, 32, 176, 148, 118, 254, 93, 198, 54, 88, 2, 23, 221, 133, 37, 246, 141, 52, 218, 116, 128, 160, 19, 202, 85, 66, 141, 72, 177, 69, 86, 48, 215, 194, 89, 137, 160, 199, 109, 65, 170, 250, 64, 202, 107, 37, 178, 72, 92, 39, 45, 181, 18, 140, 128, 160, 49, 88, 36, 43, 183, 248, 42, 185, 120, 164, 225, 72, 66, 83, 66, 103, 19, 138, 202, 183, 139, 19, 149, 225, 93, 149, 160, 87, 20, 50, 232, 78, 160, 184, 252, 89, 209, 170, 207, 127, 101, 209, 192, 81, 19, 193, 27, 196, 149, 131, 215, 211, 144, 154, 29, 134, 122, 241, 123, 203, 210, 189, 117, 151, 177, 160, 109, 7, 107, 174, 194, 8, 23, 225, 239, 219, 147, 158, 53, 175, 141, 5, 96, 168, 73, 77, 101, 167, 146, 86, 199, 191, 127, 77, 168, 18, 194, 236, 128, 160, 220, 96, 18, 235, 187, 48, 133, 188, 195, 159, 27, 203, 169, 181, 204, 36, 234, 100, 250, 35, 226, 120, 144, 175, 209, 124, 225, 201, 194, 225, 72, 119, 160, 219, 46, 103, 111, 16, 120, 201, 155, 154, 71, 160, 110, 145, 72, 118, 147, 32, 85, 131, 149, 7, 48, 122, 79, 31, 72, 93, 75, 186, 87, 133, 206, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [248, 113, 160, 53, 46, 18, 19, 172, 172, 238, 91, 231, 109, 191, 196, 2, 152, 174, 78, 62, 185, 30, 154, 179, 187, 41, 221, 91, 109, 152, 157, 242, 2, 139, 69, 184, 78, 248, 76, 128, 136, 13, 224, 182, 179, 167, 100, 0, 0, 160, 86, 232, 31, 23, 27, 204, 85, 166, 255, 131, 69, 230, 146, 192, 248, 110, 91, 72, 224, 27, 153, 108, 173, 192, 1, 98, 47, 181, 227, 99, 180, 33, 160, 197, 210, 70, 1, 134, 247, 35, 60, 146, 126, 125, 178, 220, 199, 3, 192, 229, 0, 182, 83, 202, 130, 39, 59, 123, 250, 216, 4, 93, 133, 164, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], "layerLens": [308, 115, 256, 256], "blockHeader": [249, 2, 97, 160, 164, 33, 240, 161, 61, 228, 209, 93, 15, 145, 26, 253, 68, 123, 254, 232, 28, 169, 245, 152, 88, 203, 57, 249, 13, 174, 188, 29, 24, 33, 208, 49, 160, 29, 204, 77, 232, 222, 199, 93, 122, 171, 133, 181, 103, 182, 204, 212, 26, 211, 18, 69, 27, 148, 138, 116, 19, 240, 161, 66, 253, 64, 212, 147, 71, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 185, 55, 209, 126, 176, 203, 206, 193, 131, 162, 171, 50, 25, 247, 90, 7, 228, 82, 81, 71, 181, 6, 59, 123, 234, 204, 115, 198, 8, 43, 4, 186, 160, 253, 210, 221, 124, 213, 179, 47, 247, 217, 241, 39, 254, 23, 190, 131, 200, 67, 102, 116, 243, 135, 242, 50, 135, 212, 245, 72, 135, 67, 30, 139, 112, 160, 5, 107, 35, 251, 186, 72, 6, 150, 182, 95, 229, 165, 155, 143, 33, 72, 161, 41, 145, 3, 196, 245, 125, 248, 57, 35, 58, 242, 207, 76, 162, 210, 185, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 132, 1, 201, 195, 128, 130, 82, 8, 132, 105, 7, 89, 88, 128, 160, 205, 103, 107, 53, 76, 56, 47, 38, 153, 91, 49, 242, 246, 98, 88, 72, 230, 83, 239, 41, 123, 56, 39, 1, 3, 208, 192, 223, 204, 200, 30, 222, 136, 0, 0, 0, 0, 0, 0, 0, 0, 132, 45, 167, 47, 17, 160, 86, 232, 31, 23, 27, 204, 85, 166, 255, 131, 69, 230, 146, 192, 248, 110, 91, 72, 224, 27, 153, 108, 173, 192, 1, 98, 47, 181, 227, 99, 180, 33, 128, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "blockHeaderLen": 612, "byteSecurityRelax": 0, "_proofExtraCommitment": 0} 2 | -------------------------------------------------------------------------------- /circuits/proof_of_burn.circom: -------------------------------------------------------------------------------- 1 | // __ _____ ____ __ __ 2 | // \ \ / / _ \| _ \| \/ | 3 | // \ \ /\ / / | | | |_) | |\/| | 4 | // \ V V /| |_| | _ <| | | | 5 | // \_/\_/ \___/|_| \_\_| |_| 6 | // 7 | 8 | pragma circom 2.2.2; 9 | 10 | include "./circomlib/circuits/poseidon.circom"; 11 | include "./utils/keccak.circom"; 12 | include "./utils/substring_check.circom"; 13 | include "./utils/concat.circom"; 14 | include "./utils/rlp/merkle_patricia_trie_leaf.circom"; 15 | include "./utils/public_commitment.circom"; 16 | include "./utils/proof_of_work.circom"; 17 | include "./utils/burn_address.circom"; 18 | include "./utils/constants.circom"; 19 | 20 | // Proves that there exists an account in a certain Ethereum block's state root, with a `balance` amount of ETH, 21 | // such that its address equals the first 20 bytes of: 22 | // Poseidon4(POSEIDON_BURN_ADDRESS_PREFIX, burnKey, revealAmount, burnExtraCommitment). 23 | // This is achieved by revealing some publicly verifiable inputs through a *single* public input — the Keccak hash 24 | // of 8 elements: 25 | // 26 | // 1. The `blockRoot`: the state root of the block being referenced, passed by a Solidity contract. 27 | // 2. A `nullifier`: Poseidon2(POSEIDON_NULLIFIER_PREFIX, burnKey), used to prevent revealing the same burn address more than once. 28 | // *** In the case of minting a 1:1 BETH token in exchange for burnt ETH: *** 29 | // 3. An encrypted representation of the remaining balance: Poseidon3(POSEIDON_COIN_PREFIX, burnKey, intendedBalance - revealAmount). 30 | // 4. A `revealAmount`: an amount from the minted balance that is directly revealed upon submission of the proof. 31 | // 5. A `burnExtraCommitment`: commits to the way the revealed amount should be distributed by the contract. (E.g the amounts of prover and broadcaster fees) 32 | // 6. A `_proofExtraCommitment`: to glue information to the proof that aren't necessarily processed in the circuit. (E.g prover address) 33 | // 34 | template ProofOfBurn(maxNumLayers, maxNodeBlocks, maxHeaderBlocks, minLeafAddressNibbles, amountBytes, powMinimumZeroBytes, maxIntendedBalance, maxActualBalance) { 35 | 36 | /***************************/ 37 | /* START OF IN/OUT SIGNALS */ 38 | /***************************/ 39 | 40 | // Public commitment: Keccak(blockRoot, nullifier, remainingCoin, revealAmount, burnExtraCommitment, proofExtraCommitment) 41 | signal output commitment; 42 | 43 | signal input burnKey; // Secret field number from which the burn address and nullifier are derived. 44 | signal input actualBalance; // Actual balance of the burn-address (May contain dust coming from attackers) 45 | signal input intendedBalance; // Intended balance of the burn-address (Without dust) 46 | 47 | // In case there is a 1:1 token to be minted: 48 | signal input revealAmount; // You can reveal part of minted amount upon creation 49 | signal input burnExtraCommitment; // Commit to the way revealAmount is distributed by the contract through a commitment 50 | // The rest of the balance (intendedBalance - revealAmount) is revealed as 51 | // an encrypted-coin which can later be minted through the spend.circom circuit 52 | 53 | signal input numLeafAddressNibbles; // Number of address nibbles in the leaf node (< 64) 54 | 55 | // Merkle-Patricia-Trie nodes data where: 56 | // 1. keccak(layers[0]) === stateRoot 57 | // 2. layers[numLayers - 1] === 58 | // Rlp( 59 | // addressHashNibbles[-numLeafAddressNibble:], 60 | // Rlp(NONCE, actualBalance, EMPTY_STORAGE_HASH, EMPTY_CODE_HASH) 61 | // ) 62 | signal input layers[maxNumLayers][maxNodeBlocks * 136]; // MPT nodes in bytes 63 | signal input layerLens[maxNumLayers]; // Byte length of MPT nodes 64 | signal input numLayers; // Number of MPT nodes 65 | 66 | // Block-header data where: keccak(blockHeader) == blockRoot 67 | signal input blockHeader[maxHeaderBlocks * 136]; // Block header bytes which should be hashed into blockRoot 68 | signal input blockHeaderLen; // Length of block header in bytes 69 | 70 | signal input byteSecurityRelax; // Relax the minLeafAddressNibbles by increasing PoW zero bytes 71 | 72 | signal input _proofExtraCommitment; // Commit to some extra arbitrary input 73 | 74 | /*************************/ 75 | /* END OF IN/OUT SIGNALS */ 76 | /*************************/ 77 | 78 | /******************************/ 79 | /* START OF INPUT VALIDATIONS */ 80 | /******************************/ 81 | 82 | assert(amountBytes <= 31); 83 | 84 | AssertLessEqThan(amountBytes * 8)(intendedBalance, maxIntendedBalance); 85 | AssertLessEqThan(amountBytes * 8)(actualBalance, maxActualBalance); 86 | AssertLessEqThan(amountBytes * 8)(intendedBalance, actualBalance); 87 | 88 | // At least `minLeafAddressNibbles` nibbles should be present in the leaf node 89 | // The prover can relax the security by doing more PoW 90 | AssertLessEqThan(16)(byteSecurityRelax * 2, minLeafAddressNibbles); 91 | AssertGreaterEqThan(16)(numLeafAddressNibbles, minLeafAddressNibbles - byteSecurityRelax * 2); 92 | 93 | // revealAmount should be less than the amount being minted 94 | // revealAmount will NOT overflow since intendedBalance/revealAmount 95 | // amounts are limited to `amountBytes` bytes which is <= 31. 96 | AssertBits(amountBytes * 8)(revealAmount); 97 | AssertLessEqThan(amountBytes * 8)(revealAmount, intendedBalance); 98 | 99 | for(var i = 0; i < maxNumLayers; i++) { 100 | // Check layer lens are less than maximum length 101 | AssertLessThan(16)(layerLens[i], maxNodeBlocks * 136 * 8); 102 | AssertByteString(maxNodeBlocks * 136)(layers[i]); 103 | } 104 | // Check block-header len is less than maximum length 105 | AssertLessThan(16)(blockHeaderLen, maxHeaderBlocks * 136 * 8); 106 | AssertByteString(maxHeaderBlocks * 136)(blockHeader); 107 | 108 | /****************************/ 109 | /* END OF INPUT VALIDATIONS */ 110 | /****************************/ 111 | 112 | // Calculate encrypted-balance of the remaining-coin 113 | signal remainingCoin <== Poseidon(3)([POSEIDON_COIN_PREFIX(), burnKey, intendedBalance - revealAmount]); 114 | 115 | // Calculate nullifier 116 | signal nullifier <== Poseidon(2)([POSEIDON_NULLIFIER_PREFIX(), burnKey]); 117 | 118 | // Calculate keccak hash of a burn-address 119 | signal addressHashNibbles[64] <== BurnAddressHash()(burnKey, revealAmount, burnExtraCommitment); 120 | 121 | // Calculate the block-root 122 | signal blockRoot[32] <== KeccakBytes(maxHeaderBlocks)(blockHeader, blockHeaderLen); 123 | 124 | // Fetch the stateRoot from the block-header 125 | var stateRootOffset = 91; // stateRoot starts from byte 91 of the block-header 126 | signal stateRoot[32]; 127 | for(var i = 0; i < 32; i++) { 128 | stateRoot[i] <== blockHeader[stateRootOffset + i]; 129 | } 130 | 131 | // Calculate public commitment 132 | signal nullifierBytes[32] <== Num2BigEndianBytes(32)(nullifier); 133 | signal remainingCoinBytes[32] <== Num2BigEndianBytes(32)(remainingCoin); 134 | signal revealAmountBytes[32] <== Num2BigEndianBytes(32)(revealAmount); 135 | signal burnExtraCommitmentBytes[32] <== Num2BigEndianBytes(32)(burnExtraCommitment); 136 | signal extraCommitmentBytes[32] <== Num2BigEndianBytes(32)(_proofExtraCommitment); 137 | commitment <== PublicCommitment(6)( 138 | [blockRoot, nullifierBytes, remainingCoinBytes, revealAmountBytes, burnExtraCommitmentBytes, extraCommitmentBytes] 139 | ); 140 | 141 | // layers[numLayers - 1] 142 | signal lastLayer[maxNodeBlocks * 136] <== SelectorArray1D( 143 | maxNumLayers, maxNodeBlocks * 136)(layers, numLayers - 1); 144 | 145 | // layerLens[numLayer - 1] 146 | signal lastLayerLen <== Selector(maxNumLayers)(layerLens, numLayers - 1); 147 | 148 | // Calculate keccaks of all layers and check if the keccak of each 149 | // layer is substring of the upper layer 150 | signal layerExists[maxNumLayers] <== Filter(maxNumLayers)(numLayers); // layerExists[i] <== i < numLayers 151 | signal substringCheckers[maxNumLayers - 1]; 152 | signal layerKeccaks[maxNumLayers][32]; 153 | signal reducedLayerKeccaks[maxNumLayers][31]; 154 | signal isLeaf[maxNumLayers]; 155 | 156 | var numDetectedLeaves = 0; 157 | for(var i = 0; i < maxNumLayers; i++) { 158 | // Check if layers[i] is a MPT leaf 159 | isLeaf[i] <== LeafDetector(maxNodeBlocks * 136)(layers[i], layerLens[i]); 160 | numDetectedLeaves += isLeaf[i]; 161 | 162 | // Calculate keccak of this layer 163 | layerKeccaks[i] <== KeccakBytes(maxNodeBlocks)(layers[i], layerLens[i]); 164 | 165 | // Ignore the last byte of keccak so that the bytes fit in a field element 166 | reducedLayerKeccaks[i] <== Fit(32, 31)(layerKeccaks[i]); 167 | 168 | // Check if keccak(layers[i]) is substring of layers[i - 1] 169 | if(i > 0) { 170 | substringCheckers[i - 1] <== SubstringCheck(maxNodeBlocks * 136, 31)( 171 | subInput <== reducedLayerKeccaks[i], 172 | mainLen <== layerLens[i - 1], 173 | mainInput <== layers[i - 1] 174 | ); 175 | 176 | // Check substring-ness only when the layer exists 177 | // - When layer doesn't exist: (1 - substringChecker) * 0 === 0 (Correct) 178 | // - When layer exists: (1 - substringChecker) * 1 === 0 -> substringChecker === 1 (Correct) 179 | (1 - substringCheckers[i - 1]) * layerExists[i] === 0; 180 | } 181 | } 182 | 183 | // Only the last layer of the trie proof should look like a leaf! Otherwise 184 | // attackers may fake proofs by putting arbitrary strings in codeHash/storageHash 185 | // fields of an account. 186 | numDetectedLeaves === 1; 187 | signal isLastLayerLeaf <== LeafDetector(maxNodeBlocks * 136)(lastLayer, lastLayerLen); 188 | isLastLayerLeaf === 1; 189 | 190 | // Keccak of the top layer should be equal with the claimed state-root 191 | for(var i = 0; i < 32; i++) { 192 | layerKeccaks[0][i] === stateRoot[i]; 193 | } 194 | 195 | var maxLeafLen = 139; 196 | 197 | // Calculate leaf-layer through address-hash and its balance 198 | signal (leaf[maxLeafLen], leafLen) <== RlpMerklePatriciaTrieLeaf(32, amountBytes)( 199 | addressHashNibbles, numLeafAddressNibbles, actualBalance 200 | ); 201 | 202 | // Make sure the calculated leaf-layer is equal with the last-layer 203 | for(var i = 0; i < maxLeafLen; i++) { 204 | leaf[i] === lastLayer[i]; 205 | } 206 | leafLen === lastLayerLen; 207 | 208 | // Check if PoW has been done in order to find burnKey 209 | // The user can increase the PoW zero-bytes through `byteSecurityRelax` and relax 210 | // the minimum number of leaf-key bytes needed. 211 | ProofOfWorkChecker()(burnKey, revealAmount, burnExtraCommitment, powMinimumZeroBytes + byteSecurityRelax); 212 | } -------------------------------------------------------------------------------- /circuits/utils/rlp/merkle_patricia_trie_leaf.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.2.2; 2 | 3 | include "../../circomlib/circuits/comparators.circom"; 4 | include "../../circomlib/circuits/mux1.circom"; 5 | include "../convert.circom"; 6 | include "../shift.circom"; 7 | include "../concat.circom"; 8 | include "./empty_account.circom"; 9 | 10 | // Takes `2 * addressHashBytes` nibbles and keeps `addressHashNibblesLen` of them with this pattern: 11 | // 12 | // 1. If even number of nibbles: [0x20, rest_of_the_nibbles_as_bytes] 13 | // 2. If odd number of nibbles: [0x30 + first_nibble, rest_of_the_nibbles_as_bytes] 14 | // 15 | // (Read more: https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie/#specification) 16 | // 17 | // Example: 18 | // in: [0x1, 0x2, 0x3, 0x4] 19 | // len: 4 20 | // out: [0x20, 0x12, 0x34] 21 | // outLen: 3 22 | // 23 | // Example: 24 | // in: [0x1, 0x2, 0x3, 0x4] 25 | // len: 3 26 | // out: [0x32, 0x34, 0x00] 27 | // outLen: 2 28 | // 29 | // Example: 30 | // in: [0x1, 0x2, 0x3, 0x4] 31 | // len: 2 32 | // out: [0x20, 0x34, 0x00] 33 | // outLen: 2 34 | // 35 | // Example: 36 | // in: [0x1, 0x2, 0x3, 0x4] 37 | // len: 1 38 | // out: [0x34, 0x00, 0x00] 39 | // outLen: 1 40 | // 41 | // Example: 42 | // in: [0x1, 0x2, 0x3, 0x4] 43 | // len: 0 44 | // out: [0x20, 0x00, 0x00] 45 | // outLen: 1 46 | // 47 | // Reviewers: 48 | // Keyvan: OK 49 | // 50 | template TruncatedAddressHash(addressHashBytes) { 51 | signal input addressHashNibbles[2 * addressHashBytes]; 52 | signal input addressHashNibblesLen; 53 | signal output out[addressHashBytes + 1]; 54 | signal output outLen; 55 | 56 | // addressHash is at most 32 bytes (64 nibbles) so 7 bits 57 | AssertLessEqThan(7)(addressHashNibblesLen, 2 * addressHashBytes); 58 | 59 | // Check odd/evenness 60 | signal (div, rem) <== Divide(7)(addressHashNibblesLen, 2); 61 | 62 | // Shift left (2 * addressHashBytes - addressHashNibblesLen) times so that 63 | // the last addressHashNibblesLen nibbles remain 64 | signal shifted[2 * addressHashBytes] <== ShiftLeft(2 * addressHashBytes)( 65 | addressHashNibbles, 2 * addressHashBytes - addressHashNibblesLen); 66 | 67 | signal outNibbles[2 * addressHashBytes + 2]; 68 | // 2 if even number of nibbles, 3 if odd number of nibbles 69 | outNibbles[0] <== 2 + rem; 70 | 71 | // If odd number of nibbles, the second nibble of the result 72 | // should be the first nibble of the remaining value 73 | // If even number of nibbles, the second nibble is 0 74 | outNibbles[1] <== rem * shifted[0]; 75 | 76 | signal temp[2 * addressHashBytes - 1]; 77 | 78 | // If odd number of nibbles, shift-right by one 79 | for(var i = 0; i < 2 * addressHashBytes; i++) { 80 | if(i < 2 * addressHashBytes - 1) { 81 | outNibbles[i + 2] <== Mux1()([shifted[i], shifted[i + 1]], rem); 82 | } else { 83 | // Avoid index out of bound 84 | outNibbles[i + 2] <== (1 - rem) * shifted[i]; 85 | } 86 | } 87 | 88 | out <== Nibbles2Bytes(addressHashBytes + 1)(outNibbles); 89 | outLen <== 1 + div; 90 | } 91 | 92 | // Returns RLP(key, value) 93 | // Where: 94 | // key: addressHash (Truncated to addressHashNibblesLen nibbles with certain encoding) 95 | // value: RLP([NONCE, balance, STORAGE_HASH, CODE_HASH]) 96 | // 97 | // Read: https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie/ 98 | // 99 | // Reviewers: 100 | // Keyvan: OK 101 | // 102 | template RlpMerklePatriciaTrieLeaf(maxAddressHashBytes, maxBalanceBytes) { 103 | signal input addressHashNibbles[2 * maxAddressHashBytes]; 104 | signal input addressHashNibblesLen; 105 | signal input balance; 106 | 107 | assert(maxAddressHashBytes <= 32); 108 | assert(maxBalanceBytes <= 31); 109 | 110 | // Min length: 4 + 0 + 66 = 70 111 | // Max length: 4 + 31 + 66 = 101 112 | // The "value" in a leaf node is RLP of an account 113 | var maxRlpEmptyAccountLen = 4 + maxBalanceBytes + 66; // More info in RlpEmptyAccount gadget 114 | assert(maxRlpEmptyAccountLen <= 101); 115 | 116 | // Min length: 2 + 70 = 72 117 | // Max length: 2 + 101 = 103 118 | // Byte-strings of length more than 55 bytes and less than 256 bytes are prefixed with: 119 | // [0xb7 + 1, STRING_LEN] 120 | var maxValueRlpLen = 2 + maxRlpEmptyAccountLen; // Prefix: [0xb7 + 1, VALUE_LEN] 121 | assert(maxValueRlpLen <= 103); 122 | 123 | // Leaf keys are prefixed with 0x20 or 0x3_ 124 | // Read: https://ethereum.org/de/developers/docs/data-structures-and-encoding/patricia-merkle-trie/#specification 125 | // Min length: 2 (We later put an AssertGreaterEqThan on the keyLen to avoid keyLens lower than 2) 126 | // Max length: 33 127 | var maxKeyLen = 1 + maxAddressHashBytes; 128 | assert(maxKeyLen <= 33); 129 | 130 | // keyLen is at least 2 bytes and at most 33 bytes, so [0x80 + keyLen] is the correct prefix 131 | // Min length: 3 132 | // Max length: 34 133 | var maxKeyRlpLen = 1 + maxKeyLen; // Prefix: [0x80 + KEY_LEN] 134 | assert(maxKeyRlpLen <= 34); 135 | 136 | // KEY_RLP_LEN + VALUE_RLP_LEN is minimum (3 + 73 = 76) and maximum (34 + 103 = 137) bytes 137 | // So the correct prefix is always: [0xf7 + 1, KEY_RLP_LEN + VALUE_RLP_LEN] 138 | var maxPrefixedKeyRlpLen = 2 + maxKeyRlpLen; 139 | assert(maxKeyRlpLen + maxValueRlpLen <= 137); 140 | 141 | // maxBalanceBytes + maxAddressHashBytes + 76 142 | var maxOutLen = maxPrefixedKeyRlpLen + maxValueRlpLen; 143 | 144 | signal output out[maxOutLen]; 145 | signal output outLen; 146 | 147 | // Calculate the MPT leaf key based on the address-hash nibbles 148 | signal (key[maxKeyLen], keyLen) <== TruncatedAddressHash(maxAddressHashBytes)(addressHashNibbles, addressHashNibblesLen); 149 | 150 | // A minimum of 2 bytes so that the prefix of key is always [0x80 + len] 151 | AssertGreaterEqThan(16)(keyLen, 2); 152 | 153 | signal ( 154 | rlpEmptyAccount[maxRlpEmptyAccountLen], rlpEmptyAccountLen 155 | ) <== RlpEmptyAccount(maxBalanceBytes)(balance); 156 | 157 | // Key is the RLP-encoding of part of the address-hash. 158 | // (NOTE: This is also prefixed with the RLP of the whole leaf) 159 | signal prefixedKeyRlp[maxPrefixedKeyRlpLen]; 160 | signal prefixedKeyRlpLen; 161 | 162 | // Value is the RLP-encoding of an empty-account RLP. 163 | signal valueRlp[maxValueRlpLen]; 164 | signal valueRlpLen; 165 | 166 | valueRlp[0] <== 0xb7 + 1; 167 | valueRlp[1] <== rlpEmptyAccountLen; 168 | for(var i = 0; i < maxRlpEmptyAccountLen; i++) { 169 | valueRlp[i + 2] <== rlpEmptyAccount[i]; 170 | } 171 | valueRlpLen <== 2 + rlpEmptyAccountLen; 172 | 173 | // Prefix of the whole leaf: RLP([key, value]) 174 | prefixedKeyRlp[0] <== 0xf7 + 1; 175 | prefixedKeyRlp[1] <== (keyLen + 1) + valueRlpLen; // (keyLen + 1) is keyRlpLen 176 | 177 | prefixedKeyRlp[2] <== 0x80 + keyLen; 178 | for(var i = 0; i < maxKeyLen; i++) { 179 | prefixedKeyRlp[i + 3] <== key[i]; 180 | } 181 | prefixedKeyRlpLen <== 3 + keyLen; 182 | 183 | (out, outLen) <== Concat(maxPrefixedKeyRlpLen, maxValueRlpLen)( 184 | a <== prefixedKeyRlp, 185 | aLen <== prefixedKeyRlpLen, 186 | b <== valueRlp, 187 | bLen <== valueRlpLen 188 | ); 189 | } 190 | 191 | // Checks if lower <= value <= upper 192 | // 193 | // Reviewers: 194 | // Keyvan: OK 195 | // 196 | template IsInRange(B) { 197 | signal input lower; 198 | signal input value; 199 | signal input upper; 200 | signal output out; 201 | AssertBits(B)(lower); 202 | AssertBits(B)(value); 203 | AssertBits(B)(upper); 204 | signal lowerLteValue <== LessEqThan(B)([lower, value]); // lower <= value 205 | signal valueLteUpper <== LessEqThan(B)([value, upper]); // value <= upper 206 | out <== lowerLteValue * valueLteUpper; // lowerLteValue && valueLteUpper (lower <= value <= upper) 207 | } 208 | 209 | // Checks if the input layer looks like a MPT leaf 210 | // 211 | // Pattern of a leaf node: 212 | // [0xf8, keyLen + valueLen + 5, 0x80 + keyLen] + key + [0xb8, valueLen + 2, 0xf8, valueLen] + value 213 | // 214 | // Here are the smallest and largest possible leaves as samples: 215 | // 216 | // Smallest: 217 | // rlp.encode( 218 | // [ 219 | // b"\x20", # Zero nibbles (Total 1 bytes) 220 | // rlp.encode([0, 0, b"\xff" * 32, b"\xff" * 32]), 221 | // ] 222 | // ) 223 | // f84920b846f8448080a0ffffffffffffffffffffffffffffff 224 | // ffffffffffffffffffffffffffffffffffa0ffffffffffffff 225 | // ffffffffffffffffffffffffffffffffffffffffffffffffff 226 | // (75 bytes) 227 | // 228 | // Largest: 229 | // rlp.encode( 230 | // [ 231 | // b"\x20" + b"\xff" * 32, # 0x20 + 32 bytes (64 nibbles) hash (Total 33 bytes) 232 | // rlp.encode([2**256 - 1, 2**256 - 1, b"\xff" * 32, b"\xff" * 32]), 233 | // ] 234 | // ) 235 | // f8aaa120ffffffffffffffffffffffffffffffffffffffffff 236 | // ffffffffffffffffffffffb886f884a0ffffffffffffffffff 237 | // ffffffffffffffffffffffffffffffffffffffffffffffa0ff 238 | // ffffffffffffffffffffffffffffffffffffffffffffffffff 239 | // ffffffffffffa0ffffffffffffffffffffffffffffffffffff 240 | // ffffffffffffffffffffffffffffa0ffffffffffffffffffff 241 | // ffffffffffffffffffffffffffffffffffffffffffff 242 | // (172 bytes) 243 | // 244 | // Reviewers: 245 | // Keyvan: OK 246 | // 247 | template LeafDetector(N) { 248 | signal input layer[N]; 249 | signal input layerLen; 250 | signal output isLeaf; 251 | 252 | AssertLessEqThan(16)(layerLen, N); 253 | 254 | // Leaves all start with 0xf8 because of their min/max size (75 bytes / 172 bytes, more than 55 bytes) 255 | signal leafPrefixIsF8 <== IsEqual()([layer[0], 0xf8]); 256 | signal totalLength <== layer[1]; 257 | signal isConsistentWithLayerLen <== IsEqual()([totalLength + 2, layerLen]); // 2 prefix bytes 258 | signal keyPrefix <== layer[2]; 259 | 260 | // Key prefix is never above 0xb7 since key has maximum of 33 bytes (Less than 55 bytes) 261 | signal keyPrefixIsValid <== LessEqThan(16)([keyPrefix, 0xb7]); 262 | 263 | // If 0x81 <= keyPrefix <= 0xb7 then key has more than a single byte. 264 | signal keyIsMultiByte <== IsInRange(16)(0x81, keyPrefix, 0xb7); 265 | 266 | // keyExtraLen is the extra number of bytes that come after (0x80 + len) prefix. 267 | // 268 | // - In case keyPrefix is less than 0x80, there won't be any extra bytes, 269 | // so 0. 270 | // - In case keyPrefix is more than 0xb7, then we make it 0 just to prevent 271 | // the Selector components from panicking because of out-of-range assertions. 272 | // 273 | signal keyExtraLen <== keyIsMultiByte * (keyPrefix - 0x80); 274 | signal keyLen <== 1 + keyExtraLen; 275 | 276 | // Value comes right after the key 277 | // Value is between 70-134 bytes (More than 55 bytes) 278 | // Value is wrapped in an outer RLP [0xb7 + 1, valueLen + 2, 0xf7 + 1, valueLen] + value 279 | signal valueWrapperPrefix <== Selector(N)(layer, 2 + keyLen + 0); 280 | signal valueWrapperPrefixIsB8 <== IsEqual()([valueWrapperPrefix, 0xb8]); 281 | signal valueWrapperLen <== Selector(N)(layer, 2 + keyLen + 1); 282 | 283 | signal valuePrefix <== Selector(N)(layer, 2 + keyLen + 2); 284 | signal valuePrefixIsF8 <== IsEqual()([valuePrefix, 0xf8]); 285 | signal valueLen <== Selector(N)(layer, 2 + keyLen + 2 + 1); 286 | signal isValueWrapperLenConsistent <== IsEqual()([valueWrapperLen, valueLen + 2]); 287 | signal isKeyValueLenEqualWithLayerLen <== IsEqual()([keyLen + valueLen + 6, layerLen]); 288 | 289 | isLeaf <== MultiAND(7)([ 290 | leafPrefixIsF8, isConsistentWithLayerLen, keyPrefixIsValid, 291 | valueWrapperPrefixIsB8, isValueWrapperLenConsistent, 292 | valuePrefixIsF8, isKeyValueLenEqualWithLayerLen 293 | ]); 294 | } -------------------------------------------------------------------------------- /circuits/utils/keccak.circom: -------------------------------------------------------------------------------- 1 | // Keccak256 hash function (ethereum version). 2 | // For LICENSE check https://github.com/vocdoni/keccak256-circom/blob/master/LICENSE 3 | 4 | pragma circom 2.2.2; 5 | 6 | include "../circomlib/circuits/gates.circom"; 7 | include "../circomlib/circuits/comparators.circom"; 8 | include "./selector.circom"; 9 | include "./array.circom"; 10 | include "./divide.circom"; 11 | 12 | // Example: 13 | // in: [1, 2, 3, 4, 5], n: 3 14 | // out: [4, 5, 0, 0, 0] 15 | // 16 | // Reviewers: 17 | // Keyvan: OK 18 | // 19 | template ShR(n, r) { 20 | signal input in[n]; 21 | signal output out[n]; 22 | 23 | for (var i = 0; i < n; i++) { 24 | if (i + r >= n) { 25 | out[i] <== 0; 26 | } else { 27 | out[i] <== in[i + r]; 28 | } 29 | } 30 | } 31 | 32 | 33 | // Example: 34 | // in: [1, 2, 3, 4, 5], n: 3 35 | // out: [0, 0, 0, 1, 2] 36 | // 37 | // Reviewers: 38 | // Keyvan: OK 39 | // 40 | template ShL(n, r) { 41 | signal input in[n]; 42 | signal output out[n]; 43 | 44 | for (var i = 0; i < n; i++) { 45 | if (i < r) { 46 | out[i] <== 0; 47 | } else { 48 | out[i] <== in[i - r]; 49 | } 50 | } 51 | } 52 | 53 | // Xor5 using four Xor arrays 54 | // 55 | // Reviewers: 56 | // Keyvan: OK 57 | // 58 | template Xor5(n) { 59 | signal input a[n]; 60 | signal input b[n]; 61 | signal input c[n]; 62 | signal input d[n]; 63 | signal input e[n]; 64 | signal output out[n]; 65 | 66 | signal xor_ab[n] <== XorArray(n)(a, b); 67 | signal xor_abc[n] <== XorArray(n)(xor_ab, c); 68 | signal xor_abcd[n] <== XorArray(n)(xor_abc, d); 69 | out <== XorArray(n)(xor_abcd, e); 70 | } 71 | 72 | // Array of XORs 73 | // 74 | // Reviewers: 75 | // Keyvan: OK 76 | // 77 | template XorArray(n) { 78 | signal input a[n]; 79 | signal input b[n]; 80 | signal output out[n]; 81 | 82 | for (var i = 0; i < n; i++) { 83 | out[i] <== XOR()(a[i], b[i]); 84 | } 85 | } 86 | 87 | // Array of NOTs 88 | // 89 | // Reviewers: 90 | // Keyvan: OK 91 | // 92 | template NotArray(n) { 93 | signal input a[n]; 94 | signal output out[n]; 95 | for (var i = 0; i < n; i++) { 96 | out[i] <== 1 - a[i]; 97 | } 98 | } 99 | 100 | // Array of ORs 101 | // 102 | // Reviewers: 103 | // Keyvan: OK 104 | // 105 | template OrArray(n) { 106 | signal input a[n]; 107 | signal input b[n]; 108 | signal output out[n]; 109 | 110 | for (var i = 0; i < n; i++) { 111 | out[i] <== OR()(a[i], b[i]); 112 | } 113 | } 114 | 115 | // Array of ANDs 116 | // 117 | // Reviewers: 118 | // Keyvan: OK 119 | // 120 | template AndArray(n) { 121 | signal input a[n]; 122 | signal input b[n]; 123 | signal output out[n]; 124 | 125 | for (var i = 0; i < n; i++) { 126 | out[i] <== AND()(a[i], b[i]); 127 | } 128 | } 129 | 130 | // d = b ^ (a << 1 | a >> 63) 131 | // 132 | // Reviewers: 133 | // Keyvan: OK 134 | // 135 | template D() { 136 | signal input a[64]; 137 | signal input b[64]; 138 | signal output out[64]; 139 | 140 | signal aux0[64] <== ShL(64, 1)(a); 141 | signal aux1[64] <== ShR(64, 63)(a); 142 | signal aux2[64] <== OrArray(64)(aux0, aux1); 143 | out <== XorArray(64)(b, aux2); 144 | } 145 | 146 | // Theta 147 | // 148 | // Reviewers: 149 | // Keyvan: OK 150 | // 151 | template Theta() { 152 | signal input in[25][64]; 153 | signal output out[25][64]; 154 | 155 | signal c[5][64]; 156 | for(var i = 0; i < 5; i++) { 157 | c[i] <== Xor5(64)(in[i], in[5 + i], in[10 + i], in[15 + i], in[20 + i]); 158 | } 159 | 160 | signal d[5][64]; 161 | for(var i = 0; i < 5; i++) { 162 | d[i] <== D()(c[(i + 1) % 5], c[(i + 4) % 5]); 163 | } 164 | 165 | for(var i = 0; i < 5; i++) { 166 | for(var j = 0; j < 5; j++) { 167 | out[i + j * 5] <== XorArray(64)(in[i + j * 5], d[i]); 168 | } 169 | } 170 | } 171 | 172 | // out = a << shl | a >> shr 173 | // 174 | // Reviewers: 175 | // Keyvan: OK 176 | // 177 | template stepRhoPi(shl, shr) { 178 | signal input a[64]; 179 | signal output out[64]; 180 | 181 | signal aux0[64] <== ShR(64, shr)(a); 182 | signal aux1[64] <== ShL(64, shl)(a); 183 | out <== OrArray(64)(aux0, aux1); 184 | } 185 | 186 | // RhoPi 187 | // 188 | // Reviewers: 189 | // Keyvan: OK 190 | // 191 | template RhoPi() { 192 | signal input in[25][64]; 193 | signal output out[25][64]; 194 | 195 | var rot[25] = [1, 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1]; 196 | 197 | out[0] <== in[0]; 198 | for(var i = 0; i < 24; i++) { 199 | // 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, ... 200 | var shl = ((i + 1) * (i + 2) \ 2) % 64; 201 | 202 | out[rot[i + 1]] <== stepRhoPi(shl, 64 - shl)(in[rot[i]]); 203 | } 204 | } 205 | 206 | 207 | // out = a ^ (^b) & c 208 | // 209 | // Reviewers: 210 | // Keyvan: OK 211 | // 212 | template stepChi() { 213 | signal input a[64]; 214 | signal input b[64]; 215 | signal input c[64]; 216 | signal output out[64]; 217 | 218 | signal bXor[64] <== NotArray(64)(b); // ^b 219 | signal bc[64] <== AndArray(64)(bXor, c); // (^b)&c 220 | out <== XorArray(64)(a, bc); // a^(^b)&c 221 | } 222 | 223 | // Chi 224 | // 225 | // Reviewers: 226 | // Keyvan: OK 227 | // 228 | template Chi() { 229 | signal input in[25][64]; 230 | signal output out[25][64]; 231 | 232 | for(var i = 0; i < 25; i++) { 233 | if(i % 5 == 3) { 234 | out[i] <== stepChi()(in[i], in[i + 1], in[i - 3]); 235 | } else if(i % 5 == 4) { 236 | out[i] <== stepChi()(in[i], in[i - 4], in[i - 3]); 237 | } else { 238 | out[i] <== stepChi()(in[i], in[i + 1], in[i + 2]); 239 | } 240 | } 241 | } 242 | 243 | // Round constants 244 | // 245 | // Reviewers: 246 | // Keyvan: OK 247 | // 248 | template RoundConstants(r) { 249 | signal output out[64]; 250 | 251 | assert(r < 24); 252 | // 24 * (8 byte = 64-bit) numbers 253 | var rc[24] = [ 254 | 0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 255 | 0x8000000080008000, 0x000000000000808B, 0x0000000080000001, 256 | 0x8000000080008081, 0x8000000000008009, 0x000000000000008A, 257 | 0x0000000000000088, 0x0000000080008009, 0x000000008000000A, 258 | 0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 259 | 0x8000000000008003, 0x8000000000008002, 0x8000000000000080, 260 | 0x000000000000800A, 0x800000008000000A, 0x8000000080008081, 261 | 0x8000000000008080, 0x0000000080000001, 0x8000000080008008 262 | ]; 263 | for (var i = 0; i < 64; i++) { 264 | out[i] <== (rc[r] >> i) & 1; 265 | } 266 | } 267 | 268 | // Iota 269 | // 270 | // Reviewers: 271 | // Keyvan: OK 272 | // 273 | template Iota(r) { 274 | signal input in[25][64]; 275 | signal output out[25][64]; 276 | 277 | signal roundConstants[64] <== RoundConstants(r)(); 278 | 279 | out[0] <== XorArray(64)(in[0], roundConstants); 280 | for (var i = 1; i < 25; i++) { 281 | out[i] <== in[i]; 282 | } 283 | } 284 | 285 | // Apply Theta -> Rhopi -> Chi -> Iota 286 | // 287 | // Reviewers: 288 | // Keyvan: OK 289 | // 290 | template KeccakfRound(r) { 291 | signal input in[25][64]; 292 | signal output out[25][64]; 293 | signal theta[25][64] <== Theta()(in); 294 | signal rhopi[25][64] <== RhoPi()(theta); 295 | signal chi[25][64] <== Chi()(rhopi); 296 | out <== Iota(r)(chi); 297 | } 298 | 299 | // Absorb phase 300 | // 301 | // Reviewers: 302 | // Keyvan: OK 303 | // 304 | template Absorb() { 305 | var blockSizeBytes = 136; 306 | var blockSize64BitChunks = blockSizeBytes / 8; // 17 307 | 308 | signal input s[25][64]; 309 | signal input block[blockSize64BitChunks][64]; 310 | signal output out[25][64]; 311 | 312 | signal aux[25][64]; 313 | 314 | for (var i = 0; i < 25; i++) { 315 | if(i < blockSize64BitChunks) { 316 | aux[i] <== XorArray(64)(s[i], block[i]); 317 | } else { 318 | aux[i] <== s[i]; 319 | } 320 | } 321 | 322 | out <== Keccakf()(aux); 323 | } 324 | 325 | // Final phase 326 | // 327 | // Reviewers: 328 | // Keyvan: OK 329 | // 330 | template Final(nBlocksIn) { 331 | signal input in[nBlocksIn][17][64]; 332 | signal input blocks; 333 | signal output out[25][64]; 334 | var blockSize = 136 * 8; 335 | 336 | signal s[nBlocksIn + 1][25][64]; 337 | for(var i = 0; i < 25; i++) { 338 | for(var j = 0; j < 64; j++) { 339 | s[0][i][j] <== 0; 340 | } 341 | } 342 | 343 | for (var b = 0; b < nBlocksIn; b++) { 344 | s[b + 1] <== Absorb()(s[b], in[b]); 345 | } 346 | 347 | // Return the state after applying `blocks` absorbs: s[blocks] 348 | out <== SelectorArray2D(nBlocksIn + 1, 25, 64)(s, blocks); 349 | } 350 | 351 | // Apply 24 rounds of KeccakfRound 352 | // 353 | // Reviewers: 354 | // Keyvan: OK 355 | // 356 | template Keccakf() { 357 | signal input in[25][64]; 358 | signal output out[25][64]; 359 | 360 | signal midRound[25][25][64]; 361 | midRound[0] <== in; 362 | for (var i = 0; i < 24; i++) { 363 | midRound[i + 1] <== KeccakfRound(i)(midRound[i]); 364 | } 365 | 366 | out <== midRound[24]; 367 | } 368 | 369 | // Keccak of prepared input 370 | // 371 | // Reviewers: 372 | // Keyvan: OK 373 | // 374 | template Keccak(nBlocksIn) { 375 | signal input in[nBlocksIn][17][64]; 376 | signal input blocks; 377 | signal output out[32 * 8]; 378 | 379 | signal finalState[25][64] <== Final(nBlocksIn)(in, blocks); 380 | 381 | // Squeeze 382 | for(var i = 0; i < 32 * 8; i++) { 383 | out[i] <== finalState[i \ 64][i % 64]; 384 | } 385 | } 386 | 387 | // Pads the last block of theinput with 1000...0001 according to the number 388 | // of blocks needed 389 | // 390 | // Example (maxBlocks: 3, blockSize: 4): 391 | // in: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 392 | // 393 | // numBlocks = (inLen / blockSize) + 1 394 | // 395 | // inLen: 0 out: [0x01, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0] numBlocks: 1 396 | // inLen: 1 out: [1, 0x01, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0] numBlocks: 1 397 | // inLen: 2 out: [1, 2, 0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0] numBlocks: 1 398 | // inLen: 3 out: [1, 2, 3, 0x81, 0, 0, 0, 0, 0, 0, 0, 0] numBlocks: 1 399 | // inLen: 4 out: [1, 2, 3, 4, 0x01, 0, 0, 0x80, 0, 0, 0, 0] numBlocks: 2 400 | // inLen: 5 out: [1, 2, 3, 4, 5, 0x01, 0, 0x80, 0, 0, 0, 0] numBlocks: 2 401 | // inLen: 6 out: [1, 2, 3, 4, 5, 6, 0x01, 0x80, 0, 0, 0, 0] numBlocks: 2 402 | // inLen: 7 out: [1, 2, 3, 4, 5, 6, 7, 0x81, 0, 0, 0, 0] numBlocks: 2 403 | // inLen: 8 out: [1, 2, 3, 4, 5, 6, 7, 8, 0x01, 0, 0, 0x80] numBlocks: 3 404 | // inLen: 9 out: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0x01, 0, 0x80] numBlocks: 3 405 | // inLen: 10 out: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0x01, 0x80] numBlocks: 3 406 | // inLen: 11 out: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0x81] numBlocks: 3 407 | // inLen: 12 (Cannot generate proof, because (12 / blockSize + 1) > maxBlocks) 408 | // 409 | // Reviewers: 410 | // Keyvan: OK 411 | // 412 | template Pad(maxBlocks, blockSize) { 413 | var maxBytes = maxBlocks * blockSize; 414 | signal input in[maxBytes]; 415 | signal input inLen; 416 | 417 | signal output out[maxBytes]; 418 | signal output numBlocks; 419 | 420 | signal (div, rem) <== Divide(16)(inLen, blockSize); 421 | numBlocks <== div + 1; 422 | 423 | AssertLessEqThan(16)(numBlocks, maxBlocks); 424 | 425 | // Create a 1, 1, ..., 1, 1, 0, 0, ..., 0, 0 filter 426 | // Where the first `inLen` elements are 1 (Excluding filter[0] which is always 1) 427 | signal filter[maxBytes + 1]; 428 | filter[0] <== 1; 429 | signal isEq[maxBytes]; 430 | for(var i = 0; i < maxBytes; i++) { 431 | isEq[i] <== IsEqual()([i, inLen]); 432 | filter[i + 1] <== filter[i] * (1 - isEq[i]); 433 | } 434 | 435 | signal isLast[maxBytes]; 436 | for(var i = 0; i < maxBytes; i++) { 437 | isLast[i] <== IsEqual()([i, numBlocks * blockSize - 1]); 438 | 439 | // Due to the filter, only the first `inLen` bytes are kept 440 | // +0x01 if we are on the last bit of data 441 | // +0x80 if we are on the last bit of last block 442 | // +0x81 when both 443 | // Effectively adding a 1000..0001 postfix to the data 444 | out[i] <== in[i] * filter[i + 1] + 0x01 * isEq[i] + 0x80 * isLast[i]; 445 | } 446 | } 447 | 448 | // Keccak of arbitrary number of bytes. 449 | // Padding is done automatically and only required number of blocks are used. 450 | // 451 | // Reviewers: 452 | // Keyvan: OK 453 | // 454 | template KeccakBytes(maxBlocks) { 455 | signal input in[maxBlocks * 136]; 456 | signal input inLen; 457 | signal output out[32]; 458 | 459 | // Give some space for at least a single-byte padding (0x81 == 0b10000001) 460 | AssertLessThan(16)(inLen, maxBlocks * 136); 461 | 462 | // Add 1000...0001 padding to the input bytes 463 | signal ( 464 | padded[maxBlocks * 136], numBlocks 465 | ) <== Pad(maxBlocks, 136)(in, inLen); 466 | 467 | signal inBitsArray[maxBlocks * 136][8]; 468 | for(var i = 0; i < maxBlocks * 136; i++) { 469 | // This also checks if all input elements are 8-bit values 470 | inBitsArray[i] <== Num2Bits(8)(padded[i]); 471 | } 472 | signal inBits[maxBlocks * 136 * 8] <== Flatten(maxBlocks * 136, 8)(inBitsArray); 473 | 474 | // Put the bits in blocks of 17x64-bit arrays 475 | signal inBlocks[maxBlocks][17][64]; 476 | for(var i = 0; i < maxBlocks; i++) { 477 | for(var j = 0; j < 17; j++) { 478 | for(var k = 0; k < 64; k++) { 479 | inBlocks[i][j][k] <== inBits[i * 17 * 64 + j * 64 + k]; 480 | } 481 | } 482 | } 483 | 484 | signal outBits[256] <== Keccak(maxBlocks)(inBlocks, numBlocks); 485 | signal outBytes[32][8] <== Reshape(32, 8)(outBits); 486 | for(var i = 0; i < 32; i++) { 487 | out[i] <== Bits2Num(8)(outBytes[i]); 488 | } 489 | } --------------------------------------------------------------------------------