실전 예제

← 코덱 개요 | API | 타입 규칙 | 예제 | RLP 비교


개요

이 문서는 블록체인 애플리케이션에서 바이트 코덱을 사용하는 실제 예제를 제공하며, 일반적인 패턴과 사용 사례를 보여줍니다.

기본 사용법

간단한 데이터 인코딩

import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector

// 기본 타입 인코딩
val longValue = 42L
val longBytes = ByteEncoder[Long].encode(longValue)
// longBytes: ByteVector = Chunk(
//   bytes = View(
//     at = scodec.bits.ByteVector$AtByteBuffer@308420d8,
//     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
//       )
//     )
//   )
// )

튜플

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

블록체인 데이터 구조

트랜잭션

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

// 인코딩
val txBytes = ByteEncoder[Transaction].encode(tx)
// txBytes: ByteVector = ByteVector(40 bytes, 0x0000000000000001000000000000000200000000000003e800000000000000010000018cc251f400)

// 디코딩
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)
//   )
// )

인코딩된 바이트는 (별도의 crypto 모듈에서) 해싱 함수로 전달되어 트랜잭션 ID를 계산할 수 있습니다.

블록 헤더

import org.sigilaris.core.codec.byte.*
import java.time.Instant

case class BlockHeader(
  height: Long,
  timestamp: Instant,
  previousHashBytes: Long,  // 이 예제에서는 단순화
  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)
//   )
// )

계정 상태

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 상당
  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)
//   )
// )

BigInt가 큰 잔액에 사용되어 효율적인 가변 길이 인코딩을 제공하는 것에 주목하세요.

커스텀 타입

Opaque 타입

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@19ab47df,
//     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
//       )
//     )
//   )
// )

검증이 있는 래퍼 타입

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

// 음수 잔액을 BigInt로 인코딩 (데모용)
val negativeBytes = ByteEncoder[BigInt].encode(BigInt(-50))
// negativeBytes: ByteVector = Chunk(
//   bytes = View(
//     at = scodec.bits.ByteVector$AtArray@2fb5ea5,
//     offset = 0L,
//     size = 1L
//   )
// )
ByteDecoder[PositiveBalance].decode(negativeBytes).isLeft
// res7: Boolean = true

Sealed Trait (Sum 타입)

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@23909918,
//     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
//       )
//     )
//   )
// )

에러 처리

디코딩 실패 처리

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}")
// 유효한 트랜잭션
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"
// )

// 잘못된 바이트 (데이터 부족)
val invalidBytes = ByteVector(0x01, 0x02)
// invalidBytes: ByteVector = Chunk(
//   bytes = View(
//     at = scodec.bits.ByteVector$AtArray@3283ff52,
//     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)"
// )

나머지를 사용한 부분 디코딩

import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector

// 단일 바이트 시퀀스에서 여러 트랜잭션 디코딩
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  // 에러 발생 시 중단

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

라운드트립 테스트

Property-Based 테스트

hedgehog-munit을 사용한 라운드트립 테스트 예제:

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

간단한 단위 테스트

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 예제
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 예제
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

고급 패턴

버전이 있는 인코딩

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

서로 다른 타입의 컬렉션

import org.sigilaris.core.codec.byte.*

case class Block(
  height: Long,
  txHashes: Set[Long],
  metadata: Map[Long, Long]  // key: metadata 타입 ID, value: metadata 값
)
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)
//   )
// )

참고: SetMap은 인코딩 중에 자동으로 결정론적으로 정렬됩니다.

중첩된 구조

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)
// 단순화된 예제 - 실제로는 Set이나 커스텀 컬렉션 사용
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)
//   )
// )

참고: List[A] encoder는 존재하지만 decoder는 아직 구현되지 않았습니다. 컬렉션에는 Set[A], Option[A], Map[K, V]를 사용하세요.

통합 예제

완전한 트랜잭션 파이프라인

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
)

// 단계 1: 트랜잭션 생성
val tx = Transaction(
  from = 100L,
  to = 200L,
  amount = 5000L,
  nonce = 1L,
  timestamp = Instant.now()
)

// 단계 2: 바이트로 인코딩
val txBytes = ByteEncoder[Transaction].encode(tx)
txBytes
// res22: ByteVector = ByteVector(40 bytes, 0x000000000000006400000000000000c80000000000001388000000000000000100000199d350e9a5)

// 단계 3: 이 바이트는 다음과 같이 처리됨:
// - 트랜잭션 ID 생성을 위한 해싱 (별도 crypto 모듈 사용)
// - 개인키를 사용한 서명 (별도 crypto 모듈 사용)
// - 네트워크로 브로드캐스트 (별도 네트워크 모듈 사용)

// 단계 4: 수신 노드가 디코딩
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:39.333Z
//     ),
//     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:39.333Z
//   )
// )

모범 사례 요약

  1. Case Class 사용: product 타입에 대한 자동 derivation 활용
  2. Decoder에서 검증: emap을 사용하여 검증 로직 추가
  3. 라운드트립 테스트: decode(encode(x)) == x 확인
  4. 에러 처리: 디코딩 결과에 대해 Either 패턴 매칭 사용
  5. 관심사 분리: 코덱을 해싱/서명/네트워킹과 분리 유지
  6. 결정론적 컬렉션: SetMap 자동 정렬 신뢰
  7. 큰 숫자에는 BigInt 사용: 가변 길이 인코딩의 이점 활용

← 코덱 개요 | API | 타입 규칙 | 예제 | RLP 비교