Skip to content
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

feat: improve ABI Coders decode validation #1426

Merged
merged 26 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2a91a64
feat: add bytes size valiadtion for overflow/undeflow and division is…
danielbate Nov 10, 2023
c51647c
chore: changeset
danielbate Nov 10, 2023
5fbdf6b
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Nov 10, 2023
c59a50b
feat: validate array and vector max size
danielbate Nov 10, 2023
693f170
chore: linting
danielbate Nov 13, 2023
4391980
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Nov 13, 2023
d897769
feat: add legnth checks to vecs and arrays
danielbate Nov 13, 2023
921b6c2
feat: missing test coverage for enum and b256 coder:
danielbate Nov 13, 2023
f0bedf7
Merge branch 'master' into db/feat/improve-decode-validation
danielbate Nov 13, 2023
ded84a8
Merge branch 'rc/salamander' of https://github.com/FuelLabs/fuels-ts …
danielbate Dec 22, 2023
4cad51c
Merge branch 'rc/salamander' of https://github.com/FuelLabs/fuels-ts …
danielbate Jan 3, 2024
953b76b
test: mock calc fee logic in transaction summary test
danielbate Jan 3, 2024
5432ec7
chore: refactor
danielbate Jan 3, 2024
446ef0e
chore: further refactor
danielbate Jan 3, 2024
dd71905
Merge branch 'rc/salamander' into db/feat/improve-decode-validation
danielbate Jan 4, 2024
8dd9a23
chore: rebuild
danielbate Jan 4, 2024
149d32e
Merge branch 'db/feat/improve-decode-validation' of https://github.co…
danielbate Jan 4, 2024
41cb75f
chore: rebuild
danielbate Jan 4, 2024
85fa3d5
Merge branch 'rc/salamander' into db/feat/improve-decode-validation
arboleya Jan 4, 2024
7fa1cae
chore: remove redundant import
danielbate Jan 4, 2024
7f0f6cf
chore: remove redundant import
danielbate Jan 4, 2024
18e1572
chore: linting
danielbate Jan 4, 2024
cc90177
Merge branch 'rc/salamander' into db/feat/improve-decode-validation
danielbate Jan 5, 2024
59b7155
chore: update changeset
danielbate Jan 5, 2024
23ccf9a
refactor: remove throw error function from abstract coder
danielbate Jan 5, 2024
110c161
Merge branch 'rc/salamander' into db/feat/improve-decode-validation
danielbate Jan 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-pumpkins-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/abi-coder": minor
---

Improve decode valiadtion of ABI Coders
danielbate marked this conversation as resolved.
Show resolved Hide resolved
32 changes: 31 additions & 1 deletion packages/abi-coder/src/coders/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FuelError, ErrorCode } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';

import { U8_MAX } from '../../test/utils/constants';
import { U32_MAX, U8_MAX } from '../../test/utils/constants';

import type { SmallBytesOptions } from './abstract-coder';
import { ArrayCoder } from './array';
Expand Down Expand Up @@ -108,4 +108,34 @@ describe('ArrayCoder', () => {
new FuelError(ErrorCode.ENCODE_ERROR, 'Types/values length mismatch.')
);
});

it('throws when decoding empty bytes', async () => {
const coder = new ArrayCoder(new NumberCoder('u8'), 1);
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
);
});

it('throws when decoding invalid bytes (too small)', async () => {
const coder = new ArrayCoder(new NumberCoder('u8'), 8);
const input = new Uint8Array([0]);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
);
});

it('throws when decoding vec larger than max size', async () => {
const coder = new ArrayCoder(new NumberCoder('u8'), 8);
const input = new Uint8Array(U32_MAX + 1);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
);
});
});
5 changes: 5 additions & 0 deletions packages/abi-coder/src/coders/array.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ErrorCode } from '@fuel-ts/errors';

import { MAX_BYTES } from '../constants';
import { concatWithDynamicData } from '../utilities';

import type { TypesOfCoder } from './abstract-coder';
Expand Down Expand Up @@ -34,6 +35,10 @@ export class ArrayCoder<TCoder extends Coder> extends Coder<
}

