실전 예제
← 코덱 개요 | 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)
// )
// )
참고: Set
과 Map
은 인코딩 중에 자동으로 결정론적으로 정렬됩니다.
중첩된 구조
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
// )
// )
모범 사례 요약
- Case Class 사용: product 타입에 대한 자동 derivation 활용
- Decoder에서 검증:
emap
을 사용하여 검증 로직 추가 - 라운드트립 테스트:
decode(encode(x)) == x
확인 - 에러 처리: 디코딩 결과에 대해
Either
패턴 매칭 사용 - 관심사 분리: 코덱을 해싱/서명/네트워킹과 분리 유지
- 결정론적 컬렉션:
Set
과Map
자동 정렬 신뢰 - 큰 숫자에는 BigInt 사용: 가변 길이 인코딩의 이점 활용
← 코덱 개요 | API | 타입 규칙 | 예제 | RLP 비교