Skip to content

Commit

Permalink
fix: Use a single keccak function since ethereumjs converts it to a B…
Browse files Browse the repository at this point in the history
…uffer no matter what.
  • Loading branch information
richtera committed Jan 17, 2024
1 parent 01cceea commit cdc6c0a
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 113 deletions.
20 changes: 15 additions & 5 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

/* eslint-disable @typescript-eslint/ban-types */
import { numberToHex, keccak256 } from 'web3-utils';
import { arrToBufArr, bufferToHex } from 'ethereumjs-util';

import { MethodData, Encoding, Method } from '../types/Method';

Expand Down Expand Up @@ -112,25 +113,34 @@ export const SUPPORTED_VERIFICATION_METHODS_LIST = Object.values(
SUPPORTED_VERIFICATION_METHOD_STRINGS,
);

function keccak256Utf8(data) {
return keccak256(JSON.stringify(data));
function keccak256Method(data: object | string | Uint8Array | null) {
if (data === null) {
return keccak256('');
}
if (data instanceof Uint8Array) {
return keccak256(bufferToHex(arrToBufArr(data)));
}
if (typeof data === 'object') {
return keccak256(JSON.stringify(data));
}
return keccak256(data);
}

const KECCAK256_UTF8 = {
method: keccak256Utf8,
method: keccak256Method,
name: SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_UTF8,
sig: SUPPORTED_VERIFICATION_METHOD_HASHES.HASH_KECCAK256_UTF8,
};

const KECCAK256_BYTES = {
method: keccak256,
method: keccak256Method,
name: SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_BYTES,
sig: SUPPORTED_VERIFICATION_METHOD_HASHES.HASH_KECCAK256_BYTES,
};

export const HASH_METHODS: {
[key: string]: {
method: Function;
method: (data: object | string | Uint8Array | null) => string;
name: SUPPORTED_VERIFICATION_METHOD_STRINGS;
sig: SUPPORTED_VERIFICATION_METHODS;
};
Expand Down
194 changes: 105 additions & 89 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,21 +648,23 @@ describe('Running @erc725/erc725.js tests...', () => {
const jsonString = `{"LSP3Profile":{"profileImage":"ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf","backgroundImage":"ipfs://QmZF5pxDJcB8eVvCd74rsXBFXhWL3S1XR5tty2cy1a58Ew","description":"Beautiful clothing that doesn't cost the Earth. A sustainable designer based in London Patrick works with brand partners to refocus on systemic change centred around creative education. "}}`;

const fetchStub = sinon.stub(global, 'fetch');
fetchStub.onCall(0).returns(
Promise.resolve(
new Response(jsonString, {
headers: { 'content-type': 'application/json' },
}),
),
);
const result = await erc725.fetchData('TestJSONURL');
fetchStub.restore();

assert.deepStrictEqual(result, {
key: testJSONURLSchema.key,
name: testJSONURLSchema.name,
value: JSON.parse(jsonString),
});
try {
fetchStub.onCall(0).returns(
Promise.resolve(
new Response(jsonString, {
headers: { 'content-type': 'application/json' },
}),
),
);
const result = await erc725.fetchData('TestJSONURL');
assert.deepStrictEqual(result, {
key: testJSONURLSchema.key,
name: testJSONURLSchema.name,
value: JSON.parse(jsonString),
});
} finally {
fetchStub.restore();
}
});

it('fetchData JSONURL with custom config.ipfsGateway', async () => {
Expand All @@ -686,26 +688,29 @@ describe('Running @erc725/erc725.js tests...', () => {
const jsonString = `{"LSP3Profile":{"profileImage":"ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf","backgroundImage":"ipfs://QmZF5pxDJcB8eVvCd74rsXBFXhWL3S1XR5tty2cy1a58Ew","description":"Beautiful clothing that doesn't cost the Earth. A sustainable designer based in London Patrick works with brand partners to refocus on systemic change centred around creative education. "}}`;

const fetchStub = sinon.stub(global, 'fetch');
fetchStub.onCall(0).returns(
Promise.resolve(
new Response(jsonString, {
headers: { 'content-type': 'application/json' },
}),
),
);
const result = await erc725.fetchData('TestJSONURL');
assert.deepStrictEqual(result, {
key: testJSONURLSchema.key,
name: testJSONURLSchema.name,
value: JSON.parse(jsonString),
});
fetchStub.restore();
try {
fetchStub.onCall(0).returns(
Promise.resolve(
new Response(jsonString, {
headers: { 'content-type': 'application/json' },
}),
),
);
const result = await erc725.fetchData('TestJSONURL');
assert.deepStrictEqual(result, {
key: testJSONURLSchema.key,
name: testJSONURLSchema.name,
value: JSON.parse(jsonString),
});

assert.ok(
fetchStub.calledWith(
`${ipfsGateway}/ipfs/QmbErKh3FjsAR6YjsTjHZNm6McDp6aRt82Ftcv9AJJvZbd`, // this value comes from the mockSchema
),
);
assert.ok(
fetchStub.calledWith(
`${ipfsGateway}/ipfs/QmbErKh3FjsAR6YjsTjHZNm6McDp6aRt82Ftcv9AJJvZbd`, // this value comes from the mockSchema
),
);
} finally {
fetchStub.restore();
}
});

if (contractVersion.interface === ERC725Y_INTERFACE_IDS['3.0']) {
Expand Down Expand Up @@ -739,72 +744,83 @@ describe('Running @erc725/erc725.js tests...', () => {
const jsonString = `{"LSP3Profile":{"profileImage":"ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf","backgroundImage":"ipfs://QmZF5pxDJcB8eVvCd74rsXBFXhWL3S1XR5tty2cy1a58Ew","description":"Beautiful clothing that doesn't cost the Earth. A sustainable designer based in London Patrick works with brand partners to refocus on systemic change centred around creative education. "}}`;

const fetchStub = sinon.stub(global, 'fetch');
fetchStub
.onCall(0)
.returns(Promise.resolve(new Response(jsonString)));
const result = await erc725.fetchData({
keyName: 'JSONForAddress:<address>',
dynamicKeyParts: '0xcafecafecafecafecafecafecafecafecafecafe',
});
fetchStub.restore();
assert.deepStrictEqual(result, {
name: 'JSONForAddress:cafecafecafecafecafecafecafecafecafecafe',
key: '0x84b02f6e50a0a0819a4f0000cafecafecafecafecafecafecafecafecafecafe',
value: JSON.parse(jsonString),
});
try {
fetchStub
.onCall(0)
.returns(Promise.resolve(new Response(jsonString)));
const result = await erc725.fetchData({
keyName: 'JSONForAddress:<address>',
dynamicKeyParts: '0xcafecafecafecafecafecafecafecafecafecafe',
});

assert.deepStrictEqual(result, {
name: 'JSONForAddress:cafecafecafecafecafecafecafecafecafecafe',
key: '0x84b02f6e50a0a0819a4f0000cafecafecafecafecafecafecafecafecafecafe',
value: JSON.parse(jsonString),
});
} finally {
fetchStub.restore();
}
});
}

if (contractVersion.interface === ERC725Y_INTERFACE_IDS.legacy) {
it('fetchData AssetURL', async () => {
const fetchStub = sinon.stub(global, 'fetch');
fetchStub
.onCall(0)
.returns(Promise.resolve(new Response(new Uint8Array(5))));
try {
fetchStub
.onCall(0)
.returns(
Promise.resolve(
new Response(
Uint8Array.from(Buffer.from('{"hello": "world"}')),
),
),
);

const provider = new HttpProvider(
{
returnData: [
const provider = new HttpProvider(
{
returnData: [
{
key: '0xf18290c9b373d751e12c5ec807278267a807c35c3806255168bc48a85757ceee',
value:
'0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000598019f9b1c41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec697066733a2f2f516d596f387967347a7a6d647532364e537674736f4b6555356f56523668326f686d6f61324378356939316d506600000000000000',
},

// Encoded value of:
// {
// verification: {
// method: 'keccak256(bytes)', // 0x8019f9b1
// data: '0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec',
// },
// url: 'ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf',
// },
],
},
[contractVersion.interface],
);

const erc725 = new ERC725(
[
{
name: 'TestAssetURL',
key: '0xf18290c9b373d751e12c5ec807278267a807c35c3806255168bc48a85757ceee',
value:
'0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000598019f9b1c41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec697066733a2f2f516d596f387967347a7a6d647532364e537674736f4b6555356f56523668326f686d6f61324378356939316d506600000000000000',
keyType: 'Singleton',
valueContent: 'AssetURL',
valueType: 'bytes',
},

// Encoded value of:
// {
// verification: {
// method: 'keccak256(bytes)', // 0x8019f9b1
// data: '0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec',
// },
// url: 'ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf',
// },
],
},
[contractVersion.interface],
);

const erc725 = new ERC725(
[
{
name: 'TestAssetURL',
key: '0xf18290c9b373d751e12c5ec807278267a807c35c3806255168bc48a85757ceee',
keyType: 'Singleton',
valueContent: 'AssetURL',
valueType: 'bytes',
},
],
address,
provider,
);
const result = await erc725.fetchData('TestAssetURL');

fetchStub.restore();

assert.strictEqual(
Object.prototype.toString.call(result.value),
'[object Uint8Array]',
);
address,
provider,
);
const result = await erc725.fetchData('TestAssetURL');

assert.strictEqual(result.value, {
hello: 'world',
});
} finally {
fetchStub.restore();
}
});
}
});
Expand Down
13 changes: 4 additions & 9 deletions src/lib/getDataFromExternalSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
GetDataExternalSourcesOutput,
} from '../types/decodeData';
import { ERC725JSONSchema } from '../types/ERC725JSONSchema';
import { SUPPORTED_VERIFICATION_METHOD_STRINGS } from '../constants/constants';
import { isDataAuthentic, patchIPFSUrlsIfApplicable } from './utils';
import { URLDataWithHash } from '../types';

Expand Down Expand Up @@ -84,19 +83,15 @@ export const getDataFromExternalSources = (
if (!response.ok) {
throw new Error(response.statusText);
}
if (
urlDataWithHash.verification?.method ===
SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_BYTES
) {
return response
.arrayBuffer()
.then((buffer) => new Uint8Array(buffer));
}
// Previously we used to return a Uint8Array in the case of a verification
// method of 'keccak256(bytes)' but since this is a JSONURL or VerifiableURI,
// all data has to be json for sure.
return response.json();
});
if (isDataAuthentic(receivedData, urlDataWithHash.verification)) {
return { ...dataEntry, value: receivedData };
}
console.log(receivedData, urlDataWithHash.verification);
throw new Error('result did not correctly validate');
} catch (error: any) {
error.message = `GET request to ${urlDataWithHash.url} (resolved as ${url}) failed: ${error.message}`;
Expand Down
21 changes: 21 additions & 0 deletions src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
encodeTupleKeyValue,
duplicateMultiTypeERC725SchemaEntry,
splitMultiDynamicKeyNamePart,
countSignificantBits,
} from './utils';
import { isDynamicKeyName } from './encodeKeyName';
import { decodeKey } from './decodeData';
Expand Down Expand Up @@ -252,6 +253,26 @@ describe('utils', () => {
});
});