decode(data: Uint8Array, offset: number): [DecodedValueOf<TCoder>, number] {
if (data.length < this.encodedLength || data.length > MAX_BYTES) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid array data size.`);
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
}

let newOffset = offset;
const decodedValue = Array(this.length)
.fill(0)
Expand Down
70 changes: 44 additions & 26 deletions packages/abi-coder/src/coders/b256.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';

import { B256Coder } from './b256';

/**
Expand Down Expand Up @@ -46,52 +49,67 @@ describe('B256Coder', () => {
expect(actualLength).toBe(expectedLength);
});

it('should throw an error when encoding a 256 bit hash string that is too short', () => {
it('should throw an error when encoding a 256 bit hash string that is too short', async () => {
const invalidInput = B256_DECODED.slice(0, B256_DECODED.length - 1);

expect(() => {
coder.encode(invalidInput);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.encode(invalidInput),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

it('should throw an error when decoding an encoded 256 bit hash string that is too short', () => {
it('should throw an error when decoding an encoded 256 bit hash string that is too short', async () => {
const invalidInput = B256_ENCODED.slice(0, B256_ENCODED.length - 1);

expect(() => {
coder.decode(invalidInput, 0);
}).toThrow();
await expectToThrowFuelError(
() => coder.decode(invalidInput, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data size.')
);
});

it('should throw an error when encoding a 256 bit hash string that is too long', () => {
it('should throw an error when encoding a 256 bit hash string that is too long', async () => {
const invalidInput = `${B256_DECODED}0`;

expect(() => {
coder.encode(invalidInput);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.encode(invalidInput),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

it('should throw an error when encoding a 512 bit hash string', () => {
it('should throw an error when encoding a 512 bit hash string', async () => {
const B512 =
'0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2';

expect(() => {
coder.encode(B512);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.encode(B512),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

it('should throw an error when decoding an encoded 256 bit hash string that is too long', () => {
const invalidInput = new Uint8Array(Array.from(Array(32).keys()));
it('should throw an error when encoding a 256 bit hash string that is not a hex string', async () => {
const invalidInput = 'not a hex string';

expect(() => {
coder.decode(invalidInput, 1);
}).toThrow('Invalid size for b256');
await expectToThrowFuelError(
() => coder.encode(invalidInput),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

it('should throw an error when encoding a 256 bit hash string that is not a hex string', () => {
const invalidInput = 'not a hex string';
it('throws when decoding empty bytes', async () => {
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data size.')
);
});

it('should throw an error when decoding an encoded b256 bit hash string that is too long', async () => {
const invalidInput = new Uint8Array(Array.from(Array(65).keys()));

expect(() => {
coder.encode(invalidInput);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.decode(invalidInput, 62),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 byte data size.')
);
});
});
19 changes: 14 additions & 5 deletions packages/abi-coder/src/coders/b256.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { ErrorCode } from '@fuel-ts/errors';
import { bn, toHex } from '@fuel-ts/math';
import { getBytesCopy } from 'ethers';

import { WORD_SIZE } from '../constants';

import { Coder } from './abstract-coder';

export class B256Coder extends Coder<string, string> {
constructor() {
super('b256', 'b256', 32);
super('b256', 'b256', WORD_SIZE * 4);
}

encode(value: string): Uint8Array {
Expand All @@ -16,21 +18,28 @@ export class B256Coder extends Coder<string, string> {
} catch (error) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
if (encodedValue.length !== 32) {
if (encodedValue.length !== this.encodedLength) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
return encodedValue;
}

decode(data: Uint8Array, offset: number): [string, number] {
let bytes = data.slice(offset, offset + 32);
if (data.length < this.encodedLength) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid b256 data size.`);
}

let bytes = data.slice(offset, offset + this.encodedLength);

