diff --git a/yarn-project/stdlib/src/abi/buffer.test.ts b/yarn-project/stdlib/src/abi/buffer.test.ts index 9c3329666c30..d4ca635d5b92 100644 --- a/yarn-project/stdlib/src/abi/buffer.test.ts +++ b/yarn-project/stdlib/src/abi/buffer.test.ts @@ -1,3 +1,5 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; + import { bufferAsFields, bufferFromFields } from './buffer.js'; describe('buffer', () => { @@ -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')); + }); }); diff --git a/yarn-project/stdlib/src/abi/buffer.ts b/yarn-project/stdlib/src/abi/buffer.ts index 075143244254..42d4ea8eba8a 100644 --- a/yarn-project/stdlib/src/abi/buffer.ts +++ b/yarn-project/stdlib/src/abi/buffer.ts @@ -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; }