From 030b96bf8ab83bfa41338eec2b8c7a815951e145 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 10 Aug 2021 15:11:12 -0230 Subject: [PATCH 1/4] Add `eip712Hash` unit tests Unit tests have been added for `TypedDataUtils.eip712Hash`. These tests don't exhaustively check all possible inputs because there are a lot, and they're just passed along to `hashStruct`. So the `hashStruct` tests should catch any input-specific changes. Outside of behaviour internal to `hashStruct`, all of the behaviour should be covered by these tests. Any calls to `eip712Hash` in the older signature tests have been removed, as they are now redundant. --- src/__snapshots__/index.test.ts.snap | 20 ++ src/index.test.ts | 514 +++++++++++++++++++++++++-- 2 files changed, 502 insertions(+), 32 deletions(-) diff --git a/src/__snapshots__/index.test.ts.snap b/src/__snapshots__/index.test.ts.snap index 16898866..a8d6b850 100644 --- a/src/__snapshots__/index.test.ts.snap +++ b/src/__snapshots__/index.test.ts.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`TypedDataUtils.eip712Hash V3 should hash a minimal valid typed message 1`] = `"8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38"`; + +exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with a domain separator that uses all fields. 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; + +exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with data 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; + +exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with extra domain seperator fields 1`] = `"056edc00a07b3c2f8272146428402cc56419cea26ecdd743301b78053e5aba92"`; + +exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with only custom domain seperator fields 1`] = `"8e713ff4064ccd8d5cf047d72d1417ba966ed0a4df27251e36aae310414a59b6"`; + +exports[`TypedDataUtils.eip712Hash V4 should hash a minimal valid typed message 1`] = `"8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38"`; + +exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with a domain separator that uses all fields. 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; + +exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with data 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; + +exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with extra domain seperator fields 1`] = `"056edc00a07b3c2f8272146428402cc56419cea26ecdd743301b78053e5aba92"`; + +exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with only custom domain seperator fields 1`] = `"8e713ff4064ccd8d5cf047d72d1417ba966ed0a4df27251e36aae310414a59b6"`; + exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x0" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000000"`; exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"`; diff --git a/src/index.test.ts b/src/index.test.ts index ff316baa..8bea75a6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3060,6 +3060,488 @@ describe('TypedDataUtils.sanitizeData', function () { }); }); +describe('TypedDataUtils.eip712Hash', function () { + describe('V3', function () { + it('should hash a minimal valid typed message', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }, + 'V3', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('minimal typed message hash should be identical to minimal valid typed message hash', function () { + const minimalHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: {}, + primaryType: 'EIP712Domain', + } as any, + 'V3', + ); + const minimalValidHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }, + 'V3', + ); + + expect(minimalHash.toString('hex')).toBe( + minimalValidHash.toString('hex'), + ); + }); + + it('should ignore extra top-level properties', function () { + const minimalValidHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }, + 'V3', + ); + const extraPropertiesHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + extra: 'stuff', + moreExtra: 1, + } as any, + 'V3', + ); + + expect(minimalValidHash.toString('hex')).toBe( + extraPropertiesHash.toString('hex'), + ); + }); + + it('should hash a typed message with a domain separator that uses all fields.', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + }, + primaryType: 'EIP712Domain', + domain: { + name: 'example.metamask.io', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + message: {}, + }, + 'V3', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('should hash a typed message with extra domain seperator fields', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + { + name: 'extraField', + type: 'string', + }, + ], + }, + primaryType: 'EIP712Domain', + domain: { + name: 'example.metamask.io', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + extraField: 'stuff', + }, + message: {}, + } as any, + 'V3', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('should hash a typed message with only custom domain seperator fields', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'customName', + type: 'string', + }, + { + name: 'customVersion', + type: 'string', + }, + { + name: 'customChainId', + type: 'uint256', + }, + { + name: 'customVerifyingContract', + type: 'address', + }, + { + name: 'extraField', + type: 'string', + }, + ], + }, + primaryType: 'EIP712Domain', + domain: { + customName: 'example.metamask.io', + customVersion: '1', + customChainId: 1, + customVerifyingContract: + '0x0000000000000000000000000000000000000000', + extraField: 'stuff', + }, + message: {}, + } as any, + 'V3', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('should hash a typed message with data', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + Message: [{ name: 'data', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: { + name: 'example.metamask.io', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + message: { + data: 'Hello!', + }, + }, + 'V3', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + }); + + describe('V4', function () { + it('should hash a minimal valid typed message', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }, + 'V4', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('minimal typed message hash should be identical to minimal valid typed message hash', function () { + const minimalHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: {}, + primaryType: 'EIP712Domain', + } as any, + 'V4', + ); + const minimalValidHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }, + 'V4', + ); + + expect(minimalHash.toString('hex')).toBe( + minimalValidHash.toString('hex'), + ); + }); + + it('should ignore extra top-level properties', function () { + const minimalValidHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }, + 'V4', + ); + const extraPropertiesHash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + extra: 'stuff', + moreExtra: 1, + } as any, + 'V4', + ); + + expect(minimalValidHash.toString('hex')).toBe( + extraPropertiesHash.toString('hex'), + ); + }); + + it('should hash a typed message with a domain separator that uses all fields.', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + }, + primaryType: 'EIP712Domain', + domain: { + name: 'example.metamask.io', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + message: {}, + }, + 'V4', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('should hash a typed message with extra domain seperator fields', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + { + name: 'extraField', + type: 'string', + }, + ], + }, + primaryType: 'EIP712Domain', + domain: { + name: 'example.metamask.io', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + extraField: 'stuff', + }, + message: {}, + } as any, + 'V4', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('should hash a typed message with only custom domain seperator fields', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'customName', + type: 'string', + }, + { + name: 'customVersion', + type: 'string', + }, + { + name: 'customChainId', + type: 'uint256', + }, + { + name: 'customVerifyingContract', + type: 'address', + }, + { + name: 'extraField', + type: 'string', + }, + ], + }, + primaryType: 'EIP712Domain', + domain: { + customName: 'example.metamask.io', + customVersion: '1', + customChainId: 1, + customVerifyingContract: + '0x0000000000000000000000000000000000000000', + extraField: 'stuff', + }, + message: {}, + } as any, + 'V4', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + + it('should hash a typed message with data', function () { + const hash = sigUtil.TypedDataUtils.eip712Hash( + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + Message: [{ name: 'data', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: { + name: 'example.metamask.io', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + message: { + data: 'Hello!', + }, + }, + 'V4', + ); + + expect(hash.toString('hex')).toMatchSnapshot(); + }); + }); +}); + describe('concatSig', function () { it('should concatenate an extended ECDSA signature', function () { expect( @@ -3619,14 +4101,10 @@ it('signedTypeData', function () { }, }; - const utils = sigUtil.TypedDataUtils; const privateKey = ethUtil.keccak('cow'); const address = ethUtil.privateToAddress(privateKey); const sig = sigUtil.signTypedData(privateKey, { data: typedData }, 'V3'); - expect(ethUtil.bufferToHex(utils.eip712Hash(typedData, 'V3'))).toBe( - '0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2', - ); expect(ethUtil.bufferToHex(address)).toBe( '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', ); @@ -3676,7 +4154,6 @@ it('signedTypeData with bytes', function () { '0x25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca', }, }; - const utils = sigUtil.TypedDataUtils; const privateKey = ethUtil.sha3('cow'); const address = ethUtil.privateToAddress(privateKey); const sig = sigUtil.signTypedData( @@ -3685,9 +4162,6 @@ it('signedTypeData with bytes', function () { 'V3', ); - expect(ethUtil.bufferToHex(utils.eip712Hash(typedDataWithBytes, 'V3'))).toBe( - '0xb4aaf457227fec401db772ec22d2095d1235ee5d0833f56f59108c9ffc90fb4b', - ); expect(ethUtil.bufferToHex(address)).toBe( '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', ); @@ -3748,12 +4222,6 @@ it('signedTypeData_v4', function () { }, }; - const utils = sigUtil.TypedDataUtils; - - expect(ethUtil.bufferToHex(utils.eip712Hash(typedData, 'V4'))).toBe( - '0xa85c2e2b118698e88db68a8105b794a8cc7cec074e89ef991cb4f5f533819cc2', - ); - const privateKey = ethUtil.keccak('cow'); const address = ethUtil.privateToAddress(privateKey); @@ -3820,12 +4288,6 @@ it('signedTypeData_v4', function () { }, }; - const utils = sigUtil.TypedDataUtils; - - expect(ethUtil.bufferToHex(utils.eip712Hash(typedData, 'V4'))).toBe( - '0xa85c2e2b118698e88db68a8105b794a8cc7cec074e89ef991cb4f5f533819cc2', - ); - const privateKey = ethUtil.keccak('cow'); const address = ethUtil.privateToAddress(privateKey); @@ -3879,12 +4341,6 @@ it('signedTypeData_v4 with recursive types', function () { }, }; - const utils = sigUtil.TypedDataUtils; - - expect(ethUtil.bufferToHex(utils.eip712Hash(typedData, 'V4'))).toBe( - '0x807773b9faa9879d4971b43856c4d60c2da15c6f8c062bd9d33afefb756de19c', - ); - const privateKey = ethUtil.keccak('dragon'); const address = ethUtil.privateToAddress(privateKey); @@ -3938,12 +4394,6 @@ it('signedTypeMessage V4 with recursive types', function () { }, }; - const utils = sigUtil.TypedDataUtils; - - expect(ethUtil.bufferToHex(utils.eip712Hash(typedData, 'V4'))).toBe( - '0x807773b9faa9879d4971b43856c4d60c2da15c6f8c062bd9d33afefb756de19c', - ); - const privateKey = ethUtil.keccak('dragon'); const address = ethUtil.privateToAddress(privateKey); From 42c0e6685038aa8bca7c5828875e5329892d15bd Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 16 Aug 2021 17:00:11 -0230 Subject: [PATCH 2/4] Remove redundant period from test description --- src/__snapshots__/index.test.ts.snap | 2 +- src/index.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__snapshots__/index.test.ts.snap b/src/__snapshots__/index.test.ts.snap index a8d6b850..949b4575 100644 --- a/src/__snapshots__/index.test.ts.snap +++ b/src/__snapshots__/index.test.ts.snap @@ -2,7 +2,7 @@ exports[`TypedDataUtils.eip712Hash V3 should hash a minimal valid typed message 1`] = `"8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38"`; -exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with a domain separator that uses all fields. 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; +exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with a domain separator that uses all fields 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; exports[`TypedDataUtils.eip712Hash V3 should hash a typed message with data 1`] = `"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"`; diff --git a/src/index.test.ts b/src/index.test.ts index 8bea75a6..d646094d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3134,7 +3134,7 @@ describe('TypedDataUtils.eip712Hash', function () { ); }); - it('should hash a typed message with a domain separator that uses all fields.', function () { + it('should hash a typed message with a domain separator that uses all fields', function () { const hash = sigUtil.TypedDataUtils.eip712Hash( { types: { From 02b37496b6c1e7d381f5cac2cec38c18b521fcdb Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 16 Aug 2021 17:12:46 -0230 Subject: [PATCH 3/4] Add comments to further explain minimal message tests --- src/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index d646094d..b4fdd288 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3064,6 +3064,9 @@ describe('TypedDataUtils.eip712Hash', function () { describe('V3', function () { it('should hash a minimal valid typed message', function () { const hash = sigUtil.TypedDataUtils.eip712Hash( + // This represents the most basic "typed message" that is valid according to our types. + // It's not a very useful message (it's totally empty), but it's complete according to the + // spec. { types: { EIP712Domain: [], @@ -3080,6 +3083,8 @@ describe('TypedDataUtils.eip712Hash', function () { it('minimal typed message hash should be identical to minimal valid typed message hash', function () { const minimalHash = sigUtil.TypedDataUtils.eip712Hash( + // This tests that when the mandatory fields `domain`, `message`, and `types.EIP712Domain` + // are omitted, the result is the same as if they were included but empty. { types: {}, primaryType: 'EIP712Domain', From 4b17682b65cbc6a45b7c993b48737c5cf02daa11 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 16 Aug 2021 17:23:41 -0230 Subject: [PATCH 4/4] Add comments to V4 tests as well --- src/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index b4fdd288..54c66152 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3308,6 +3308,9 @@ describe('TypedDataUtils.eip712Hash', function () { describe('V4', function () { it('should hash a minimal valid typed message', function () { + // This represents the most basic "typed message" that is valid according to our types. + // It's not a very useful message (it's totally empty), but it's complete according to the + // spec. const hash = sigUtil.TypedDataUtils.eip712Hash( { types: { @@ -3324,6 +3327,8 @@ describe('TypedDataUtils.eip712Hash', function () { }); it('minimal typed message hash should be identical to minimal valid typed message hash', function () { + // This tests that when the mandatory fields `domain`, `message`, and `types.EIP712Domain` + // are omitted, the result is the same as if they were included but empty. const minimalHash = sigUtil.TypedDataUtils.eip712Hash( { types: {},