const decoded = bn(bytes);
if (decoded.isZero()) {
bytes = new Uint8Array(32);
}
if (bytes.length !== 32) {
this.throwError(ErrorCode.DECODE_ERROR, `'Invalid size for b256'.`);

if (bytes.length !== this.encodedLength) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid b256 byte data size.`);
}

return [toHex(bytes, 32), offset + 32];
}
}
46 changes: 30 additions & 16 deletions packages/abi-coder/src/coders/b512.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';

import { B512Coder } from './b512';

/**
Expand Down Expand Up @@ -58,14 +61,6 @@ describe('B512Coder', () => {
}).toThrow(/Invalid struct B512/);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too short', () => {
const invalidInput = B512_ENCODED.slice(0, B512_ENCODED.length - 1);

expect(() => {
coder.decode(invalidInput, 0);
}).toThrow('Invalid size for b512');
});

it('should throw an error when encoding a 512 bit hash string that is too long', () => {
const invalidInput = `${B512_DECODED}0`;

Expand All @@ -82,19 +77,38 @@ describe('B512Coder', () => {
}).toThrow(/Invalid struct B512/);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too long', () => {
const invalidInput = new Uint8Array(Array.from(Array(32).keys()));

expect(() => {
coder.decode(invalidInput, 1);
}).toThrow('Invalid size for b512');
});

it('should throw an error when encoding a 512 bit hash string that is not a hex string', () => {
const invalidInput = 'not a hex string';

expect(() => {
coder.encode(invalidInput);
}).toThrow(/Invalid struct B512/);
});

it('throws when decoding empty bytes', async () => {
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data size.')
);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too short', async () => {
const invalidInput = B512_ENCODED.slice(0, B512_ENCODED.length - 1);

await expectToThrowFuelError(
() => coder.decode(invalidInput, 8),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data size.')
);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too long', async () => {
const invalidInput = new Uint8Array(Array.from(Array(65).keys()));

await expectToThrowFuelError(
() => coder.decode(invalidInput, 8),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 byte data size.')
);
});
});
21 changes: 15 additions & 6 deletions packages/abi-coder/src/coders/b512.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { ErrorCode } from '@fuel-ts/errors';
import { bn, toHex } from '@fuel-ts/math';
import { getBytesCopy } from 'ethers';

import { WORD_SIZE } from '../constants';

import { Coder } from './abstract-coder';

export class B512Coder extends Coder<string, string> {
constructor() {
super('b512', 'struct B512', 64);
super('b512', 'struct B512', WORD_SIZE * 8);
}

encode(value: string): Uint8Array {
Expand All @@ -16,21 +18,28 @@ export class B512Coder extends Coder<string, string> {
} catch (error) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
if (encodedValue.length !== 64) {
if (encodedValue.length !== this.encodedLength) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
return encodedValue;
}

decode(data: Uint8Array, offset: number): [string, number] {
let bytes = data.slice(offset, offset + 64);
if (data.length < this.encodedLength) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid b512 data size.`);
}

let bytes = data.slice(offset, offset + this.encodedLength);

const decoded = bn(bytes);
if (decoded.isZero()) {
bytes = new Uint8Array(64);
}
if (bytes.length !== 64) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid size for b512.`);

if (bytes.length !== this.encodedLength) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid b512 byte data size.`);
}
return [toHex(bytes, 64), offset + 64];

return [toHex(bytes, this.encodedLength), offset + this.encodedLength];
}
}
9 changes: 9 additions & 0 deletions packages/abi-coder/src/coders/boolean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,13 @@ describe('BooleanCoder', () => {
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid boolean value.')
);
});

it('throws when decoding empty bytes', async () => {
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid boolean data size.')
);
});
});
13 changes: 9 additions & 4 deletions packages/abi-coder/src/coders/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,24 @@ export class BooleanCoder extends Coder<boolean, boolean> {
}

decode(data: Uint8Array, offset: number): [boolean, number] {
if (data.length < this.paddingLength) {
this.throwError(ErrorCode.DECODE_ERROR, 'Invalid boolean data size.');
}

let bytes;

if (this.options.isRightPadded) {
bytes = bn(data.slice(offset, offset + 1));
bytes = data.slice(offset, offset + 1);
} else {
bytes = bn(data.slice(offset, offset + this.paddingLength));
bytes = data.slice(offset, offset + this.paddingLength);
}

if (bytes.isZero()) {
const decodedValue = bn(bytes);
if (decodedValue.isZero()) {
return [false, offset + this.paddingLength];
}

if (!bytes.eq(bn(1))) {
if (!decodedValue.eq(bn(1))) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid boolean value.`);
}

Expand Down
Loading
Loading