Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions yarn-project/stdlib/src/abi/buffer.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Fr } from '@aztec/foundation/curves/bn254';

import { bufferAsFields, bufferFromFields } from './buffer.js';

describe('buffer', () => {
Expand All @@ -11,4 +13,44 @@ describe('buffer', () => {
const buffer = Buffer.from('1234567890abcdef'.repeat(10), 'hex');
expect(() => bufferAsFields(buffer, 3)).toThrow(/exceeds maximum size/);
});

it('pads with zeros when declared length exceeds payload', () => {
// Create a small buffer, encode it, then truncate the field array before decoding.
const buffer = Buffer.from('aabbccdd', 'hex'); // 4 bytes
const fields = bufferAsFields(buffer, 10);
// Declared length is 4 bytes, stored in fields[0]. Payload fields follow.
// Artificially inflate the declared length to 62 bytes (2 full fields).

const inflatedFields = [new Fr(62), ...fields.slice(1)];
const result = bufferFromFields(inflatedFields);
// Result should be exactly 62 bytes: original 4 bytes followed by 58 zero bytes.
expect(result.length).toBe(62);
expect(result.subarray(0, 4).toString('hex')).toEqual('aabbccdd');
expect(result.subarray(4).every(b => b === 0)).toBe(true);
});

it('pads with zeros when payload fields are truncated', () => {
// Simulate the blob reconstruction scenario: declared length says 93 bytes (3 fields),
// but only 1 payload field is present.

const payloadField = Fr.fromBuffer(
Buffer.from('00' + 'ab'.repeat(31), 'hex'), // 31 bytes of 0xab
);
// Declared length = 93 bytes (would need 3 fields), but only 1 field in payload.
const fields = [new Fr(93), payloadField];
const result = bufferFromFields(fields);
expect(result.length).toBe(93);
// First 31 bytes come from the single payload field.
expect(result.subarray(0, 31).every(b => b === 0xab)).toBe(true);
// Remaining 62 bytes are zero-padded.
expect(result.subarray(31).every(b => b === 0)).toBe(true);
});

it('returns exact buffer when payload matches declared length', () => {
const buffer = Buffer.from('ff'.repeat(31), 'hex'); // exactly 1 field of payload
const fields = bufferAsFields(buffer, 5);
const result = bufferFromFields(fields);
expect(result.length).toBe(31);
expect(result.toString('hex')).toEqual(buffer.toString('hex'));
});
});
29 changes: 25 additions & 4 deletions yarn-project/stdlib/src/abi/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,32 @@ export function bufferAsFields(input: Buffer, targetLength: number): Fr[] {
}

/**
* Recovers a buffer from an array of fields.
* @param fields - An output from bufferAsFields.
* @returns The recovered buffer.
* Recovers a buffer from an array of fields previously encoded with bufferAsFields.
*
* The first field encodes the byte length of the original buffer. The remaining fields
* each carry 31 bytes of payload (the leading byte of each 32-byte field element is skipped).
*
* If the declared byte length exceeds the bytes available from the payload fields, the result
* is zero-padded to the full declared length. This is important for correctness when the field
* array has been truncated (e.g. contract class logs reconstructed from blobs using a short
* emittedLength): without padding, the resulting buffer would be shorter than declared, causing
* bytecode commitment computations to diverge from what the circuit produced.
*
* @param fields - An output from bufferAsFields: [byteLength, ...payloadFields].
* @returns A buffer of exactly `byteLength` bytes.
*/
export function bufferFromFields(fields: Fr[]): Buffer {
const [length, ...payload] = fields;
return Buffer.concat(payload.map(f => f.toBuffer().subarray(1))).subarray(0, length.toNumber());
const byteLength = length.toNumber();
const raw = Buffer.concat(payload.map(f => f.toBuffer().subarray(1)));
if (raw.length >= byteLength) {
return raw.subarray(0, byteLength);
}
// Pad with zeros if the declared length exceeds the available payload bytes.
// This ensures the returned buffer always matches the declared length, so that
// downstream bytecode commitment computations are consistent even when the
// source field array was truncated (e.g. reconstructed from blob with a short emittedLength).
const result = Buffer.alloc(byteLength);
raw.copy(result);
return result;
}
Loading