Skip to content

Commit

Permalink
Add Gas Benchmarking Tests for WebAuthn Signer (#324)
Browse files Browse the repository at this point in the history
This PR adds gas benchmarking tests for the WebAuthn signer. Note that
we got rid of the `MultipleVerifiers.spec.ts` file, as it is essentially
the new gas benchmarking tests, and there was no need to duplicate the
work.

The benchmarks look like this:
```
  Gas Benchmarking
    WebAuthnSigner
      ⛽ deployment: 612123
      ✔ Benchmark signer deployment cost (694ms)
      ⛽ verification (FreshCryptoLib): 219365
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (172ms)
      ⛽ verification (daimo-eth): 351273
      ✔ Benchmark signer verification cost with daimo-eth verifier (201ms)
      ⛽ verification (Dummy): 13835
      ✔ Benchmark signer verification cost with Dummy verifier
```

Note that we include a "dummy" benchmark, this is because gas
consumption of P-256 signature verification is unstable, and this allows
us to better compare gas characteristics of the `WebAuthn` signing
message computation overhead.

Furthermore, we remove the check on whether or not the P-256 verifier
has code. This is important as precompiles have no code, so our WebAuthn
stuff wouldn't work with precompiles without this change.
  • Loading branch information
nlordell authored Mar 19, 2024
1 parent 7468b32 commit 1e96cdc
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 82 deletions.
12 changes: 1 addition & 11 deletions modules/passkey/contracts/WebAuthnSignerFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ contract WebAuthnSignerFactory is ICustomECDSASignerFactory {
function createSigner(uint256 x, uint256 y, address verifier) external returns (address signer) {
signer = getSigner(x, y, verifier);

if (_hasNoCode(signer) && _validVerifier(verifier)) {
if (_hasNoCode(signer)) {
WebAuthnSigner created = new WebAuthnSigner{salt: bytes32(0)}(x, y, verifier);
require(address(created) == signer);
}
Expand All @@ -47,16 +47,6 @@ contract WebAuthnSignerFactory is ICustomECDSASignerFactory {
}
}

/**
* @dev Checks if the given verifier address contains code.
* @param verifier The address of the verifier to check.
* @return A boolean indicating whether the verifier contains code or not.
*/
function _validVerifier(address verifier) internal view returns (bool) {
// The verifier should contain code (The only way to implement a webauthn verifier is with a smart contract)
return !_hasNoCode(verifier);
}

/**
* @dev Checks if the provided account has no code.
* @param account The address of the account to check.
Expand Down
19 changes: 19 additions & 0 deletions modules/passkey/contracts/test/Benchmarker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.0;

contract Benchmarker {
function call(address to, bytes memory data) external returns (uint256 gas, bytes memory returnData) {
gas = gasleft();

bool success;
(success, returnData) = to.call(data);
if (!success) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
revert(add(returnData, 32), mload(returnData))
}
}

gas = gas - gasleft();
}
}
12 changes: 12 additions & 0 deletions modules/passkey/contracts/test/DummyP256Verifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: LGPL-3.0-only
/* solhint-disable no-complex-fallback */
/* solhint-disable payable-fallback */
pragma solidity ^0.8.0;

import {IP256Verifier} from "../interfaces/IP256Verifier.sol";

contract DummyP256Verifier is IP256Verifier {
fallback(bytes calldata) external returns (bytes memory output) {
output = abi.encode(true);
}
}
94 changes: 94 additions & 0 deletions modules/passkey/test/GasBenchmarking.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { expect } from 'chai'
import { deployments, ethers } from 'hardhat'

import { WebAuthnCredentials, decodePublicKey, encodeWebAuthnSignature } from './utils/webauthn'
import { IP256Verifier } from '../typechain-types'

describe('Gas Benchmarking', function () {
const navigator = {
credentials: new WebAuthnCredentials(),
}
const credential = navigator.credentials.create({
publicKey: {
rp: {
name: 'Safe',
id: 'safe.global',
},
user: {
id: ethers.getBytes(ethers.id('chucknorris')),
name: 'chucknorris',
displayName: 'Chuck Norris',
},
challenge: ethers.toBeArray(Date.now()),
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
},
})

const setupTests = deployments.createFixture(async ({ deployments }) => {
const { DaimoP256Verifier, FCLP256Verifier, WebAuthnSignerFactory } = await deployments.fixture()

const Benchmarker = await ethers.getContractFactory('Benchmarker')
const benchmarker = await Benchmarker.deploy()

const factory = await ethers.getContractAt('WebAuthnSignerFactory', WebAuthnSignerFactory.address)

const DummyP256Verifier = await ethers.getContractFactory('DummyP256Verifier')
const verifiers = {
fcl: await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address),
daimo: await ethers.getContractAt('IP256Verifier', DaimoP256Verifier.address),
dummy: await DummyP256Verifier.deploy(),
} as Record<string, IP256Verifier>

return { benchmarker, factory, verifiers }
})

describe('WebAuthnSigner', () => {
it(`Benchmark signer deployment cost`, async function () {
const { benchmarker, factory } = await setupTests()

const { x, y } = decodePublicKey(credential.response)
const verifier = `0x${'ee'.repeat(20)}`

const [gas] = await benchmarker.call.staticCall(factory, factory.interface.encodeFunctionData('createSigner', [x, y, verifier]))

console.log(` ⛽ deployment: ${gas}`)
})

for (const [name, key] of [
['FreshCryptoLib', 'fcl'],
['daimo-eth', 'daimo'],
['Dummy', 'dummy'],
]) {
it(`Benchmark signer verification cost with ${name} verifier`, async function () {
const { benchmarker, verifiers, factory } = await setupTests()

const challenge = ethers.id('hello world')
const assertion = navigator.credentials.get({
publicKey: {
challenge: ethers.getBytes(challenge),
rpId: 'safe.global',
allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }],
userVerification: 'required',
},
})

const { x, y } = decodePublicKey(credential.response)
const verifier = verifiers[key]

await factory.createSigner(x, y, verifier)
const signer = await ethers.getContractAt('WebAuthnSigner', await factory.getSigner(x, y, verifier))
const signature = encodeWebAuthnSignature(assertion.response)

const [gas, returnData] = await benchmarker.call.staticCall(
signer,
signer.interface.encodeFunctionData('isValidSignature(bytes32,bytes)', [challenge, signature]),
)

const [magicValue] = ethers.AbiCoder.defaultAbiCoder().decode(['bytes4'], returnData)
expect(magicValue).to.equal('0x1626ba7e')

console.log(` ⛽ verification (${name}): ${gas}`)
})
}
})
})
69 changes: 0 additions & 69 deletions modules/passkey/test/MultipleVerifiers.spec.ts

This file was deleted.

4 changes: 2 additions & 2 deletions modules/passkey/test/webauthn/WebAuthnShim.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('WebAuthn Shim', () => {
}

describe('navigator.credentials.create()', () => {
it('creates and verifies a new credential', async () => {
it('Should create and verify a new credential', async () => {
const options = await generateRegistrationOptions({
rpName: rp.name,
rpID: rp.id,
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('WebAuthn Shim', () => {
})

describe('navigator.credentials.get()', () => {
it('authorises and verifies an existing credential', async () => {
it('Should authorise and verify an existing credential', async () => {
const credential = navigator.credentials.create({
publicKey: {
rp,
Expand Down

0 comments on commit 1e96cdc

Please sign in to comment.