describe('count bits', () => {
const testCases = [
{
value: '0x00',
result: 0,
},
{
value: '0x01',
result: 1,
},
{ value: '0x1000', result: 13 },
{ value: '0x000f', result: 4 },
];
testCases.forEach(({ value, result }) => {
it(`should count the number of bits in ${value}`, () => {
assert.equal(countSignificantBits(value), result);
});
});
});

describe('encodeKeyValue/decodeKeyValue', () => {
const testCases = [
{
Expand Down
19 changes: 9 additions & 10 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
padLeft,
stripHexPrefix,
} from 'web3-utils';
import { arrToBufArr } from 'ethereumjs-util';

import {
URLDataToEncode,
Expand Down Expand Up @@ -505,17 +504,11 @@ export function isDataAuthentic(
data: string | Uint8Array,
options: Verification,
): boolean {
let dataHash: string;

if (!options || !options.method) {
return true;
}

if (data instanceof Uint8Array) {
dataHash = hashData(arrToBufArr(data), options.method);
} else {
dataHash = hashData(data, options.method);
}
const dataHash = hashData(data, options.method);

if (dataHash !== options.data) {
console.error(
Expand Down Expand Up @@ -589,11 +582,17 @@ export function countNumberOfBytes(data: string) {
return stripHexPrefix(data).length / 2;
}

export function countSignificantBits(data = '0x0') {
export function countSignificantBits(data: string) {
const bytes = hexToBytes(data);
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] !== 0) {
return (bytes.length - i - 1) * 8 + 32 - Math.clz32(bytes[i]);
return (
(bytes.length - i - 1) * 8 + // The number of bits with non-zero values from the right.
(32 - Math.clz32(bytes[i])) // The number of bits from the right of the current byte.
// The docs for Math.clz32 are:
// Returns the number of leading zero bits in the 32-bit binary representation of a number.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
);
}
}
return 0;
Expand Down

0 comments on commit cdc6c0a

Please sign in to comment.