Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize Base-64 Encoding Algorithm #289

Closed
nlordell opened this issue Feb 29, 2024 · 0 comments
Closed

Optimize Base-64 Encoding Algorithm #289

nlordell opened this issue Feb 29, 2024 · 0 comments
Assignees

Comments

@nlordell
Copy link
Collaborator

nlordell commented Feb 29, 2024

The WebAuthn signing process requires encoding the challenge to base64. The "challenge" is a relying party chosen value for replay protection. In the passkey design we use, this is the hash of the data we are verifying for, and assume replay protection already applies to it (for example, the nonce is included in the Safe transaction hash which gets verified and ensures replay protection).

The issue is that the challenge is computed as a bytes32 on-chain, but needs to be encoded in base64 for computing the clientDataJSON which is ultimately used in computing the "signing message" (i.e. what is actually signed by the private key).

Currently, the base64 implementation that is used in FCL and in the experimental contracts first:

  • Converts the bytes32 to a bytes memory array (thus incurring unnecessary allocations)
  • Operates on arbitrarily sized bytes memory arrays (thus adding code complexity and marginal runtime costs)
  • Returns a string memory which needs to be concatenated to an existing bytes memory (which incurs a copy loop - at least until mcopy is available - which has a non-negligible runtime and codesize cost).

The proposal is to instead compute the base64 incoding in-place with an algorithm specialized for 32 bytes of data, thus avoiding memory allocations and copies, and additional complexity for supporting arbitrary sized arrays.

The expected outcome is a specialized base64 encoding implementation for use in the WebAuthn signature validator contract.

@nlordell nlordell assigned nlordell and akshay-ap and unassigned nlordell Mar 4, 2024
nlordell added a commit that referenced this issue Mar 18, 2024
This PR refactors the WebAuthn implementation into a library instead of
split into multiple contracts. This removes the amount of `CALL`s
required for verifying a signature.

I did some analysis on the deployment vs. signature verification costs
comparing this code, `main` and this change incorporating optimizations
from #289 and found this:

| setup | deployment | verification (dummy) | break even |
| ------------------------------------ | ---------- |
-------------------- | ---------- |
| main (viaIR = false) | 373543 | 17789 | 0 |
| main (viaIR = true) | 393494 | 16443 | 15 |
| this branch (viaIR = false) | 612123 | 13835 | 61 |
| this branch (viaIR = true) | 594058 | 12551 | 43 |
| base64 optimizations (viaIR = false) | 442545 | 12965 | 15 |
| base64 optimizations (viaIR = true) | 414941 | 11102 | 7 |

Meaning that after 7 signatures, even with the slightly larger
deployment costs, we would break even by avoiding the extra `CALL`
(noting that ABI-encoding the call to the `WebAuthnVerifier` is not
particularly efficient in the first place).
@mmv08 mmv08 assigned mmv08 and unassigned akshay-ap Mar 21, 2024
mmv08 added a commit that referenced this issue Mar 27, 2024
This PR implements #289 

The algorithm is implemented as a library instead of the suggested
in-place adjustment from the original issue. Akshay's original
implementation was doing it in-place (which can still be found in the
commit history). While it was efficient code size-wise, but gas-wise, it
consumed more gas than FCL's implementation, probably due to some
inefficient stack manipulations. I tested it with Solady's
[library](https://github.com/vectorized/solady/blob/main/src/utils/Base64.sol)
then, and it turned out to be much more efficient even though it wasn't
doing the encoding in place.

The implementation I'm proposing is based on Solady's efficient
algorithm. It seems to me that it's not adjustable to in-place encoding
due to the fact the algorithm takes advantage of optimizing stack
operations by writing X bytes to the scratch space and then writing them
to the string at position Y+(I*N) (I=Iteration, Y=start, N=bytes written
in an iteration) as a 32-byte word. Because it writes them as a 32-byte
word, it’s not suitable for modifying a string in place because it will
overwrite the existing string bytes with empty characters. (I'm happy if
I'm wrong though)

**Changes in PR**:
- Add `codesize` task
- Add a `Base64Url.sol` library with a Base64Url encoding algorithm that
is optimized for encoding `bytes32` data.
- Add tests for the base64 library
- Edit the typescript configuration to include the hardhat.config.ts so
all the type extensions are picked up. Necessary to get the project
compiled.

### Code size change

This PR:
`WebAuthnSigner 2693 bytes (limit is 24576)`

Default main branch:
`WebAuthnSigner 2817 bytes (limit is 24576)`

### Gas Usage - Needs additional test for benchmarking

**Solady**:
```
  Gas Benchmarking
    WebAuthnSigner
      ⛽ deployment: 550077
      ✔ Benchmark signer deployment cost
      ⛽ verification (FreshCryptoLib): 216563
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (70ms)
      ⛽ verification (daimo-eth): 347246
      ✔ Benchmark signer verification cost with daimo-eth verifier (116ms)
      ⛽ verification (Dummy): 12713
      ✔ Benchmark signer verification cost with Dummy verifier
```

**FCL**:
```
  Gas Benchmarking
    WebAuthnSigner
      ⛽ deployment: 612929
      ✔ Benchmark signer deployment cost
      ⛽ verification (FreshCryptoLib): 217272
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (70ms)
      ⛽ verification (daimo-eth): 347636
      ✔ Benchmark signer verification cost with daimo-eth verifier (108ms)
      ⛽ verification (Dummy): 13844
      ✔ Benchmark signer verification cost with Dummy verifier
```

**This PR**:
```
  Gas Benchmarking
    WebAuthnSigner
      ⛽ deployment: 533456
      ✔ Benchmark signer deployment cost
      ⛽ verification (FreshCryptoLib): 210645
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (64ms)
      ⛽ verification (daimo-eth): 337941
      ✔ Benchmark signer verification cost with daimo-eth verifier (108ms)
      ⛽ verification (Dummy): 12281
      ✔ Benchmark signer verification cost with Dummy verifier
```

**This PR (viaIR)**:
```
  Gas Benchmarking
    WebAuthnSigner
      ⛽ deployment: 544844
      ✔ Benchmark signer deployment cost
      ⛽ verification (FreshCryptoLib): 294440
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (108ms)
      ⛽ verification (daimo-eth): 335453
      ✔ Benchmark signer verification cost with daimo-eth verifier (117ms)
      ⛽ verification (Dummy): 10352
      ✔ Benchmark signer verification cost with Dummy verifier
```

---------

Co-authored-by: Mikhail Mikheev <[email protected]>
Co-authored-by: Nicholas Rodrigues Lordello <[email protected]>
@mmv08 mmv08 closed this as completed Mar 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants