13 KiB
EXODUS - EXchange Of Digital Uniformed Signatures
Overview
This module optimizes a hot spot in the verification formula used for missing‑signer recovery. The target expression is:
k*P + l*Q + d*M
which requires three scalar multiplications and two point additions. Naive elliptic‑curve routines make this very gas‑expensive; this work reduces cost while maintaining correctness.
For full background and protocol details see the project wiki.
Goals
- Reduce gas for the targeted combination of scalar multiplications and additions.
- Keep implementation compact and auditable for on‑chain use.
- Maintain correctness and safety for cryptographic operations.
Design choices
- Use Projective coordinates (not Jacobian) to cut down on the number of
mulmod/addmodoperations where possible while retaining simple formulas for point addition and doubling. - Perform the final conversion to affine coordinates with an optimized
Extended Euclidean Algorithmimplemented in inline assembly to reduce gas compared with high‑level inversion routines. - Benchmark against the Jacobian implementation from the witnet elliptic‑curve‑solidity project as a reference.
Rationale
- Projective coordinates: fewer modular multiplications in the common path, making point operations cheaper on average.
- Assembly
Extended Euclidean Algorithmfor inversion: this algorithm in optimized inline assembly typically yields lower gas for single inversions compared with repeatedmulmodexponentiation or other higher‑level approaches. - Comparing to a well‑maintained Jacobian implementation gives a meaningful baseline for gas and correctness.
Gas Usage
Jacobian is the original implementation used as a reference implementation, while Projective is optimized one.
╭----------------------------------------+-----------------+---------+---------+---------+---------╮
| src/MathTester.sol:MathTester Contract | | | | | |
+==================================================================================================+
| Deployment Cost | Deployment Size | | | | |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| 3269040 | 14905 | | | | |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| | | | | | |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| Function Name | Min | Avg | Median | Max | # Calls |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| addGhost | 1450 | 1450 | 1450 | 1450 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| addJacobian | 1976 | 1976 | 1976 | 1976 | 88 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| addMixedGhost | 1084 | 1084 | 1084 | 1084 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| addMixedProjective | 1098 | 1098 | 1098 | 1098 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| addProjective | 1252 | 1252 | 1252 | 1252 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| doubleGhost | 699 | 699 | 699 | 699 | 45 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| doubleJacobian | 1101 | 1101 | 1101 | 1101 | 45 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| doubleProjective | 677 | 677 | 677 | 677 | 45 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| isOnCurve | 296 | 296 | 296 | 296 | 306 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| isOnCurveGhost | 824 | 824 | 824 | 824 | 306 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulPairEc | 103705 | 731500 | 1058710 | 1270392 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulPairGhost | 4185 | 174517 | 303073 | 330416 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulPairProjective | 5275 | 182401 | 327577 | 361377 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulQuartetEc | 308036 | 1561245 | 1982575 | 2672715 | 42 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulQuartetProjective | 12468 | 206689 | 346026 | 396570 | 42 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulSingleEc | 2093 | 327159 | 408874 | 600125 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulSingleGhost | 1775 | 109646 | 143531 | 212019 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulSingleProjective | 1916 | 114289 | 149838 | 220860 | 44 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulTripletEc | 205416 | 1140277 | 1546137 | 1958683 | 43 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| mulTripletProjective | 6636 | 199868 | 346618 | 397506 | 43 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| toAffineGhost | 11853 | 13845 | 13925 | 15527 | 176 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| toAffineJacobian | 40395 | 47860 | 47979 | 53983 | 133 |
|----------------------------------------+-----------------+---------+---------+---------+---------|
| toAffineProjective | 11165 | 13918 | 13900 | 16214 | 351 |
╰----------------------------------------+-----------------+---------+---------+---------+---------╯
╭-----------------------------------------------------+-----------------+--------+--------+--------+---------╮
| test/GhostVerifier.t.sol:GhostVerifierImpl Contract | | | | | |
+============================================================================================================+
| Deployment Cost | Deployment Size | | | | |
|-----------------------------------------------------+-----------------+--------+--------+--------+---------|
| 1687628 | 7589 | | | | |
|-----------------------------------------------------+-----------------+--------+--------+--------+---------|
| | | | | | |
|-----------------------------------------------------+-----------------+--------+--------+--------+---------|
| Function Name | Min | Avg | Median | Max | # Calls |
|-----------------------------------------------------+-----------------+--------+--------+--------+---------|
| setPubkey | 111516 | 111529 | 111528 | 111540 | 9 |
|-----------------------------------------------------+-----------------+--------+--------+--------+---------|
| verify | 538818 | 609541 | 578174 | 884038 | 9 |
╰-----------------------------------------------------+-----------------+--------+--------+--------+---------╯
The verify function consumes a maximum of 955,747 884,038 gas units when processing 1,000 signers with 300 missing signatures. Assuming an ETH price of $4,200 and gas cost of 2 gwei:
test_verify_5_of_5_works() (gas: 735686 = $6.18) - Baseline (100%)test_verify_67_of_100() (gas: 812318 = $6.82) - 10% increasetest_verify_700_of_1000() (gas: 1118143 = $9.39) - 51% increase- test_verify_5_of_5_works() (gas: 663441 = $5.57) - Baseline (100%)
- test_verify_67_of_100() (gas: 740823 = $6.22) - 11% increase
- test_verify_700_of_1000() (gas: 1046398 = $8.78) - 57% increase
The gas cost scales reasonably across different numbers of signers, making the solution practical for real-world use cases.
Compiler Inconsistency Note
We observed unexpected behavior in the Solidity compiler. Despite identical code in ECMathProjective and GhostEllipticCurves libraries, gas costs vary for critical elliptic curve operations:
doubleProjectiveandaddProjectiveshow inconsistent gas costs between implementationsmulSingleGhostunexpectedly consumes less gas thanmulSingleProjective
These variations occur even with clean compilation runs, suggesting compiler-level optimizations that behave unpredictably with identical code in different library contexts. The root cause remains unclear.
Implementation Details
Final implementation formula is:
s*G + sum_i^n (e*ai*(Pi) + b*R1m + d*R2m) = R + e*Hagg
s*G + sum_i^n (e*ai*(Pi)) + sum_i^n (b*R1m) + sum_i^n (b*R2m) = b*R1 + d*R2 + e*Hagg
s*G + e*(Hagg - sum_i^n Hi) = b*(R1 - sum_i^n R1m) + d*(R2 - sum_i^n R2m)
s*G + e*(Hagg - sum_i^n Hi) = b*R1e + d*R2e
where
R1 = R1e + R1m (existing nonce + missing nonce)
R2 = R2e + R2m
Hi = ai*Pi
Nonce Abuse
Our approach enables any participant to submit pairs of (R1e, R1m) and (R2e, R2m), which we use to reconstruct the original nonces R1 and R2. These reconstructed values are essential for computing b and d during the verification process, ultimately deriving the final R required for commitment e.
This design leverages circular dependencies and cryptographic hash functions to ensure verification integrity.
arbitrary data: s, msg, R1e, R1m, R2e, R2m
s*G + e*H` = b*R1e + d*R2e
s*G = b*R1e + d*R2e - e*H`
s*G = target_value (Discrete Log Problem)
However, the hash dependencies create a circular constraint similar to the "chicken and egg" problem:
b = sha256(R1e + R1m, R2e + R2m, H, msg)
d = sha256(msg, H, R2e + R2m, R1e + R1m)
e = sha256(b*(R1e+R1m) + d*(R2e+R2m), H, msg)
b*R1e + d*R2e - s*G = e*H`
For now it looks safe if sha256 or keccak256 hashing algorithm is used.
Missing Aggregation Abuse
We are proposing the to store not only final Hagg point but the hash of all signers (both coordinates for each) in the sequence that they were aggregated. Thus each verify call will have all list of Xi=ai*Pi with indexes of missing signers. Here we are relying on fact that sha256 and keccak256 do not have any known ability to find collisions and resistant to preimage attack.
We propose storing not only the final aggregated point Hagg but also the cryptographic hash of all signers' coordinates in their aggregation sequence. This approach ensures each verify call has access to the complete list of Xi = ai * Pi along with indexes of missing signers.
This design relies on the cryptographic security of sha256 and keccak256, both of which:
- Have no known practical collision attacks
- Maintain strong resistance to preimage attacks
- Provide robust security guarantees for our verification process
Precompile Abuse
The step by step hack is described here.
Contributing
All contributions are welcome — whether it's code, documentation, tests, performance benchmarks, or review. Please submit commits, issues, or pull requests; any help to improve correctness, security, or gas efficiency is greatly appreciated.