Skip to content

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