
Solana 트랜잭션 버전: 레거시, v0, v1
Solana 트랜잭션 형식의 간략한 역사
Solana에는 레거시, v0, v1이라는 세 가지 트랜잭션 형식이 있습니다. 큰 틀에서 모두 같은 목적을 수행합니다. 검증인이 온체인 프로그램을 대상으로 실행할 서명, 계정, 명령을 패키징합니다.
시간이 지나며 달라진 것은 와이어 형식입니다. 즉, 이러한 정보 조각을 직렬화하고 네트워크로 전송하는 방식입니다.
각각의 새 형식은 Solana 애플리케이션이 복잡해지면서 드러난 한계를 해결하기 위해 도입됐습니다. 동시에 이전 형식이 계속 작동하도록 하위 호환성을 유지했습니다.
세 형식은 모두 네트워크에 공존하며, 개발자는 구축 중인 트랜잭션에 가장 적합한 형식을 사용할 수 있습니다.
레거시 트랜잭션
최초 형식은 트랜잭션 버전 관리보다 먼저 만들어졌고 버전 식별자가 없기 때문에 레거시라고 부릅니다.
최대 크기인 1,232바이트는 Solana의 초기 네트워킹 설계에서 비롯됐습니다. 트랜잭션이 IPv6 최소 MTU인 1,280바이트 안에 들어가도록 했으며, 네트워크 오버헤드를 제외하면 1,232바이트가 남았습니다.
작은 트랜잭션에는 적합했지만 애플리케이션이 성장하면서 공간 제약이 심해졌습니다. 트랜잭션에 직접 포함된 각 계정 주소는 32바이트, 각 Ed25519 서명은 추가로 64바이트를 차지합니다.
따라서 실제로는 계정이 많은 트랜잭션이 런타임의 계정 잠금 한도에 도달하기 훨씬 전에 바이트 한도를 소진할 수 있었습니다.
트랜잭션 v0
이 부담을 완화하려는 첫 시도가 트랜잭션 v0입니다. 2022년 10월 mainnet-beta에 적용됐습니다. v0은 1,232바이트 한도를 늘리는 대신 **Address Lookup Tables(ALTs)**를 도입했습니다.
ALT는 계정 주소를 온체인에 저장합니다. 따라서 v0 트랜잭션은 각 32바이트 공개 키를 반복하는 대신 1바이트 인덱스로 참조할 수 있습니다. 기존 패킷 크기 제약을 지키면서 런타임의 계정 64개 한도까지 사용할 수 있습니다.
v0은 레거시 형식을 점진적으로 확장했습니다. 동일한 기본 메시지와 명령 인코딩을 유지하면서 0x80 버전 접두사와 주소 조회 섹션을 추가했습니다.
버전 관리의 도입으로 새 트랜잭션 형식이 레거시 트랜잭션을 대체하지 않고 공존할 수 있는 프레임워크도 마련됐습니다.
ALT는 당장의 계정 크기 문제를 해결했지만 새로운 복잡성을 만들었습니다. 애플리케이션은 온체인 조회 테이블을 생성하고 유지해야 하며, 검증인은 실행 전에 항목을 해석해야 합니다. RPC 서비스와 인덱서도 전체 트랜잭션을 재구성하려면 해석된 주소가 필요합니다.
원시 트랜잭션이 참조하는 조회 테이블이 나중에 닫히면 과거 데이터 디코딩도 훨씬 어려워집니다. 따라서 v0은 계정 목록을 효과적으로 압축했지만, 트랜잭션의 와이어 표현에 외부 온체인 의존성을 추가하는 방식이었습니다.
ALT는 Solana 개발자 사이에서 널리 채택됐습니다. v1 트랜잭션 도입 직전에 수행된 연구에 따르면 v0 트랜잭션의 약 62%가 하나 이상의 ALT를 참조합니다.
우선순위 수수료의 사후 도입
우선순위 수수료도 v0 트랜잭션이 메인넷에서 활성화되기 전인 2022년에 Solana에 도입됐습니다. 다만 당시 v0 형식 자체는 이미 설계된 상태였습니다. Solana는 두 트랜잭션 형식 중 하나를 재설계하는 대신 기존 Compute Budget Program을 통해 수수료 구성을 추가했습니다. 트랜잭션에 SetComputeUnitLimit 및 SetComputeUnitPrice 같은 명령을 포함해 컴퓨트 예산을 요청하고 스케줄링 우선순위를 위한 추가 수수료를 지정할 수 있었습니다. 형식별 수수료 메커니즘을 따로 만들지 않고도 레거시와 v0 트랜잭션 모두에서 동일하게 작동했습니다.
실용적이고 하위 호환되는 해결책이었지만 특별히 세련되지는 않았습니다. 우선순위 수수료와 컴퓨트 한도는 실제로 트랜잭션 수준의 메타데이터입니다. 하지만 레거시와 v0에는 전용 필드가 없어 프로그램 호출 형태로 명령 스트림에 사후 추가됐습니다. 따라서 트랜잭션이 Compute Budget Program을 참조해야 하고, 구성에 명령 바이트가 사용되며, 검증인은 트랜잭션 수집 중 해당 명령을 검사해 수수료와 리소스 설정을 복원해야 합니다.
따라서 v0은 Solana 트랜잭션 형식을 폭넓게 재설계한 것이 아닙니다. 핵심 목표는 레거시 메시지 구조 대부분을 유지하면서 Address Lookup Tables로 계정 주소 병목을 해결하는 것이었습니다. Compute Budget 명령이 이미 두 형식 모두에서 우선순위 수수료를 지원하는 호환 방식을 제공했으므로 v0의 범위를 더 확장할 이유는 거의 없었습니다.
더 큰 트랜잭션 크기
시간이 지나면서 Solana는 트랜잭션 수집 방식을 UDP에서 QUIC으로 전환했습니다. QUIC 스트림은 더 이상 단일 MTU 크기 페이로드로 제한되지 않습니다. 따라서 기존의 1,232바이트 상한은 더 이상 전송 요건이 아니었습니다. 2025년에는 새 v1 형식을 도입하기 위한 두 가지 SIMD가 제안됐습니다. SIMD-0296은 v1 트랜잭션의 최대 크기를 이전 한도의 약 3.3배인 4,096바이트로 높였습니다. SIMD-0385는 추가 공간과 더 효율적인 검증인 수집을 중심으로 설계된 완전히 새로운 와이어 형식을 정의했습니다.
추가 공간 덕분에 v1은 ALT를 완전히 제거할 수 있습니다. 32바이트 주소 64개의 한도에는 최대 2,048바이트가 필요하므로 전체 계정 목록을 인라인으로 담을 수 있습니다.
Solana Foundation 분석에 따르면 기존 v0 트랜잭션 대부분을 업그레이드할 때 모든 주소를 인라인화해도 영향은 크지 않습니다. 50%는 420바이트 미만, 90%는 1,400바이트 미만 증가합니다. ALT를 많이 사용하는 트랜잭션도 v1의 4,096바이트 한도 안에서 충분한 여유를 확보합니다.
더 중요한 점은 v1이 단순히 더 큰 v0 트랜잭션이 아니라는 것입니다. 와이어 형식을 대폭 재구성했습니다. 서명은 끝으로 이동하고, 리소스 구성은 Compute Budget 명령에서 트랜잭션 구성 섹션으로 이동하며, 가변 길이 명령 페이로드는 고정 너비 헤더와 분리됩니다. 이러한 변경 덕분에 검증인이 트랜잭션을 더 저렴하고 간단하게 파싱하고 우선순위를 지정할 수 있습니다.
더 커진 4,096바이트 한도는 주소 압축만으로는 지원할 수 없었던 워크로드도 가능하게 합니다. 영지식 증명, 중첩 멀티시그, 잘리지 않은 Winternitz 서명, BLS 서명 및 기타 암호화 연산에는 수백 또는 수천 바이트의 명령 데이터가 필요할 수 있습니다. 예를 들어 Token-2022 Confidential Balances는 이제 여러 트랜잭션으로 나누지 않고 하나의 원자적 v1 트랜잭션으로 구성할 수 있습니다.
특히 초기 v1 형식은 Solana의 컴퓨트, 서명, 명령 수 또는 계정 64개 한도를 높이지 않습니다. 주된 목적은 크기 병목을 제거하고 트랜잭션의 와이어 표현 방식을 재설계하는 것입니다.
Anza가 제안한 SIMD-0596은 v1 계정 잠금 한도를 64개에서 96개로 높입니다. 여기에는 서명자, 프로그램 ID, 쓰기 가능 및 읽기 전용 계정 등 트랜잭션이 참조하는 모든 계정이 포함됩니다. 작성 시점에는 아직 검토 중입니다. 레거시와 v0 트랜잭션은 64개 한도를 유지합니다.
형식 사양 및 비교
| 한도 | 레거시 | v0 | v1 |
| 최대 크기 | 1,232바이트 | 1,232바이트 | 4,096바이트 |
| 계정 주소 | 약 32개, 크기에 따라 제한 | 64개, 조회 테이블 사용 | 64개, 인라인 |
| Address Lookup Tables | 지원하지 않음 | 지원 | 지원하지 않음 |
| 중복 주소 | 허용 | 허용 | 거부 |
| 접두사 | 없음 | 0x80 | 0x81 |
| 우선순위 수수료 | SetComputeUnitPrice 명령, CU당 micro-lamports | SetComputeUnitPrice 명령, CU당 micro-lamports | config.priorityFeeLamports, 총 lamports |
| 컴퓨트 유닛 한도 | SetComputeUnitLimit 명령 | SetComputeUnitLimit 명령 | config.computeUnitLimit |
| 힙 크기 | RequestHeapFrame 명령 | RequestHeapFrame 명령 | config.heapSize |
| 로드된 계정 한도 | SetLoadedAccountsDataSizeLimit 명령 | SetLoadedAccountsDataSizeLimit 명령 | config.loadedAccountsDataSizeLimit |
레거시 트랜잭션 사양
직렬화된 레거시 트랜잭션은 compact-u16 서명 개수로 시작하며, 그 뒤에 64바이트 Ed25519 서명이 이어집니다.
그다음에는 메시지가 옵니다. 메시지에는 3바이트 헤더, compact 길이 접두사가 붙은 계정 목록, 32바이트 최근 블록해시, compact 길이 접두사가 붙은 컴파일된 명령 배열이 포함됩니다.
레거시 트랜잭션에는 버전 접두사가 없습니다.
NumSignatures (compact-u16)
Signatures [[u8; 64]]
-- Length = NumSignatures
MessageHeader (u8, u8, u8)
-- (NumRequiredSignatures,
NumReadonlySignedAccounts,
NumReadonlyUnsignedAccounts)
NumAccountKeys (compact-u16)
AccountKeys [[u8; 32]]
-- Length = NumAccountKeys
RecentBlockhash [u8; 32]
NumInstructions (compact-u16)
Instructions [CompiledInstruction]
-- Length = NumInstructions
CompiledInstruction:
ProgramIdIndex (u8)
NumInstructionAccounts (compact-u16)
InstructionAccountIndexes [u8]
-- Length = NumInstructionAccounts
InstructionDataLength (compact-u16)
InstructionData [u8]
-- Length = InstructionDataLength핵심 특징은 다음 명령이 시작되기 전에 각 CompiledInstruction이 완전히 직렬화된다는 점입니다.
계정 인덱스 및 데이터 배열은 가변 길이이며, compact-u16 접두사가 크기를 정의합니다.
트랜잭션 v0 사양
트랜잭션 v0은 레거시 와이어 형식의 거의 전부를 유지합니다. 트랜잭션은 여전히 서명 개수와 서명으로 시작하지만, 메시지는 버전 접두사 0x80로 시작합니다.
그다음 메시지는 레거시와 동일한 3바이트 헤더, 정적 계정 목록, 블록해시, 컴파일된 명령 형식을 사용하고 마지막에 Address Lookup Table 섹션을 추가합니다.
이 덕분에 v0 트랜잭션은 1,232바이트 트랜잭션 한도를 지키면서 ALT를 통해 추가 계정을 로드할 수 있습니다.
NumSignatures (compact-u16)
Signatures [[u8; 64]]
-- Length = NumSignatures
VersionByte (u8)
-- 0x80
MessageHeader (u8, u8, u8)
NumStaticAccountKeys (compact-u16)
StaticAccountKeys [[u8; 32]]
-- Length = NumStaticAccountKeys
RecentBlockhash [u8; 32]
NumInstructions (compact-u16)
Instructions [CompiledInstruction]
-- Same encoding as legacy
NumAddressTableLookups (compact-u16)
AddressTableLookups [MessageAddressTableLookup]
-- Length = NumAddressTableLookups
MessageAddressTableLookup:
AccountKey [u8; 32]
-- Address of the lookup table account
NumWritableIndexes (compact-u16)
WritableIndexes [u8]
-- Indices into the lookup table
NumReadonlyIndexes (compact-u16)
ReadonlyIndexes [u8]
-- Indices into the lookup table정적 계정 키와 로드된 주소의 구분은 중요합니다. 정적 키만 메시지에 직접 직렬화됩니다. ALT를 통해 참조한 주소는 런타임에 해석되어 실제 계정 목록에 추가됩니다.
ALT를 사용하는 v0 트랜잭션의 경우 검증인은 먼저 Bank와 AccountsDB에서 참조된 각 조회 테이블을 로드해야 합니다. 해당 계정이 유효한 Address Lookup Table인지 확인하고, 상태를 역직렬화하고, 요청된 인덱스를 전체 32바이트 주소로 해석한 다음, 그 주소를 트랜잭션의 정적 계정 키와 병합합니다. 이 해석 단계가 끝나야 읽기/쓰기 잠금과 다른 트랜잭션과의 충돌을 판단하는 데 필요한 전체 계정 목록을 확보할 수 있습니다.
트랜잭션 v1 사양
직렬화된 v1 트랜잭션은 버전 바이트 0x81로 시작합니다. 이어서 3바이트 레거시 스타일 메시지 헤더, 32비트 트랜잭션 구성 마스크, 32바이트 수명 지정자, 명령 및 주소 개수, 전체 32바이트 주소 배열, 구성 값, 고정 크기 명령 헤더, 연속된 명령 페이로드, 마지막으로 서명이 배치됩니다.
VersionByte (u8)
-- 0x81
LegacyHeader (u8, u8, u8)
TransactionConfigMask (u32)
-- Bitmask describing which configuration values are present
LifetimeSpecifier [u8; 32]
NumInstructions (u8)
NumAddresses (u8)
Addresses [[u8; 32]]
-- Length = NumAddresses
ConfigValues [[u8; 4]]
-- Length = popcount(TransactionConfigMask)
-- Multi-word values, such as priority fee, consume multiple entries
InstructionHeaders [(u8, u8, u16)]
-- Length = NumInstructions
-- (ProgramAccountIndex,
NumInstructionAccounts,
NumInstructionDataBytes)
InstructionPayloads [InstructionPayload]
-- Length = NumInstructions
InstructionPayload:
InstructionAccountIndexes [u8]
-- Length = NumInstructionAccounts
InstructionData [u8]
-- Length = NumInstructionDataBytes
Signatures [[u8; 64]]
-- Length = LegacyHeader.NumRequiredSignatures따라서 v1은 이전 형식과 몇 가지 근본적인 차이가 있습니다. 버전 식별자가 0번 바이트에 있고, 서명이 끝으로 이동해 별도의 길이 접두사가 필요하지 않습니다. 계정 및 명령 개수는 고정 너비 u8 값이 되고, 리소스 구성이 트랜잭션에 직접 인코딩되며, 고정 크기 명령 헤더는 가변 길이 페이로드와 분리됩니다. 레거시 및 v0과 달리 v1은 Address Lookup Tables를 사용하지 않습니다.
트랜잭션 v1은 Compute Budget 프로그램 구성 명령을 트랜잭션 헤더의 필드로 대체합니다. 초기 TransactionConfigMask에서 다음 항목을 선언할 수 있습니다.
- lamports 단위의 총 우선순위 수수료
- 컴퓨트 유닛 한도
- 로드된 계정 데이터 크기 한도
- 요청한 힙 크기
설정된 각 비트는 함께 제공되는 4바이트 구성 값을 식별합니다. 64비트 우선순위 수수료는 마스크 위치 두 개를 차지합니다. 이 마스크는 향후 트랜잭션 버전에서 확장할 수 있도록 설계됐습니다.
트랜잭션 예시
Solana의 여러 트랜잭션 형식을 비교하기 위해 가장 단순하면서 유용한 예시인 주소 간 SOL 전송을 살펴보겠습니다.
예시 트랜잭션에는 서명자 한 명, 즉 발신자가 있습니다. System Program을 한 번 호출해 수신자에게 1,000 lamports를 전송합니다. 발신자, 수신자, System Program의 세 주소를 참조합니다.
레거시 및 v0 트랜잭션
레거시와 v0 트랜잭션은 최상위 레이아웃이 거의 같습니다. 둘 다 서명 개수로 시작하고 바로 뒤에 서명이 옵니다. 그다음에는 트랜잭션 메시지가 이어집니다. 단일 서명자 전송에서는 첫 번째 바이트가 서명 하나를 지정하는 01이고, 그 뒤에 발신자의 64바이트 Ed25519 서명이 옵니다.
메시지에는 3바이트 헤더, 계정 주소, 최근 블록해시(수명 지정자), 컴파일된 명령이 포함됩니다.
발신자는 주소 0, 수신자는 주소 1, System Program은 주소 2입니다. 전송 명령은 인덱스 2의 프로그램을 참조하고 계정 인덱스 00 01을 프로그램에 전달합니다.
레거시와 v0 형식의 차이는 미미합니다.
- 레거시 메시지는 3바이트 메시지 헤더로 바로 시작하지만, v0 메시지는 버전 바이트
0x80로 시작한 뒤 헤더가 이어집니다. - v0은 명령 뒤에 Address Lookup Table 섹션도 추가합니다. 이 간단한 예시는 조회 테이블을 사용하지 않으므로 이 섹션에는 조회 항목이 0개임을 나타내는
00바이트 하나만 있습니다.
따라서 이 SOL 전송 예시의 크기는 레거시 트랜잭션에서 215바이트, v0에서 217바이트입니다. v0에는 버전 접두사 1바이트와 빈 조회 테이블 배열 1바이트가 추가됩니다. 나머지 트랜잭션은 동일합니다.
발신자는 메시지에만 서명합니다. 레거시 예시에서는 서명이 65~214바이트를 포함합니다. v0에서는 0x80 메시지 버전 접두사로 시작하는 65~216바이트를 포함합니다.
아래 표는 v0 트랜잭션 형식을 사용해 두 주소 사이에서 SOL을 전송하는 예시의 바이트 레이아웃을 자세히 보여줍니다.
| 오프셋 | 크기 | 필드 | 예시 값 | 설명 |
| 0 | 1 B | 서명 개수 | 01 | 서명 1개, compact-u16 |
| 1–64 | 64 B | 서명 0 | <Ed25519 signature> | 발신자 서명 |
| 65 | 1 B | 버전 접두사 | 80 | 버전 메시지, 버전 0 |
| 66–68 | 3 B | 메시지 헤더 | 01 00 01 | 필수 서명자 1명, 읽기 전용 서명 계정 0개, 읽기 전용 미서명 계정 1개 |
| 69 | 1 B | 정적 계정 개수 | 03 | 인라인 계정 3개 |
| 70–101 | 32 B | 계정 0 | <sender pubkey> | 발신자/수수료 지불자, 쓰기 가능한 서명자 |
| 102–133 | 32 B | 계정 1 | <recipient pubkey> | 수신자, 쓰기 가능한 미서명 계정 |
| 134–165 | 32 B | 계정 2 | 11111111111111111111111111111111 | System Program, 읽기 전용 미서명 계정 |
| 166–197 | 32 B | 최근 블록해시 | <recent blockhash> | 트랜잭션 수명 |
| 198 | 1 B | 명령 개수 | 01 | 명령 1개 |
| 199 | 1 B | 프로그램 ID 인덱스 | 02 | System Program |
| 200 | 1 B | 명령 계정 개수 | 02 | 명령 계정 2개 |
| 201–202 | 2 B | 계정 인덱스 | 00 01 | 발신자, 수신자 |
| 203 | 1 B | 데이터 길이 | 0C | 12바이트 |
| 204–207 | 4 B | 전송 판별자 | 02 00 00 00 | SystemInstruction::Transfer |
| 208–215 | 8 B | Lamports | E8 03 00 00 00 00 00 00 | 1,000 lamports |
| 216 | 1 B | 주소 테이블 조회 개수 | 00 | ALT 조회 없음 |
| 합계 | 217 B |
리소스 구성
레거시와 v0 트랜잭션에서는 리소스 구성을 Compute Budget Program 명령으로 표현합니다. 일반적으로 SetComputeUnitLimit 및 SetComputeUnitPrice을 사용합니다. 필요하면 SetLoadedAccountsDataSizeLimit 및 RequestHeapFrame도 사용합니다.
최소 구성 트랜잭션 예시에는 이러한 명령이 없습니다. 대신 런타임이 기본값을 제공합니다.
컴퓨트 유닛 가격이 없으면 우선순위 수수료도 없으며, 로드된 계정 데이터 한도는 런타임 최댓값이 기본입니다.
트랜잭션 수집 중 검증인이 명령 목록에서 Compute Budget 명령을 스캔해야 하는 비효율성은 v1 형식을 개선한 동기 중 하나입니다. v1에서는 이 정보가 대신 TransactionConfigMask 및 ConfigValues로 이동합니다.
애초에 트랜잭션 수준 구성을 명령으로 표현하는 방식 자체에도 오버헤드가 있습니다. 레거시와 v0에는 요청한 컴퓨트 또는 우선순위 수수료를 위한 전용 필드가 없었습니다. 따라서 이러한 설정은 Compute Budget Program을 통해 사후 추가됐습니다. 트랜잭션이 해당 프로그램 ID를 참조해야 하고, 각 설정이 명령 목록의 공간을 차지하며, 프로그램 호출 자체는 애플리케이션 작업을 수행하지 않으면서도 컴퓨트를 소모합니다.
v1은 이러한 값에 와이어 형식 내의 네이티브 위치를 제공합니다. 추가 명령 오버헤드를 피하고 검증인이 리소스 구성을 더 저렴하고 쉽게 검사할 수 있습니다.
트랜잭션 v1
v1 트랜잭션은 서명 개수와 서명으로 시작하지 않고 버전 바이트 0x81로 바로 시작합니다.
그다음에는 3바이트 헤더, 새로운 4바이트 TransactionConfigMask, 수명 지정자(일반적으로 블록해시지만 nonce일 수도 있음), 명령 및 주소 개수, 계정 주소, 구성 값, 명령, 마지막으로 서명이 이어집니다.
| 오프셋 | 크기 | 필드 | 예시 값 | 설명 |
| 0 | 1 B | 버전 바이트 | 81 | v1 트랜잭션 |
| 1–3 | 3 B | 레거시 헤더 | 01 00 01 | 필수 서명 1개, 읽기 전용 서명 계정 0개, 읽기 전용 미서명 계정 1개 |
| 4–7 | 4 B | 트랜잭션 구성 마스크 | 0C 00 00 00 | 0x0000000C: 비트 2와 3이 설정되어 4바이트 구성 값 2개를 지정 |
| 8–39 | 32 B | 수명 지정자 | <recent blockhash> | 최근 블록해시(또는 nonce) |
| 40 | 1 B | 명령 개수 | 01 | System Program 명령 1개 |
| 41 | 1 B | 주소 개수 | 03 | 발신자, 수신자, System Program |
| 42–73 | 32 B | 주소 0 | <sender pubkey> | 발신자, 수수료 지불자, 쓰기 가능한 서명자 |
| 74–105 | 32 B | 주소 1 | <recipient pubkey> | 수신자, 쓰기 가능한 미서명 계정 |
| 106–137 | 32 B | 주소 2 | 11111111111111111111111111111111 | System Program, 읽기 전용, 미서명 |
| 138–141 | 4 B | 구성 값: 컴퓨트 유닛 한도 | 10 27 00 00 | 리틀 엔디언 u32 형식의 10,000 CU(마스크 비트 2에 해당) |
| 142–145 | 4 B | 구성 값: 로드된 계정 데이터 크기 한도 | 00 00 01 00 | 리틀 엔디언 u32 형식의 65,536바이트/64 KiB(마스크 비트 3에 해당) |
| 146–149 | 4 B | 명령: 헤더 | 02 02 0C 00 | 프로그램 인덱스 2, 계정 2개, 데이터 12바이트 |
| 150–151 | 2 B | 명령: 계정 인덱스 | 00 01 | 주소 0 = 발신자, 주소 1 = 수신자 |
| 152–155 | 4 B | 명령: 전송 판별자 | 02 00 00 00 | SystemInstruction::Transfer |
| 156–163 | 8 B | 명령: Lamports | e.g. E8 03 00 00 00 00 00 00 | 리틀 엔디언 u64 형식의 1,000 lamports |
| 164–227 | 64 B | 서명 0 | <Ed25519 signature> | 0~155바이트에 대한 발신자 서명 |
| 합계 | 228 B |
서명 검증
서명 검증은 트랜잭션이 검증인에 도달했을 때 가장 먼저 수행되는 작업 중 하나입니다. 서명이 유효하지 않은 트랜잭션은 스케줄러에 도달하기 전에 필터링하고 삭제해야 합니다. 가변 길이 메시지 뒤에 서명을 배치하면 접근하기 더 어려울 것처럼 보이지만 실제로는 그렇지 않습니다.
비용이 큰 Ed25519 검증을 수행하기 전에 검증인은 트랜잭션의 각 부분이 시작하고 끝나는 위치만 식별하면 됩니다. v1 형식은 이 작업을 저렴하고 결정적으로 수행하도록 설계됐습니다.
첫 번째 바이트 0x81가 트랜잭션을 즉시 v1으로 식별합니다. 뒤따르는 헤더에는 num_required_signatures이 포함되므로 검증인은 처음부터 몇 개의 64바이트 서명을 찾아야 하는지 알 수 있습니다.
그다음 Agave의 TransactionView 파서는 오프셋을 기록하며 트랜잭션을 순차적으로 탐색합니다. 고정 크기 필드를 읽고 NumAddresses을 사용해 주소 배열을 건너뜁니다.
구성 마스크를 사용해 구성 섹션의 크기를 결정하고, 각 4바이트 명령 헤더를 읽어 해당 명령 페이로드의 크기를 파악합니다.
파서가 마지막 명령 페이로드를 지나면 현재 위치를 서명 오프셋으로 기록합니다. 이 위치부터 Agave는 정확히 num_required_signatures × 64바이트가 이어질 것으로 예상합니다.
구현은 서명 개수와 패킷 내 첫 번째 서명의 바이트 오프셋을 포함하는 SignatureFrame을 저장합니다. 트랜잭션의 서명된 메시지는 v1 메시지 시작부터 기록된 서명 오프셋까지의 바이트 범위입니다. 따라서 검증인은 트랜잭션을 실행하거나 계정을 로드하지 않고도 비용이 큰 Ed25519 검증을 수행할 수 있습니다.
v1은 서명 배열 뒤의 후행 바이트도 금지합니다. 파서가 서명에 도달하면 남은 바이트 크기는 서명 개수에 따라 명확하게 결정됩니다. SIMD-0385는 각 필수 서명자마다 정확히 하나의 64바이트 서명이 있어야 하며 마지막 서명 뒤에는 데이터가 없어야 한다고 규정합니다.
개선된 명령 파싱
트랜잭션 v1의 레이아웃은 명령 파싱을 더 쉽게 만듭니다. 레거시와 v0 트랜잭션에서 뒤쪽 명령을 찾으려면 파서가 앞선 명령을 순차적으로 탐색하고 길이를 디코딩한 뒤 각 가변 길이 페이로드를 건너뛰어야 합니다.
v1은 고정 너비 명령 헤더를 가변 길이 페이로드와 분리합니다. 각 명령은 먼저 프로그램 계정 인덱스, 명령 계정 수, 명령 데이터 길이를 포함하는 4바이트 헤더를 제공합니다. 이러한 헤더는 계정 인덱스와 데이터 페이로드 앞에 함께 배치됩니다. 따라서 모든 명령에 대한 간결한 설명을 먼저 확인할 수 있습니다. 명령 섹션의 경계를 지정하고 건너뛰는 비용을 줄이며, 명령 데이터를 파싱하지 않고도 뒤쪽 서명 배열의 오프셋을 판단하는 데 도움이 됩니다.
트랜잭션 구성 마스크
v1 형식의 또 다른 주요 추가 요소는 4바이트 TransactionConfigMask입니다.
레거시와 v0 트랜잭션은 Compute Budget Program 명령을 통해 리소스를 구성합니다. 예를 들어 트랜잭션에 SetComputeUnitLimit, SetComputeUnitPrice 또는 SetLoadedAccountsDataSizeLimit 명령이 포함될 수 있습니다. 트랜잭션의 리소스 요건이나 우선순위를 파악하려는 검증인은 이러한 명령을 찾아 해석해야 합니다. 트랜잭션 v1은 이 정보를 명령 스트림에서 트랜잭션 형식 자체로 이동합니다.
마스크는 32비트 리틀 엔디언 비트 필드입니다. 설정된 각 비트는 트랜잭션 뒤쪽의 ConfigValues 섹션에 있는 4바이트 워드 하나에 대응합니다.
- 비트 0과 1: lamports 단위의 총 우선순위 수수료(8바이트 리틀 엔디언
u64로 인코딩) - 비트 2: 컴퓨트 유닛 한도(4바이트
u32) - 비트 3: 로드된 계정 데이터 크기 한도(4바이트
u32) - 비트 4: 요청한 힙 크기(4바이트
u32)
참고: 초기 v1 사양은 비트 0~4에만 의미를 할당합니다. 나머지 비트는 현재 할당되지 않아 향후 트랜잭션 수준 구성 필드를 위한 공간으로 남아 있습니다.
마스크는 어떤 필드가 존재하고 몇 바이트가 필요한지를 파서에 알려주는 간결한 스키마 역할을 합니다. 예를 들어 간단한 SOL 전송에는 컴퓨트 유닛 한도와 로드된 계정 데이터 크기 한도가 필요하지만, 우선순위 수수료나 사용자 지정 힙 크기는 필요하지 않습니다.
이 두 비트가 설정되어 있으므로 트랜잭션의 주소 배열 뒤에 정확히 두 개의 4바이트 구성 값이 이어집니다. 이 예시에서는 20,000 CU 한도와 64 KiB의 로드된 계정 데이터 한도를 요청합니다.
마스크는 첫 번째 4바이트 구성이 비트 2인 컴퓨트 유닛 한도에 속하고, 두 번째 구성이 비트 3인 로드된 계정 데이터 한도에 속한다는 것을 검증인에게 알려줍니다.
레거시와 v0 트랜잭션에서는 컴퓨트 유닛 한도 명령을 생략하면 트랜잭션에 암시적 컴퓨트 예산이 적용됩니다. v1은 동일한 기본값을 사용하지 않습니다. 설정되지 않은 컴퓨트 유닛 한도와 로드된 계정 데이터 크기 한도는 0으로 처리됩니다. 힙만 32 KiB 기본값을 유지합니다. 따라서 v1 트랜잭션은 필요한 리소스를 명시적으로 요청해야 합니다.
이러한 설정을 명확히 정의된 구성 영역으로 이동하면 검증인이 명령을 검색하지 않고도 트랜잭션 수집 중 필요한 정보에 직접 접근할 수 있습니다. 또한 Compute Budget Program 명령은 더 이상 v1 트랜잭션을 구성하지 않습니다. 포함하더라도 무작동 명령으로 처리되어 무시되지만 컴퓨트 유닛은 계속 소모합니다.
공간 측면의 이점도 있습니다. 레거시/v0 트랜잭션에 Compute Budget 명령을 추가하려면 일반적으로 계정 목록에 32바이트 Compute Budget Program 주소와 직렬화된 명령 자체를 포함해야 합니다.
결론
Solana의 세 가지 트랜잭션 형식은 네트워크의 발전 과정을 보여줍니다. 레거시는 최초 모델을 확립했고, v0은 ALT로 이를 확장해 1,232바이트 상한 안에서 더 큰 계정 집합을 지원했습니다. v1은 더 큰 트랜잭션, 간단한 파싱, 인라인 주소, 네이티브 리소스 구성을 중심으로 와이어 형식을 근본적으로 재설계합니다.
세 형식은 모두 여전히 유효하지만 각각 Solana 발전의 다른 단계를 반영합니다. 이들을 함께 살펴보면 네트워크의 애플리케이션, 수수료 시장, 검증인 요건이 발전함에 따라 트랜잭션 형식이 어떻게 적응했는지 알 수 있습니다.
추가 자료
- v1 트랜잭션과 ALT의 트레이드오프 - Umberto Natale, Solana Foundation
- 버전 트랜잭션 - Solana 문서
- 트랜잭션 크기 확대 - SIMD 토론 포럼
- 더 큰 트랜잭션 크기 - Solana 업그레이드
관련 아티클
Helius 구독하기
최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요


