Skip to content

Commit f71db8f

Browse files
Add hashDelegation function to @metamask/delegation-core (#13)
* Add function to core to generate delegation hash, add tests. * Consume implementation from delegation-core for hashing caveats, and related constants. * Correctly encode caveat terms before hashing. Updated expected hashes validated against current version of @metamask/delegation-toolkit * Rename getDelegationHash to hashDelegation, update readme * Minor improvements - Updated description of ANY_BENEFICIARY to be more descriptive - Fixed incorrect definition of DELEGATION_TYPEHASH, which excluded the caveat suffix Co-authored-by: Hanzel Anchia Mena <33629234+hanzel98@users.noreply.github.com> --------- Co-authored-by: Hanzel Anchia Mena <33629234+hanzel98@users.noreply.github.com>
1 parent 2f013d6 commit f71db8f

14 files changed

Lines changed: 777 additions & 98 deletions

File tree

packages/delegation-core/README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,23 +240,48 @@ import { ROOT_AUTHORITY } from '@metamask/delegation-core';
240240
console.log(ROOT_AUTHORITY); // Root authority identifier
241241
```
242242

243+
#### `hashDelegation(delegation)`
244+
245+
Computes a hash for a given delegation object.
246+
247+
**Parameters:**
248+
- `delegation: DelegationStruct` - The delegation object to hash
249+
250+
**Returns:** `Hex` - The hash of the delegation
251+
252+
**Example:**
253+
```typescript
254+
import { hashDelegation } from '@metamask/delegation-core';
255+
256+
const delegation = {
257+
delegate: '0x742d35Cc6634C0532925a3b8d40EC49b0e8BaA5e',
258+
delegator: '0x...',
259+
authority: '0x...',
260+
caveats: [],
261+
salt: 0n,
262+
signature: '0x...'
263+
};
264+
265+
const hash = hashDelegation(delegation);
266+
```
267+
243268
## Type Definitions
244269

245270
### Core Types
246271

247272
```typescript
248273
export type Hex = `0x${string}`;
249274

250-
export type Delegation = {
275+
export type DelegationStruct = {
251276
delegate: string;
252277
delegator: string;
253278
authority: string;
254-
caveats: Caveat[];
279+
caveats: CaveatStruct[];
255280
salt: bigint;
256281
signature: string;
257282
};
258283

259-
export type Caveat = {
284+
export type CaveatStruct = {
260285
enforcer: string;
261286
terms: string;
262287
};

packages/delegation-core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
},
7272
"dependencies": {
7373
"@metamask/abi-utils": "^3.0.0",
74-
"@metamask/utils": "^11.4.0"
74+
"@metamask/utils": "^11.4.0",
75+
"@noble/hashes": "^1.8.0"
7576
}
7677
}

packages/delegation-core/src/delegation.ts

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { encodeSingle, decodeSingle } from '@metamask/abi-utils';
2-
import { type BytesLike } from '@metamask/utils';
1+
import { encode, encodeSingle, decodeSingle } from '@metamask/abi-utils';
2+
import { hexToBytes, type BytesLike } from '@metamask/utils';
3+
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
34

45
import {
56
bytesLikeToBytes,
@@ -9,14 +10,35 @@ import {
910
type EncodingOptions,
1011
type ResultValue,
1112
} from './returns';
12-
import type { Delegation, Hex } from './types';
13+
import type { CaveatStruct, DelegationStruct, Hex } from './types';
14+
15+
/**
16+
* When designated as the delegate address in a delegation, this allows any beneficiary to redeem the delegation.
17+
*/
18+
export const ANY_BENEFICIARY = '0x0000000000000000000000000000000000000a11';
1319

1420
/**
1521
* To be used on a delegation as the root authority.
1622
*/
1723
export const ROOT_AUTHORITY =
1824
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
1925

26+
/**
27+
* The typehash for a delegation, used when generating a delegation hash.
28+
*
29+
* keccak('Delegation(address delegate,address delegator,bytes32 authority,Caveat[] caveats,uint256 salt)Caveat(address enforcer,bytes terms)')
30+
*/
31+
export const DELEGATION_TYPEHASH =
32+
'0x88c1d2ecf185adf710588203a5f263f0ff61be0d33da39792cde19ba9aa4331e';
33+
34+
/**
35+
* The typehash for a caveat, used when generating a caveat hash.
36+
*
37+
* keccak('Caveat(address enforcer,bytes terms)')
38+
*/
39+
export const CAVEAT_TYPEHASH =
40+
'0x80ad7e1b04ee6d994a125f4714ca0720908bd80ed16063ec8aee4b88e9253e2d';
41+
2042
/**
2143
* The ABI types for an array of delegations.
2244
*/
@@ -30,11 +52,11 @@ const DELEGATION_ARRAY_ABI_TYPES =
3052
* @returns The encoded delegations as a hex string (default) or Uint8Array.
3153
*/
3254
export function encodeDelegations(
33-
delegations: Delegation[],
55+
delegations: DelegationStruct[],
3456
options?: EncodingOptions<'hex'>,
3557
): Hex;
3658
export function encodeDelegations(
37-
delegations: Delegation[],
59+
delegations: DelegationStruct[],
3860
options: EncodingOptions<'bytes'>,
3961
): Uint8Array;
4062
/**
@@ -44,7 +66,7 @@ export function encodeDelegations(
4466
* @returns The encoded delegations as a hex string (default) or Uint8Array.
4567
*/
4668
export function encodeDelegations(
47-
delegations: Delegation[],
69+
delegations: DelegationStruct[],
4870
options: EncodingOptions<ResultValue> = defaultOptions,
4971
): Hex | Uint8Array {
5072
let result: Uint8Array;
@@ -83,7 +105,7 @@ export function encodeDelegations(
83105
const delegationFromDecodedDelegation = <TEncoding extends BytesLike>(
84106
decodedDelegation: DecodedDelegation,
85107
convertFn: (value: BytesLike) => TEncoding,
86-
): Delegation<TEncoding> => {
108+
): DelegationStruct<TEncoding> => {
87109
const [delegate, delegator, authority, caveats, salt, signature] =
88110
decodedDelegation;
89111

@@ -124,11 +146,11 @@ type DecodedDelegation = [
124146
export function decodeDelegations(
125147
encoded: BytesLike,
126148
options?: EncodingOptions<'hex'>,
127-
): Delegation<Hex>[];
149+
): DelegationStruct<Hex>[];
128150
export function decodeDelegations(
129151
encoded: BytesLike,
130152
options: EncodingOptions<'bytes'>,
131-
): Delegation<Uint8Array>[];
153+
): DelegationStruct<Uint8Array>[];
132154
/**
133155
* Decodes an encoded permission context back into an array of delegations.
134156
* @param encoded - The encoded delegations as a hex string or Uint8Array.
@@ -138,7 +160,7 @@ export function decodeDelegations(
138160
export function decodeDelegations(
139161
encoded: BytesLike,
140162
options: EncodingOptions<ResultValue> = defaultOptions,
141-
): Delegation<Hex>[] | Delegation<Uint8Array>[] {
163+
): DelegationStruct<Hex>[] | DelegationStruct<Uint8Array>[] {
142164
// it's possible to short circuit for empty delegations, but due to the
143165
// complexity of the input type, and the relative infrequency of empty delegations,
144166
// it's not worthwhile.
@@ -158,3 +180,91 @@ export function decodeDelegations(
158180
delegationFromDecodedDelegation(struct, bytesLikeToHex),
159181
);
160182
}
183+
184+
/**
185+
* Calculates the hash of a delegation for signing purposes.
186+
* The hash is computed by encoding the delegation parameters with the delegation typehash
187+
* and then applying keccak256 hashing.
188+
*
189+
* @param delegation - The delegation to hash.
190+
* @param options - Encoding options. Defaults to { out: 'hex' }.
191+
* @returns The keccak256 hash of the encoded delegation as a hex string (default) or Uint8Array.
192+
*/
193+
export function hashDelegation(
194+
delegation: DelegationStruct,
195+
options?: EncodingOptions<'hex'>,
196+
): Hex;
197+
export function hashDelegation(
198+
delegation: DelegationStruct,
199+
options: EncodingOptions<'bytes'>,
200+
): Uint8Array;
201+
/**
202+
* Calculates the hash of a delegation for signing purposes.
203+
* The hash is computed by encoding the delegation parameters with the delegation typehash
204+
* and then applying keccak256 hashing.
205+
*
206+
* @param delegation - The delegation to hash.
207+
* @param options - Encoding options. Defaults to { out: 'hex' }.
208+
* @returns The keccak256 hash of the encoded delegation as a hex string (default) or Uint8Array.
209+
*/
210+
export function hashDelegation(
211+
delegation: DelegationStruct,
212+
options: EncodingOptions<ResultValue> = defaultOptions,
213+
): Hex | Uint8Array {
214+
const encoded = encode(
215+
['bytes32', 'address', 'address', 'bytes32', 'bytes32', 'uint256'],
216+
[
217+
DELEGATION_TYPEHASH,
218+
delegation.delegate,
219+
delegation.delegator,
220+
delegation.authority,
221+
getCaveatsArrayHash(delegation.caveats),
222+
delegation.salt,
223+
],
224+
);
225+
const hash = keccak256(encoded);
226+
return prepareResult(hash, options);
227+
}
228+
229+
/**
230+
* Calculates the hash of an array of caveats. The caveats are individually abi
231+
* encoded and hashed, and concatenated. The resulting byte array is then
232+
* hashed to produce the CaveatsArrayHash.
233+
*
234+
* @param caveats - The array of caveats to hash.
235+
* @returns The keccak256 hash of the encoded caveat array.
236+
*/
237+
function getCaveatsArrayHash(caveats: CaveatStruct[]): Uint8Array {
238+
const byteLength = 32 * caveats.length;
239+
const encoded = new Uint8Array(byteLength);
240+
241+
for (let i = 0; i < caveats.length; i++) {
242+
const caveat = caveats[i];
243+
if (!caveat) {
244+
throw new Error(`Caveat was undefined at index ${i}`);
245+
}
246+
const caveatHash = getCaveatHash(caveat);
247+
encoded.set(caveatHash, i * 32);
248+
}
249+
250+
return keccak256(encoded);
251+
}
252+
253+
/**
254+
* Calculates the hash of a single caveat.
255+
* @param caveat - The caveat to hash.
256+
* @returns The keccak256 hash of the encoded caveat.
257+
*/
258+
function getCaveatHash(caveat: CaveatStruct): Uint8Array {
259+
const termsBytes =
260+
typeof caveat.terms === 'string' ? hexToBytes(caveat.terms) : caveat.terms;
261+
262+
const termsHash = keccak256(termsBytes);
263+
264+
const encoded = encode(
265+
['bytes32', 'address', 'bytes32'],
266+
[CAVEAT_TYPEHASH, caveat.enforcer, termsHash],
267+
);
268+
const hash = keccak256(encoded);
269+
return hash;
270+
}

packages/delegation-core/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export type { Hex, Delegation, Caveat } from './types';
1+
export type {
2+
Hex,
3+
DelegationStruct as Delegation,
4+
CaveatStruct as Caveat,
5+
} from './types';
26

37
export {
48
createValueLteTerms,
@@ -12,5 +16,9 @@ export {
1216
export {
1317
encodeDelegations,
1418
decodeDelegations,
19+
hashDelegation,
1520
ROOT_AUTHORITY,
21+
ANY_BENEFICIARY,
22+
DELEGATION_TYPEHASH,
23+
CAVEAT_TYPEHASH,
1624
} from './delegation';

packages/delegation-core/src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type { Hex } from '@metamask/utils';
99
* @property terms - The terms or conditions of the caveat encoded as hex data.
1010
* @property args - Additional arguments required by the caveat enforcer, encoded as hex data.
1111
*/
12-
export type Caveat<TBytes extends BytesLike = BytesLike> = {
12+
export type CaveatStruct<TBytes extends BytesLike = BytesLike> = {
1313
enforcer: TBytes;
1414
terms: TBytes;
1515
args: TBytes;
@@ -25,11 +25,11 @@ export type Caveat<TBytes extends BytesLike = BytesLike> = {
2525
* @property salt - A unique value to prevent replay attacks and ensure uniqueness of the delegation.
2626
* @property signature - The cryptographic signature validating this delegation.
2727
*/
28-
export type Delegation<TBytes extends BytesLike = BytesLike> = {
28+
export type DelegationStruct<TBytes extends BytesLike = BytesLike> = {
2929
delegate: TBytes;
3030
delegator: TBytes;
3131
authority: TBytes;
32-
caveats: Caveat<TBytes>[];
32+
caveats: CaveatStruct<TBytes>[];
3333
salt: bigint;
3434
signature: TBytes;
3535
};

0 commit comments

Comments
 (0)