Boxes and Registers
In ErgoScript, a 'box' is akin to a more versatile version of what a UTXO (Unspent Transaction Output) represents in Bitcoin and many other cryptocurrencies. A box is not only a ledger entry denoting the amount of cryptocurrency owned by a particular address, but it also carries 'registers', allowing it to contain additional data. This data could range from simple values to more complex structures, which can later be integrated into transactions and used in the execution of smart contracts.
Key Concepts:
- Box: A versatile UTXO that can carry additional data in registers
- Registers: Storage slots (R4-R9) that can hold custom data
- Enhanced Functionality: Beyond simple value storage, enables complex smart contract operations
Interpreting this register data off-chain is a common task; see Fleet SDK Recipes for examples using JavaScript/TypeScript.
Box vs Traditional UTXO
This makes Ergo's box different from a traditional UTXO, which only represents an amount of unspent cryptocurrency associated with a certain address. In UTXO-based cryptocurrencies, each transaction consumes one or more UTXOs as inputs and creates one or more UTXOs as outputs, with the 'unspent' outputs being the 'coins' that can be spent in future transactions.
The term 'box' in Ergo's context captures the idea that these entities are like containers holding various types of information (value, tokens, custom data, etc.), beyond just the unspent transaction output balance. This makes the boxes in Ergo significantly more flexible and functional, enabling more complex operations, such as running scripts or smart contracts, directly on the blockchain.
Box Structure and Properties
Each box in Ergo contains several key components:
- Value: The amount of ERG (in NanoERGs) stored in the box
- Box ID: A unique identifier for the box
- Proposition Bytes: The serialized spending condition (ErgoScript contract)
- Tokens: A collection of native tokens stored in the box
- Registers (R0-R9): Data storage slots with different purposes:
R0: Value (mandatory)R1: Script protection (mandatory)R2: Tokens (mandatory)R3: Creation info (mandatory)R4-R9: Custom data (optional)
Accessing Box Data in ErgoScript
ErgoScript provides several ways to access box data within smart contracts:
Basic Box Properties
// Accessing basic box properties
val boxValue = SELF.value // Get the ERG value in NanoERGs
val boxId = SELF.id // Get the unique box ID
val boxBytes = SELF.bytes // Get serialized box content
val boxTokens = SELF.tokens // Get all tokens in the box
val propositionBytes = SELF.propositionBytes // Get the spending conditionRegister Access
Registers R4-R9 can store arbitrary typed data. Here's how to access them safely:
// Accessing registers with type checking
val r4Data = SELF.R4[Coll[Byte]].get // Get R4 as byte collection (throws if empty)
val r5Value = SELF.R5[Long].get // Get R5 as Long (throws if empty)
val r6Option = SELF.R6[Int] // Get R6 as Option[Int] (safe)
// Safe register access with default values
val r4Safe = SELF.R4[Coll[Byte]].getOrElse(Coll[Byte]())
val r5Safe = SELF.R5[Long].getOrElse(0L)
// Checking if register exists
val hasR4 = SELF.R4[Coll[Byte]].isDefined
val hasR5 = SELF.R5[Long].isDefinedToken Access
Tokens are stored as a collection of (TokenId, Amount) pairs:
// Accessing tokens
val allTokens = SELF.tokens // Get all tokens as Coll[(Coll[Byte], Long)]
val tokenCount = SELF.tokens.size // Number of different tokens
// Accessing specific tokens (if they exist)
if (SELF.tokens.size > 0) {
val firstTokenId = SELF.tokens(0)._1 // First token ID
val firstTokenAmount = SELF.tokens(0)._2 // First token amount
}
// Finding a specific token
val targetTokenId: Coll[Byte] = fromBase16("...")
val tokenExists = SELF.tokens.exists { (tokenPair: (Coll[Byte], Long)) =>
tokenPair._1 == targetTokenId
}Working with Input and Output Boxes
Contracts often need to examine other boxes in the transaction:
// Accessing transaction boxes
val allInputs = INPUTS // All input boxes
val allOutputs = OUTPUTS // All output boxes
val firstInput = INPUTS(0) // First input box
val firstOutput = OUTPUTS(0) // First output box
// Common patterns
val totalInputValue = INPUTS.fold(0L, { (acc: Long, box: Box) => acc + box.value })
val totalOutputValue = OUTPUTS.fold(0L, { (acc: Long, box: Box) => acc + box.value })
// Finding boxes with specific conditions
val highValueOutputs = OUTPUTS.filter { (box: Box) => box.value > 1000000L }
val boxesWithTokens = OUTPUTS.filter { (box: Box) => box.tokens.size > 0 }Practical Examples
Here are some common patterns for working with boxes and registers:
Data Storage and Retrieval
// Example: Oracle box storing price data
val oracleBox = CONTEXT.dataInputs(0) // Reference oracle as data input
val currentPrice = oracleBox.R4[Long].get // Price stored in R4
val timestamp = oracleBox.R5[Long].get // Timestamp in R5
val isValidPrice = timestamp > HEIGHT - 10 // Price is recent (within 10 blocks)
sigmaProp(isValidPrice && currentPrice > 1000000L)Token Validation
// Example: Ensuring specific token is preserved
val requiredTokenId = fromBase16("1234567890abcdef...")
val inputTokenAmount = SELF.tokens.fold(0L, { (acc: Long, token: (Coll[Byte], Long)) =>
if (token._1 == requiredTokenId) acc + token._2 else acc
})
val outputTokenAmount = OUTPUTS(0).tokens.fold(0L, { (acc: Long, token: (Coll[Byte], Long)) =>
if (token._1 == requiredTokenId) acc + token._2 else acc
})
sigmaProp(inputTokenAmount == outputTokenAmount)Best Practices
- Always check register existence: Use
isDefinedorgetOrElseto avoid errors - Type safety: Specify correct types when accessing registers to prevent runtime errors
- Efficient token handling: Use fold or filter operations instead of multiple individual checks
- Validate box structure: Always verify that boxes contain expected data before using it
- Consider gas costs: Complex box operations consume more computation resources