Skip to content
Algorand Developer Portal

ABI Bool Array Packing

← Back to ABI Encoding

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
  • No LocalNet required

From the repository root:

Terminal window
cd examples
npm run example abi/10-bool-packing.ts

View source on GitHub

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();