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

  1. Use Case Classes: Leverage automatic derivation for product types
  2. Validate in Decoders: Use emap to add validation logic
  3. Test Roundtrips: Ensure decode(encode(x)) == x
  4. Handle Errors: Pattern match on Either for decode results
  5. Separate Concerns: Keep codec separate from hashing/signing/networking
  6. Deterministic Collections: Trust Set and Map automatic sorting
  7. Use BigInt for Large Numbers: Benefit from variable-length encoding

← Codec Overview | API | Type Rules | Examples | RLP Comparison