Practical Examples
← Codec Overview | API | Type Rules | Examples | RLP Comparison
Overview
This document provides real-world examples of using the byte codec in blockchain applications, demonstrating common patterns and use cases.
Basic Usage
Simple Data Encoding
import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
// Encoding primitives
val longValue = 42L
val longBytes = ByteEncoder[Long].encode(longValue)
// longBytes: ByteVector = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtByteBuffer@4630e946,
// offset = 0L,
// size = 8L
// )
// )
val longDecoded = ByteDecoder[Long].decode(longBytes)
// longDecoded: Either[DecodeFailure, DecodeResult[Long]] = Right(
// value = DecodeResult(
// value = 42L,
// remainder = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
// offset = 0L,
// size = 0L
// )
// )
// )
// )
Tuples
import org.sigilaris.core.codec.byte.*
val pair = (100L, 200L)
// pair: Tuple2[Long, Long] = (100L, 200L)
val pairBytes = ByteEncoder[(Long, Long)].encode(pair)
// pairBytes: ByteVector = ByteVector(16 bytes, 0x000000000000006400000000000000c8)
val pairDecoded = ByteDecoder[(Long, Long)].decode(pairBytes)
// pairDecoded: Either[DecodeFailure, DecodeResult[Tuple2[Long, Long]]] = Right(
// value = DecodeResult(value = (100L, 200L), remainder = ByteVector(empty))
// )
Blockchain Data Structures
Transaction
import org.sigilaris.core.codec.byte.*
import java.time.Instant
case class Address(id: Long)
case class Transaction(
from: Address,
to: Address,
amount: Long,
nonce: Long,
timestamp: Instant
)
val tx = Transaction(
from = Address(1L),
to = Address(2L),
amount = 1000L,
nonce = 1L,
timestamp = Instant.parse("2024-01-01T00:00:00Z")
)
// tx: Transaction = Transaction(
// from = Address(id = 1L),
// to = Address(id = 2L),
// amount = 1000L,
// nonce = 1L,
// timestamp = 2024-01-01T00:00:00Z
// )
// Encoding
val txBytes = ByteEncoder[Transaction].encode(tx)
// txBytes: ByteVector = ByteVector(40 bytes, 0x0000000000000001000000000000000200000000000003e800000000000000010000018cc251f400)
// Decoding
val txDecoded = ByteDecoder[Transaction].decode(txBytes)
// txDecoded: Either[DecodeFailure, DecodeResult[Transaction]] = Right(
// value = DecodeResult(
// value = Transaction(
// from = Address(id = 1L),
// to = Address(id = 2L),
// amount = 1000L,
// nonce = 1L,
// timestamp = 2024-01-01T00:00:00Z
// ),
// remainder = ByteVector(empty)
// )
// )
The encoded bytes can be passed to a hashing function (in a separate crypto module) to compute the transaction ID.
Block Header
import org.sigilaris.core.codec.byte.*
import java.time.Instant
case class BlockHeader(
height: Long,
timestamp: Instant,
previousHashBytes: Long, // Simplified for this example
txCount: Long
)
val header = BlockHeader(
height = 100L,
timestamp = Instant.parse("2024-01-01T00:00:00Z"),
previousHashBytes = 0x123abcL,
txCount = 42L
)
// header: BlockHeader = BlockHeader(
// height = 100L,
// timestamp = 2024-01-01T00:00:00Z,
// previousHashBytes = 1194684L,
// txCount = 42L
// )
val headerBytes = ByteEncoder[BlockHeader].encode(header)
// headerBytes: ByteVector = ByteVector(32 bytes, 0x00000000000000640000018cc251f4000000000000123abc000000000000002a)
val headerDecoded = ByteDecoder[BlockHeader].decode(headerBytes)
// headerDecoded: Either[DecodeFailure, DecodeResult[BlockHeader]] = Right(
// value = DecodeResult(
// value = BlockHeader(
// height = 100L,
// timestamp = 2024-01-01T00:00:00Z,
// previousHashBytes = 1194684L,
// txCount = 42L
// ),
// remainder = ByteVector(empty)
// )
// )
Account State
import org.sigilaris.core.codec.byte.*
case class Account(
address: Long,
balance: BigInt,
nonce: Long
)
val account = Account(
address = 100L,
balance = BigInt("1000000000000000000"), // 1 ETH equivalent
nonce = 5L
)
// account: Account = Account(
// address = 100L,
// balance = 1000000000000000000,
// nonce = 5L
// )
val accountBytes = ByteEncoder[Account].encode(account)
// accountBytes: ByteVector = ByteVector(25 bytes, 0x0000000000000064881bc16d674ec800000000000000000005)
val accountDecoded = ByteDecoder[Account].decode(accountBytes)
// accountDecoded: Either[DecodeFailure, DecodeResult[Account]] = Right(
// value = DecodeResult(
// value = Account(address = 100L, balance = 1000000000000000000, nonce = 5L),
// remainder = ByteVector(empty)
// )
// )
Note how BigInt
is used for large balances, providing efficient variable-length encoding.
Custom Types
Opaque Types
import org.sigilaris.core.codec.byte.*
opaque type UserId = Long
object UserId:
def apply(value: Long): UserId = value
given ByteEncoder[UserId] = ByteEncoder[Long].contramap(identity)
given ByteDecoder[UserId] = ByteDecoder[Long].map(identity)
val userId: UserId = UserId(12345L)
// userId: Long = 12345L
val userIdBytes = ByteEncoder[UserId].encode(userId)
// userIdBytes: ByteVector = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtByteBuffer@264560d3,
// offset = 0L,
// size = 8L
// )
// )
val userIdDecoded = ByteDecoder[UserId].decode(userIdBytes)
// userIdDecoded: Either[DecodeFailure, DecodeResult[UserId]] = Right(
// value = DecodeResult(
// value = 12345L,
// remainder = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
// offset = 0L,
// size = 0L
// )
// )
// )
// )
Wrapper Types with Validation
import org.sigilaris.core.codec.byte.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
case class PositiveBalance(value: BigInt)
object PositiveBalance:
given ByteEncoder[PositiveBalance] =
ByteEncoder[BigInt].contramap(_.value)
given ByteDecoder[PositiveBalance] =
ByteDecoder[BigInt].emap: n =>
if n >= 0 then PositiveBalance(n).asRight
else DecodeFailure(s"Balance must be non-negative, got $n").asLeft
val validBalance = PositiveBalance(BigInt(100))
// validBalance: PositiveBalance = PositiveBalance(value = 100)
val validBytes = ByteEncoder[PositiveBalance].encode(validBalance)
// validBytes: ByteVector = ByteVector(2 bytes, 0x81c8)
ByteDecoder[PositiveBalance].decode(validBytes)
// res6: Either[DecodeFailure, DecodeResult[PositiveBalance]] = Right(
// value = DecodeResult(
// value = PositiveBalance(value = 100),
// remainder = ByteVector(empty)
// )
// )
// Negative balance encoding as BigInt (for demonstration)
val negativeBytes = ByteEncoder[BigInt].encode(BigInt(-50))
// negativeBytes: ByteVector = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtArray@3a626d5e,
// offset = 0L,
// size = 1L
// )
// )
ByteDecoder[PositiveBalance].decode(negativeBytes).isLeft
// res7: Boolean = true
Sealed Trait (Sum Types)
import org.sigilaris.core.codec.byte.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
import scodec.bits.ByteVector
sealed trait TxType
case object Transfer extends TxType
case object Deploy extends TxType
case object Call extends TxType
object TxType:
given ByteEncoder[TxType] = ByteEncoder[Byte].contramap:
case Transfer => 0
case Deploy => 1
case Call => 2
given ByteDecoder[TxType] = ByteDecoder[Byte].emap:
case 0 => Transfer.asRight
case 1 => Deploy.asRight
case 2 => Call.asRight
case n => DecodeFailure(s"Unknown transaction type: $n").asLeft
val txType: TxType = Transfer
// txType: TxType = Transfer
val txTypeBytes = ByteEncoder[TxType].encode(txType)
// txTypeBytes: ByteVector = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtArray@26ada7fb,
// offset = 0L,
// size = 1L
// )
// )
val txTypeDecoded = ByteDecoder[TxType].decode(txTypeBytes)
// txTypeDecoded: Either[DecodeFailure, DecodeResult[TxType]] = Right(
// value = DecodeResult(
// value = Transfer,
// remainder = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
// offset = 0L,
// size = 0L
// )
// )
// )
// )
Error Handling
Handling Decode Failures
import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
case class Transaction(from: Long, to: Long, amount: Long)
def processTransaction(bytes: ByteVector): Either[String, String] =
ByteDecoder[Transaction].decode(bytes) match
case Right(result) =>
val tx = result.value
Right(s"Processed: ${tx.from} -> ${tx.to}, amount ${tx.amount}")
case Left(failure) =>
Left(s"Failed to decode transaction: ${failure.msg}")
// Valid transaction
val validTx = Transaction(1L, 2L, 100L)
// validTx: Transaction = Transaction(from = 1L, to = 2L, amount = 100L)
val validBytes = ByteEncoder[Transaction].encode(validTx)
// validBytes: ByteVector = ByteVector(24 bytes, 0x000000000000000100000000000000020000000000000064)
processTransaction(validBytes)
// res10: Either[String, String] = Right(
// value = "Processed: 1 -> 2, amount 100"
// )
// Invalid bytes (insufficient data)
val invalidBytes = ByteVector(0x01, 0x02)
// invalidBytes: ByteVector = Chunk(
// bytes = View(
// at = scodec.bits.ByteVector$AtArray@2be31805,
// offset = 0L,
// size = 2L
// )
// )
processTransaction(invalidBytes)
// res11: Either[String, String] = Left(
// value = "Failed to decode transaction: Too short bytes to decode Long; required 8 bytes, but received 2 bytes: ByteVector(2 bytes, 0x0102)"
// )
Partial Decoding with Remainder
import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
// Decode multiple transactions from a single byte sequence
def decodeMultiple(bytes: ByteVector): List[Transaction] =
def go(bs: ByteVector, acc: List[Transaction]): List[Transaction] =
if bs.isEmpty then acc.reverse
else
ByteDecoder[Transaction].decode(bs) match
case Right(result) => go(result.remainder, result.value :: acc)
case Left(_) => acc.reverse // Stop on error
go(bytes, Nil)
case class Transaction(from: Long, to: Long, amount: Long)
val tx1 = Transaction(1L, 2L, 100L)
// tx1: Transaction = Transaction(from = 1L, to = 2L, amount = 100L)
val tx2 = Transaction(3L, 4L, 200L)
// tx2: Transaction = Transaction(from = 3L, to = 4L, amount = 200L)
val concatenated = ByteEncoder[Transaction].encode(tx1) ++ ByteEncoder[Transaction].encode(tx2)
// concatenated: ByteVector = ByteVector(48 bytes, 0x0000000000000001000000000000000200000000000000640000000000000003000000000000000400000000000000c8)
decodeMultiple(concatenated)
// res13: List[Transaction] = List(
// Transaction(from = 1L, to = 2L, amount = 100L),
// Transaction(from = 3L, to = 4L, amount = 200L)
// )
Roundtrip Testing
Property-Based Testing
Example using hedgehog-munit for roundtrip testing:
import org.sigilaris.core.codec.byte.*
import hedgehog.*
import hedgehog.munit.HedgehogSuite
class TransactionCodecSuite extends HedgehogSuite:
property("Transaction roundtrip"):
for
from <- Gen.long(Range.linear(0L, 1000L))
to <- Gen.long(Range.linear(0L, 1000L))
amount <- Gen.long(Range.linear(0L, 1000000L))
yield
val tx = Transaction(from, to, amount)
val encoded = ByteEncoder[Transaction].encode(tx)
val decoded = ByteDecoder[Transaction].decode(encoded)
decoded ==== Right(DecodeResult(tx, ByteVector.empty))
Simple Unit Tests
import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
case class User(id: Long, balance: BigInt)
def testRoundtrip[A](value: A)(using enc: ByteEncoder[A], dec: ByteDecoder[A]): Boolean =
val encoded = enc.encode(value)
val decoded = dec.decode(encoded)
decoded match
case Right(result) => result.value == value && result.remainder.isEmpty
case Left(_) => false
val user = User(1L, BigInt(1000))
// user: User = User(id = 1L, balance = 1000)
testRoundtrip(user)
// res15: Boolean = true
// Option example
val maybeUser = Option(User(1L, BigInt(100)))
// maybeUser: Option[User] = Some(value = User(id = 1L, balance = 100))
testRoundtrip[Option[User]](maybeUser)
// res16: Boolean = true
// Set example with explicit type
val userSet = Set(User(1L, BigInt(100)), User(2L, BigInt(200)))
// userSet: Set[User] = Set(
// User(id = 1L, balance = 100),
// User(id = 2L, balance = 200)
// )
testRoundtrip[Set[User]](userSet)
// res17: Boolean = true
Advanced Patterns
Versioned Encoding
import org.sigilaris.core.codec.byte.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
case class VersionedTransaction(version: Byte, data: Transaction)
case class Transaction(from: Long, to: Long, amount: Long)
object VersionedTransaction:
given ByteEncoder[VersionedTransaction] = new ByteEncoder[VersionedTransaction]:
def encode(vt: VersionedTransaction) =
ByteEncoder[Byte].encode(vt.version) ++ ByteEncoder[Transaction].encode(vt.data)
given ByteDecoder[VersionedTransaction] =
ByteDecoder[Byte].flatMap: version =>
if version == 1 then
ByteDecoder[Transaction].map(data => VersionedTransaction(version, data))
else
new ByteDecoder[VersionedTransaction]:
def decode(bytes: scodec.bits.ByteVector) =
Left(DecodeFailure(s"Unsupported version: $version"))
val vTx = VersionedTransaction(1, Transaction(1L, 2L, 100L))
// vTx: VersionedTransaction = VersionedTransaction(
// version = 1,
// data = Transaction(from = 1L, to = 2L, amount = 100L)
// )
val vTxBytes = ByteEncoder[VersionedTransaction].encode(vTx)
// vTxBytes: ByteVector = ByteVector(25 bytes, 0x01000000000000000100000000000000020000000000000064)
val vTxDecoded = ByteDecoder[VersionedTransaction].decode(vTxBytes)
// vTxDecoded: Either[DecodeFailure, DecodeResult[VersionedTransaction]] = Right(
// value = DecodeResult(
// value = VersionedTransaction(
// version = 1,
// data = Transaction(from = 1L, to = 2L, amount = 100L)
// ),
// remainder = ByteVector(empty)
// )
// )
Collection of Different Types
import org.sigilaris.core.codec.byte.*
case class Block(
height: Long,
txHashes: Set[Long],
metadata: Map[Long, Long] // key: metadata type ID, value: metadata value
)
val block = Block(
height = 100L,
txHashes = Set(0xabcL, 0xdefL, 0x123L),
metadata = Map(1L -> 1672531200L, 2L -> 21000L) // 1: timestamp, 2: gasUsed
)
// block: Block = Block(
// height = 100L,
// txHashes = Set(2748L, 3567L, 291L),
// metadata = Map(1L -> 1672531200L, 2L -> 21000L)
// )
val blockBytes = ByteEncoder[Block].encode(block)
// blockBytes: ByteVector = ByteVector(66 bytes, 0x00000000000000640300000000000001230000000000000abc0000000000000def0200000000000000010000000063b0cd0000000000000000020000000000005208)
val blockDecoded = ByteDecoder[Block].decode(blockBytes)
// blockDecoded: Either[DecodeFailure, DecodeResult[Block]] = Right(
// value = DecodeResult(
// value = Block(
// height = 100L,
// txHashes = Set(291L, 2748L, 3567L),
// metadata = Map(1L -> 1672531200L, 2L -> 21000L)
// ),
// remainder = ByteVector(empty)
// )
// )
Note: Set
and Map
are automatically sorted deterministically during encoding.
Nested Structures
import org.sigilaris.core.codec.byte.*
case class Address(id: Long)
case class Transaction(from: Address, to: Address, amount: Long)
case class Block(height: Long, txCount: Long, lastTxAmount: Long)
// Simplified example - in practice, use Set or custom collection
val block = Block(
height = 100L,
txCount = 2L,
lastTxAmount = 200L
)
// block: Block = Block(height = 100L, txCount = 2L, lastTxAmount = 200L)
val blockBytes = ByteEncoder[Block].encode(block)
// blockBytes: ByteVector = ByteVector(24 bytes, 0x0000000000000064000000000000000200000000000000c8)
val blockDecoded = ByteDecoder[Block].decode(blockBytes)
// blockDecoded: Either[DecodeFailure, DecodeResult[Block]] = Right(
// value = DecodeResult(
// value = Block(height = 100L, txCount = 2L, lastTxAmount = 200L),
// remainder = ByteVector(empty)
// )
// )
Note: List[A]
encoder exists but decoder is not yet implemented. Use Set[A]
, Option[A]
, or Map[K, V]
for collections.
Integration Example
Complete Transaction Pipeline
import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
import java.time.Instant
case class Transaction(
from: Long,
to: Long,
amount: Long,
nonce: Long,
timestamp: Instant
)
// Step 1: Create transaction
val tx = Transaction(
from = 100L,
to = 200L,
amount = 5000L,
nonce = 1L,
timestamp = Instant.now()
)
// Step 2: Encode to bytes
val txBytes = ByteEncoder[Transaction].encode(tx)
txBytes
// res22: ByteVector = ByteVector(40 bytes, 0x000000000000006400000000000000c80000000000001388000000000000000100000199d350784c)
// Step 3: These bytes would then be:
// - Hashed to create transaction ID (using separate crypto module)
// - Signed using private key (using separate crypto module)
// - Broadcast to network (using separate network module)
// Step 4: Receiving node decodes
val received = ByteDecoder[Transaction].decode(txBytes)
// received: Either[DecodeFailure, DecodeResult[Transaction]] = Right(
// value = DecodeResult(
// value = Transaction(
// from = 100L,
// to = 200L,
// amount = 5000L,
// nonce = 1L,
// timestamp = 2025-10-11T12:48:10.316Z
// ),
// remainder = ByteVector(empty)
// )
// )
received.map(_.value)
// res23: Either[DecodeFailure, Transaction] = Right(
// value = Transaction(
// from = 100L,
// to = 200L,
// amount = 5000L,
// nonce = 1L,
// timestamp = 2025-10-11T12:48:10.316Z
// )
// )
Best Practices Summary
- Use Case Classes: Leverage automatic derivation for product types
- Validate in Decoders: Use
emap
to add validation logic - Test Roundtrips: Ensure
decode(encode(x)) == x
- Handle Errors: Pattern match on
Either
for decode results - Separate Concerns: Keep codec separate from hashing/signing/networking
- Deterministic Collections: Trust
Set
andMap
automatic sorting - Use BigInt for Large Numbers: Benefit from variable-length encoding
← Codec Overview | API | Type Rules | Examples | RLP Comparison