diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6f7ebae..979a461 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -45,8 +45,6 @@ jobs: - name: Run Forge build run: | forge install - forge --version - forge build --sizes id: build - name: Run Forge tests diff --git a/README.md b/README.md index 4df53b8..4269c9c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@ -# Add signers to a safe and keep them secret +# 🥷🏽 Dark Safe 🏦 + +## Deep Dive Video 📺 + +
+ +
-Project Structure +Repo Structure ```plaintext ├── circuits # contains the noir code @@ -18,17 +32,92 @@
-## Setup +## What + +A zodiac-compatible [Safe](https://app.safe.global) module that shields the ethereum addresses of up to 8 authorized "dark signers". + +## Background + +The idea for this project started back at devcon 6 - where I saw Noir had efficient keccak256 and secp256k1 signature verification circuits. + +I spent the plane ride home wondering if Noir could enable Gnosis Safes with a shielded set of signers, removing the attack vector of publically stored signer addresses. + +This would allow for anonymous onchain orgs, where signers could coordinate to execute transactions. + +### Problem + +Using this project as a research playground, I wanted to find an _~ elegant ~_ data structure that represented **_the set of valid sigers_** and **_the signing threshold_** in a **single hash**. + +- I knew I did not want to store all the leaves of some `n` depth merkle tree, nor do hash path and leaf index computation off-chain. Also... Merkle trees are boring 🚫🌳 + +### Solution + +Thanks to some great help from @autoparallel and @0x_jepsen, I ended up representing valid signer sets (including signing threshold) into a polynomial. + +This polynomial is [emitted in an event onchain](contracts/DarkSafe.sol#L48) as a _reverse_ encoded array, of 32 byte coefficiencts, with the array index representing the degree of the `x` value's exponent. For example: + +```plaintext +# Polynomial in array form: index represents exponent degree. +[42, 1, 9, 145, 1] + +# Polynomial in standard algebraic form +Polynomial = x^4 + 145x^3 + 9x^2 + x + 42 + 👆🏼 👆🏼 👆🏼 +# (implied) (1x^4) (1x^1) (42x^0) +``` + +This polynomial represents all the valid combinations of the signers. + +Just like Safe, it protects against: + +- double signing +- under signing +- non-signer signatures + +## A TLDR Of How It works + +### 👷🏽‍♂️ Setup + +1. An admin selects up to 8 Ethereum EOA `addresses` as signers on the safe and a signing `threshold` + - (Note: to prevent brute-force attacks decoding who the signers are, add at least **one** fresh EOA as a signer). +2. [`K choose N`](scripts/build.ts#L53) over the signer set and the threshold to find all possible _additive_ combinations of the Ethereum addresses (remember, an eth address is just a number, so you can use addition to express a combination of multiple addresses). +3. Consider those combinations as ["roots"](scripts/build.ts#L60) and [Lagrange Interpolate](scripts/build.ts#L68) a polynomial that passes through all those points where `y=0`. +4. Take the [Pedersen hash](scripts/build.ts#L137) of the polynomial as a suscinct commitment to the polynomial. +5. Deploy a new instance of [DarkSafe](contracts/DarkSafe.sol#L31), via the [ModuleProxyFactory](lib/zodiac/contracts/factory/ModuleProxyFactory.sol#L40) passing the `polynomialHash` and the `polynomial` as initialization params. + - The contract will emit the `polynomial` and `polynomialHash` in a `SignersRotated()` event as decentralized, succinct data stucture to represent the signer set and threshold. + +### ✍️ Signing + +1. Sign over a [SafeMessageHash](lib/safe-contracts/contracts/Safe.sol#L427) with an EOA Private Key (via `eth_personalSign`). +2. Pass your signature to the next signer. +3. Repeat steps 1+2 until a valid signer until a valid proof can be generated via the Noir circuit. + - This keeps other signers anonymous on a "need-to-know" basis. In other words, not all signers need to be known at proof generation. +4. Have some relayer call the [\_execute](contracts/DarkSafe.sol#L55) function, passing only the safe TX data, and the valid noir proof. +5. 🍃 Transaction is executed 🍃 + +## See it in action ```bash -yarn && yarn build +yarn && yarn build --debug ``` ## Run tests + ```bash yarn && yarn build cd circuits/ && nargo prove forge test -``` \ No newline at end of file +``` + +## Notes + +- This project is just for fun, demonstrating a relatively efficient and elegant usecase for Noir and shouldn't be used in production unless we work together on this and get it audited + +- Interpolating a polynomial over the K choose N of the signer set is _not_ secure enough for me to be comfortable. It is not impossible to brute force k choose n up to 8 over all the Ethereum addresses and compute f(x) to try and brute-force find out who's on the safe. + +Some possible solutions are: + +- Always spin up a fresh EOA to add as a signer, it's important this account has never made an Ethereum transaction on any chain. +- Refactor the code to accept a bit of randomness: an `r` value to hash together with each `root`. This makes it impossible to brute force. The `r` value can be as simple as a known `password` has to at least be known by the prover. diff --git a/contracts/test/E2E.t copy.sol b/contracts/test/E2E.t copy.sol deleted file mode 100644 index c4a568e..0000000 --- a/contracts/test/E2E.t copy.sol +++ /dev/null @@ -1,49 +0,0 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity 0.8.27; - -// import "safe-tools/SafeTestTools.sol"; -// import "forge-std/Test.sol"; - -// import {DeployScript} from "../script/Deploy.s.sol"; -// import {DarkSafeVerifier} from "../Verifier.sol"; -// import {DarkSafe} from "../DarkSafe.sol"; -// import {DarkSafeFactory} from "../utils/DarkSafeFactory.sol"; - -// contract DarkSafeTest is Test, SafeTestTools { -// using SafeTestLib for SafeInstance; - -// address private alice = address(0xA11c3); -// DarkSafeFactory private darkSafeFactory; -// DarkSafeVerifier private verifier; - -// function setUp() public { -// (darkSafeFactory, verifier) = (new DeployScript()).run(); -// } - -// function test() public { -// uint256[] memory ownerPKs = new uint256[](1); -// ownerPKs[0] = 12345; - -// SafeInstance memory safeInstance = _setupSafe({ -// ownerPKs: ownerPKs, -// threshold: 1, -// initialBalance: 1 ether, -// advancedParams: AdvancedSafeInitParams({ -// includeFallbackHandler: true, -// initData: "", -// saltNonce: 0, -// setupModulesCall_to: address(0), -// setupModulesCall_data: "", -// refundAmount: 0, -// refundToken: address(0), -// refundReceiver: payable(address(0)) -// }) -// }); - -// DarkSafe darkSafeMo - -// safeInstance.execTransaction({to: alice, value: 0.5 ether, data: ""}); - -// assertEq(alice.balance, 0.5 ether); -// } -// } diff --git a/contracts/test/E2E.t.sol b/contracts/test/E2E.t.sol index 1c788e8..16eefc9 100644 --- a/contracts/test/E2E.t.sol +++ b/contracts/test/E2E.t.sol @@ -71,7 +71,7 @@ contract DarkSafeTest is Test, SafeTestTools { ); // expect the signers rotated event to be emitted - vm.expectEmit(true, true, true, true); + vm.expectEmit(); emit DarkSafe.SignersRotated(polynomialInput.polynomial_hash, polynomialInput.polynomial); // deploy a new module proxy off the master copy diff --git a/scripts/utils/inquirer.ts b/scripts/utils/inquirer.ts index b16a795..8cca8ed 100644 --- a/scripts/utils/inquirer.ts +++ b/scripts/utils/inquirer.ts @@ -8,6 +8,7 @@ function parseCliArgs() { options: { signers: { type: "string" }, threshold: { type: "string" }, + debug: { type: "boolean" }, }, });