-
Notifications
You must be signed in to change notification settings - Fork 23
feat: Endorsement Certificates #443
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
Changes from all commits
6be6998
374acc3
5a4ec65
8ce3807
ea1c8f8
b2538d1
174a7e8
0959768
8fdb6bf
67af7c0
a34808c
953f4c8
c4ea7c7
e5236ad
ebebd81
344d9d2
c81971a
7c784b7
cdcf6d4
7c58dd8
6874d4a
77a4524
1318554
f1a66ef
72047d7
57c44a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import type { TypedDataToPrimitiveTypes } from 'abitype' | ||
| import type { Account, Address, Chain, Client, Hex, Transport } from 'viem' | ||
| import { bytesToBigInt, bytesToHex, concat, hexToBytes, numberToHex, recoverTypedDataAddress } from 'viem' | ||
| import { signTypedData } from 'viem/actions' | ||
| import { randU256 } from '../utils/rand.ts' | ||
|
|
||
| export type Endorsement = { | ||
| /** | ||
| * Unique nonce to suport nonce based revocation. | ||
| */ | ||
| nonce: bigint | ||
| /** | ||
| * This certificate becomes invalid after `notAfter` timestamp. | ||
| */ | ||
| notAfter: bigint | ||
| } | ||
|
|
||
| export type SignedEndorsement = Endorsement & { | ||
| signature: Hex | ||
| } | ||
|
|
||
| export const EIP712Endorsement = { | ||
| Endorsement: [ | ||
| { name: 'nonce', type: 'uint64' }, | ||
| { name: 'notAfter', type: 'uint64' }, | ||
| { name: 'providerId', type: 'uint256' }, | ||
| ], | ||
| } as const | ||
|
|
||
| export type TypedEn = TypedDataToPrimitiveTypes<typeof EIP712Endorsement>['Endorsement'] | ||
|
|
||
| export type SignCertOptions = { | ||
| nonce?: bigint // uint64 | ||
| notAfter: bigint // uint64 | ||
| providerId: bigint | ||
| } | ||
|
|
||
| /** | ||
| * Signs an endorsement certificate for a specific provider | ||
| * @param client - The client to use to sign the message | ||
| * @param options - nonce (randomised if null), not after and who to sign it for | ||
| * @returns encoded certificate data abiEncodePacked([nonce, notAfter, signature]), the provider id is implicit by where it will get placed in registry. | ||
| */ | ||
| export async function signEndorsement(client: Client<Transport, Chain, Account>, options: SignCertOptions) { | ||
| const nonce = (options.nonce ?? randU256()) & 0xffffffffffffffffn | ||
| const signature = await signTypedData(client, { | ||
| account: client.account, | ||
| domain: { | ||
| name: 'Storage Endorsement', | ||
| version: '1', | ||
| chainId: client.chain.id, | ||
| }, | ||
| types: EIP712Endorsement, | ||
| primaryType: 'Endorsement', | ||
| message: { | ||
| nonce: nonce, | ||
| notAfter: options.notAfter, | ||
| providerId: options.providerId, | ||
| }, | ||
| }) | ||
|
|
||
| const encodedNonce = numberToHex(nonce, { size: 8 }) | ||
| const encodedNotAfter = numberToHex(options.notAfter, { size: 8 }) | ||
|
|
||
| return concat([encodedNonce, encodedNotAfter, signature]) | ||
| } | ||
|
|
||
| export async function decodeEndorsement( | ||
| providerId: bigint, | ||
| chainId: number | bigint, | ||
| hexData: Hex | ||
| ): Promise<{ | ||
| address: Address | null | ||
| endorsement: SignedEndorsement | ||
| }> { | ||
| if (hexData.length !== 164) { | ||
| return { | ||
| address: null, | ||
| endorsement: { | ||
| nonce: 0n, | ||
| notAfter: 0n, | ||
| signature: '0x', | ||
| }, | ||
| } | ||
| } | ||
|
Comment on lines
+76
to
+85
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not just throw err?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to reduce use of try/catch for expected situations because it can mask unexpected situations |
||
| const data = hexToBytes(hexData) | ||
| const endorsement: SignedEndorsement = { | ||
| nonce: bytesToBigInt(data.slice(0, 8)), | ||
| notAfter: bytesToBigInt(data.slice(8, 16)), | ||
| signature: bytesToHex(data.slice(16)), | ||
wjmelements marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| const address = await recoverTypedDataAddress({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked into why this was
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, that's nasty, but oh well |
||
| domain: { | ||
| name: 'Storage Endorsement', | ||
| version: '1', | ||
| chainId, | ||
| }, | ||
| types: EIP712Endorsement, | ||
| primaryType: 'Endorsement', | ||
| message: { | ||
| nonce: endorsement.nonce, | ||
| notAfter: endorsement.notAfter, | ||
| providerId: providerId, | ||
| }, | ||
| signature: endorsement.signature, | ||
| }).catch(() => { | ||
| return null | ||
| }) | ||
| return { address, endorsement } | ||
| } | ||
|
|
||
| /** | ||
| * Validates endorsement capabilities, if any, filtering out invalid ones | ||
| * @returns mapping of valid endorsements to expiry, nonce, signature | ||
| */ | ||
| export async function decodeEndorsements( | ||
| providerId: bigint, | ||
| chainId: number | bigint, | ||
| capabilities: Record<string, Hex> | ||
| ): Promise<Record<Address, SignedEndorsement>> { | ||
| const now = Date.now() / 1000 | ||
| const result: Record<Address, SignedEndorsement> = {} | ||
|
|
||
| for (const hexData of Object.values(capabilities)) { | ||
| try { | ||
| const { address, endorsement } = await decodeEndorsement(providerId, chainId, hexData) | ||
| if (address && endorsement.notAfter > now) { | ||
| result[address] = endorsement | ||
| } | ||
| } catch { | ||
| // Skip invalid endorsements | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can remove this catch now
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would need to make sure that signatures that fail to recover don't throw. Should have a test case covering this in case that behavior changes.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 looks to me like a failure here would be of the fatal kind now since you're doing so much in |
||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| /** | ||
| * @returns a list of capability keys and a list of capability values for the ServiceProviderRegistry | ||
| */ | ||
| export function encodeEndorsements(endorsements: Record<Address, SignedEndorsement>): [string[], Hex[]] { | ||
| const keys: string[] = [] | ||
| const values: Hex[] = [] | ||
| Object.values(endorsements).forEach((value, index) => { | ||
| keys.push(`endorsement${index.toString()}`) | ||
| values.push( | ||
| concat([numberToHex(value.nonce, { size: 8 }), numberToHex(value.notAfter, { size: 8 }), value.signature]) | ||
| ) | ||
| }) | ||
| return [keys, values] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be a const