ABI Bool Array Packing
Description
Section titled “Description”This example demonstrates how bool arrays are packed efficiently in ARC-4:
- 8 booleans fit in 1 byte (1 bit per boolean)
- bool[8] encodes to exactly 1 byte
- bool[16] encodes to 2 bytes
- Partial byte arrays (e.g., bool[5]) still use full bytes Key characteristics of bool array packing:
- Each bool is stored as a single bit, not a full byte
- Bools are packed left-to-right starting from the MSB (most significant bit)
- Array length is rounded up to the next full byte
- This is much more space-efficient than storing each bool as uint8
Prerequisites
Section titled “Prerequisites”- No LocalNet required
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example abi/10-bool-packing.ts/** * Example: ABI Bool Array Packing * * This example demonstrates how bool arrays are packed efficiently in ARC-4: * - 8 booleans fit in 1 byte (1 bit per boolean) * - bool[8] encodes to exactly 1 byte * - bool[16] encodes to 2 bytes * - Partial byte arrays (e.g., bool[5]) still use full bytes * * Key characteristics of bool array packing: * - Each bool is stored as a single bit, not a full byte * - Bools are packed left-to-right starting from the MSB (most significant bit) * - Array length is rounded up to the next full byte * - This is much more space-efficient than storing each bool as uint8 * * Prerequisites: * - No LocalNet required */
import { ABIArrayDynamicType, ABIArrayStaticType, ABIType,} from '@algorandfoundation/algokit-utils/abi';import { formatHex, printHeader, printInfo, printStep, printSuccess } from '../shared/utils.js';
/** * Format a byte as a binary string showing all 8 bits */function formatBinary(byte: number): string { return byte.toString(2).padStart(8, '0');}
/** * Format a byte array as binary showing the bit layout */function formatBinaryBytes(bytes: Uint8Array): string { return Array.from(bytes) .map(b => formatBinary(b)) .join(' ');}
function main() { printHeader('ABI Bool Array Packing Example');
// Step 1: Introduction to bool array packing printStep(1, 'Introduction to Bool Array Packing');
printInfo('In ARC-4 ABI encoding, boolean arrays use bit-packing:'); printInfo(' - Each bool takes 1 bit (not 1 byte)'); printInfo(' - 8 bools fit in 1 byte'); printInfo(' - Bools are packed from MSB (bit 7) to LSB (bit 0)'); printInfo(' - true = 1, false = 0');
// Step 2: bool[8] - exactly 1 byte printStep(2, 'bool[8] Encoding - Exactly 1 Byte');
const bool8Type = ABIType.from('bool[8]') as ABIArrayStaticType;
printInfo(`Type: ${bool8Type.toString()}`); printInfo(`childType: ${bool8Type.childType.toString()}`); printInfo(`length: ${bool8Type.length}`); printInfo(`byteLen(): ${bool8Type.byteLen()}`); printInfo(`isDynamic(): ${bool8Type.isDynamic()}`);
// All true - should be 0b11111111 = 0xFF const allTrue8 = [true, true, true, true, true, true, true, true]; const allTrue8Encoded = bool8Type.encode(allTrue8);
printInfo(`\nAll true: [${allTrue8.join(', ')}]`); printInfo(` Encoded: ${formatHex(allTrue8Encoded)}`); printInfo(` Binary: ${formatBinaryBytes(allTrue8Encoded)}`); printInfo(` Length: ${allTrue8Encoded.length} byte`);
// All false - should be 0b00000000 = 0x00 const allFalse8 = [false, false, false, false, false, false, false, false]; const allFalse8Encoded = bool8Type.encode(allFalse8);
printInfo(`\nAll false: [${allFalse8.join(', ')}]`); printInfo(` Encoded: ${formatHex(allFalse8Encoded)}`); printInfo(` Binary: ${formatBinaryBytes(allFalse8Encoded)}`); printInfo(` Length: ${allFalse8Encoded.length} byte`);
// Alternating pattern - should be 0b10101010 = 0xAA const alternating8 = [true, false, true, false, true, false, true, false]; const alternating8Encoded = bool8Type.encode(alternating8);
printInfo(`\nAlternating: [${alternating8.join(', ')}]`); printInfo(` Encoded: ${formatHex(alternating8Encoded)}`); printInfo(` Binary: ${formatBinaryBytes(alternating8Encoded)}`); printInfo(` Expected: 10101010 = 0xAA`); printInfo(` Matches: ${alternating8Encoded[0] === 0xaa}`);
// Step 3: Bit position mapping printStep(3, 'Bit Position Mapping');
printInfo('Bools map to bit positions (MSB first):'); printInfo(' Array index: [0] [1] [2] [3] [4] [5] [6] [7]'); printInfo(' Bit position: b7 b6 b5 b4 b3 b2 b1 b0'); printInfo(' Bit value: 128 64 32 16 8 4 2 1');
// Single true at each position printInfo('\nSingle true at each position:');
for (let i = 0; i < 8; i++) { const bools = new Array(8).fill(false); bools[i] = true; const encoded = bool8Type.encode(bools); const expectedByte = 1 << (7 - i); // MSB first printInfo( ` Index ${i}: ${formatBinary(encoded[0])} = 0x${encoded[0].toString(16).padStart(2, '0')} (expected: 0x${expectedByte.toString(16).padStart(2, '0')})`, ); }
// Step 4: bool[16] - 2 bytes printStep(4, 'bool[16] Encoding - 2 Bytes');
const bool16Type = ABIType.from('bool[16]') as ABIArrayStaticType;
printInfo(`Type: ${bool16Type.toString()}`); printInfo(`byteLen(): ${bool16Type.byteLen()}`);
// First 8 true, rest false const firstHalf16 = [ true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, ]; const firstHalf16Encoded = bool16Type.encode(firstHalf16);
printInfo(`\nFirst 8 true, rest false:`); printInfo(` Encoded: ${formatHex(firstHalf16Encoded)}`); printInfo(` Binary: ${formatBinaryBytes(firstHalf16Encoded)}`); printInfo(` Byte 0: ${formatBinary(firstHalf16Encoded[0])} (positions 0-7)`); printInfo(` Byte 1: ${formatBinary(firstHalf16Encoded[1])} (positions 8-15)`);
// All true 16 const allTrue16 = new Array(16).fill(true); const allTrue16Encoded = bool16Type.encode(allTrue16);
printInfo(`\nAll 16 true:`); printInfo(` Encoded: ${formatHex(allTrue16Encoded)}`); printInfo(` Binary: ${formatBinaryBytes(allTrue16Encoded)}`); printInfo(` Length: ${allTrue16Encoded.length} bytes`);
// Step 5: Partial byte arrays (bool[5]) printStep(5, 'Partial Byte Arrays - bool[5]');
const bool5Type = ABIType.from('bool[5]') as ABIArrayStaticType;
printInfo(`Type: ${bool5Type.toString()}`); printInfo(`byteLen(): ${bool5Type.byteLen()}`); printInfo(`Note: 5 bools still require 1 full byte (bits 0-4 used, bits 5-7 are padding zeros)`);
const bool5Values = [true, true, true, true, true]; const bool5Encoded = bool5Type.encode(bool5Values);
printInfo(`\nAll 5 true: [${bool5Values.join(', ')}]`); printInfo(` Encoded: ${formatHex(bool5Encoded)}`); printInfo(` Binary: ${formatBinaryBytes(bool5Encoded)}`); printInfo(` Expected: 11111000 (5 ones + 3 padding zeros)`); printInfo(` Expected value: 0xF8 = ${0xf8}`); printInfo(` Matches: ${bool5Encoded[0] === 0xf8}`);
// Different bool[5] patterns const bool5Pattern = [true, false, true, false, true]; const bool5PatternEncoded = bool5Type.encode(bool5Pattern);
printInfo(`\nPattern [T,F,T,F,T]:`); printInfo(` Encoded: ${formatHex(bool5PatternEncoded)}`); printInfo(` Binary: ${formatBinaryBytes(bool5PatternEncoded)}`); printInfo(` Expected: 10101000 (pattern + 3 padding zeros)`);
// Step 6: Various partial byte sizes printStep(6, 'Byte Length Formula for Bool Arrays');
printInfo('Formula: byteLen = ceil(arrayLength / 8)'); printInfo(''); printInfo('Length | Bytes | Formula'); printInfo('-------|-------|--------');
const boolSizes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 24, 32];
for (const size of boolSizes) { const type = ABIType.from(`bool[${size}]`) as ABIArrayStaticType; const expectedBytes = Math.ceil(size / 8); printInfo( `${String(size).padStart(6)} | ${String(type.byteLen()).padStart(5)} | ceil(${size}/8) = ${expectedBytes}`, ); }
// Step 7: Compare bool[] vs uint8[] encoding size printStep(7, 'Size Comparison: bool[] vs uint8[]');
printInfo('Comparison of encoded sizes for same element count:'); printInfo(''); printInfo('Count | bool[N] bytes | uint8[N] bytes | Savings'); printInfo('------|---------------|----------------|--------');
const counts = [8, 16, 32, 64, 100, 256];
for (const count of counts) { const boolType = ABIType.from(`bool[${count}]`) as ABIArrayStaticType; const uint8Type = ABIType.from(`uint8[${count}]`) as ABIArrayStaticType;
const boolBytes = boolType.byteLen(); const uint8Bytes = uint8Type.byteLen(); const savings = ((1 - boolBytes / uint8Bytes) * 100).toFixed(1);
printInfo( `${String(count).padStart(5)} | ${String(boolBytes).padStart(13)} | ${String(uint8Bytes).padStart(14)} | ${savings}%`, ); }
printInfo('\nBool arrays use 8x less space than uint8 arrays for boolean data!');
// Step 8: Dynamic bool arrays printStep(8, 'Dynamic Bool Arrays');
const boolDynamicType = ABIType.from('bool[]') as ABIArrayDynamicType;
printInfo(`Type: ${boolDynamicType.toString()}`); printInfo(`childType: ${boolDynamicType.childType.toString()}`); printInfo(`isDynamic(): ${boolDynamicType.isDynamic()}`);
// Encode a dynamic bool array const dynamicBools = [true, true, false, true, true, false, false, true, true, false]; const dynamicEncoded = boolDynamicType.encode(dynamicBools);
printInfo(`\nDynamic array: [${dynamicBools.join(', ')}] (${dynamicBools.length} elements)`); printInfo(` Encoded: ${formatHex(dynamicEncoded)}`); printInfo(` Length: ${dynamicEncoded.length} bytes`);
// Break down the encoding const lengthPrefix = (dynamicEncoded[0] << 8) | dynamicEncoded[1]; const dataBytes = dynamicEncoded.slice(2);
printInfo(`\nEncoding breakdown:`); printInfo(` Length prefix: ${formatHex(dynamicEncoded.slice(0, 2))} = ${lengthPrefix} elements`); printInfo(` Data bytes: ${formatHex(dataBytes)}`); printInfo(` Data binary: ${formatBinaryBytes(dataBytes)}`);
// Step 9: Decoding packed bool arrays printStep(9, 'Decoding Packed Bool Arrays');
// Static array decode const encodeValues = [true, false, true, true, false, false, true, false]; const encoded = bool8Type.encode(encodeValues); const decoded = bool8Type.decode(encoded) as boolean[];
printInfo(`Original: [${encodeValues.join(', ')}]`); printInfo(`Encoded: ${formatHex(encoded)} = ${formatBinaryBytes(encoded)}`); printInfo(`Decoded: [${decoded.join(', ')}]`); printInfo(`Round-trip: ${decoded.every((v, i) => v === encodeValues[i])}`);
// Dynamic array decode const dynamicDecoded = boolDynamicType.decode(dynamicEncoded) as boolean[];
printInfo(`\nDynamic original: [${dynamicBools.join(', ')}]`); printInfo(`Dynamic decoded: [${dynamicDecoded.join(', ')}]`); printInfo(`Round-trip: ${dynamicDecoded.every((v, i) => v === dynamicBools[i])}`);
// Step 10: Bit-level view visualization printStep(10, 'Bit-Level View Visualization');
printInfo('Detailed bit-level view of bool[8] encoding:'); printInfo('');
const visualBools = [true, false, true, true, false, true, false, true]; const visualEncoded = bool8Type.encode(visualBools);
printInfo(`Values: [${visualBools.join(', ')}]`); printInfo(`Byte: ${formatHex(visualEncoded)} = ${formatBinary(visualEncoded[0])}`); printInfo(''); printInfo('Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |'); printInfo('---------|-----|-----|-----|-----|-----|-----|-----|-----|'); printInfo(`Value | ${visualBools.map(b => (b ? 'T' : 'F')).join(' | ')} |`); printInfo(`Bit | ${Array.from(formatBinary(visualEncoded[0])).join(' | ')} |`); printInfo(`Weight | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |`);
// Calculate the byte value step by step let byteValue = 0; const contributions: string[] = [];
for (let i = 0; i < 8; i++) { if (visualBools[i]) { const bitValue = 1 << (7 - i); byteValue += bitValue; contributions.push(String(bitValue)); } }
printInfo(''); printInfo( `Byte value = ${contributions.join(' + ')} = ${byteValue} = 0x${byteValue.toString(16).padStart(2, '0')}`, );
// Step 11: Summary printStep(11, 'Summary');
printInfo('Bool array packing in ARC-4:'); printInfo(''); printInfo('Packing rules:'); printInfo(' - Each bool = 1 bit'); printInfo(' - 8 bools pack into 1 byte'); printInfo(' - Bit order: MSB first (index 0 = bit 7)'); printInfo(' - Padding: zeros added to complete the last byte'); printInfo(''); printInfo('Size formula:'); printInfo(' - Static: byteLen = ceil(arrayLength / 8)'); printInfo(' - Dynamic: 2 (length prefix) + ceil(arrayLength / 8)'); printInfo(''); printInfo('Space efficiency:'); printInfo(' - 8x more efficient than storing bools as uint8'); printInfo(' - bool[256] = 32 bytes vs uint8[256] = 256 bytes'); printInfo(''); printInfo('Bit values by position:'); printInfo(' Position 0 (MSB): 0x80 = 128'); printInfo(' Position 1: 0x40 = 64'); printInfo(' Position 2: 0x20 = 32'); printInfo(' Position 3: 0x10 = 16'); printInfo(' Position 4: 0x08 = 8'); printInfo(' Position 5: 0x04 = 4'); printInfo(' Position 6: 0x02 = 2'); printInfo(' Position 7 (LSB): 0x01 = 1');
printSuccess('ABI Bool Array Packing example completed successfully!');}
main();