Files
api/zap/wire.go
T
zeekayandHanzo Dev 8bdf9cbf1b zap: carry the Quasar EXPORT capability across the rpcchainvm boundary (wire)
Two-tier consensus v1.36 splits finality into Nova (local, reorgable accept tip)
and Quasar (⅔-by-stake EXPORT-final). A VM's `finalized`/`safe` tags and its
cross-chain (warp) export gate must resolve to the Quasar height. But the
C-Chain EVM runs as a SEPARATE rpcchainvm plugin process, and nothing carried
the export height across the ZAP boundary — so the whole export tier was dormant
in the real deploy (finalized/safe stuck at genesis).

Wire additions (OPTIONAL, capability-gated — generic VMs are unaffected):
- MsgSetQuasarFinalized (60) / MsgQuasarHeight (61): the two export RPCs. Kept
  < 0x40 so the response/error flags OR in cleanly, grouped after Warp (50-59).
- InitializeResponse.Capabilities (uint64) + CapQuasarExport bit: the plugin
  advertises the export capability in the handshake so the node wires the
  observer only for an export-capable VM. Appended to the struct codec (all four
  encode/decode sites go through the struct methods, so it stays consistent).
- SetQuasarFinalizedRequest / QuasarHeightResponse message structs.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-10 10:14:31 -07:00

373 lines
9.2 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap
import (
"encoding/binary"
"errors"
"io"
"sync"
)
const (
// MaxMessageSize is the maximum allowed message size (16MB)
MaxMessageSize = 16 * 1024 * 1024
// HeaderSize is the size of the message header (4 bytes length + 1 byte type)
HeaderSize = 5
// DefaultBufferSize for pooled buffers
DefaultBufferSize = 64 * 1024
)
var (
ErrMessageTooLarge = errors.New("zap: message exceeds maximum size")
ErrInvalidMessage = errors.New("zap: invalid message format")
ErrUnknownType = errors.New("zap: unknown message type")
)
// MessageType identifies the RPC method being called
type MessageType uint8
const (
// VM service methods (1-31)
MsgInitialize MessageType = iota + 1
MsgSetState
MsgShutdown
MsgCreateHandlers
MsgNewHTTPHandler
MsgWaitForEvent
MsgConnected
MsgDisconnected
MsgBuildBlock
MsgParseBlock
MsgGetBlock
MsgSetPreference
MsgHealth
MsgVersion
MsgRequest
MsgRequestFailed
MsgResponse
MsgGossip
MsgGather
MsgGetAncestors
MsgBatchedParseBlock
MsgGetBlockIDAtHeight
MsgStateSyncEnabled
MsgGetOngoingSyncStateSummary
MsgGetLastStateSummary
MsgParseStateSummary
MsgGetStateSummary
MsgBlockVerify
MsgBlockAccept
MsgBlockReject
MsgStateSummaryAccept // = 31
// p2p.Sender methods (40-49)
MsgSendRequest MessageType = 40
MsgSendResponse MessageType = 41
MsgSendError MessageType = 42
MsgSendGossip MessageType = 43
// Warp signing methods (50-59)
MsgWarpSign MessageType = 50
MsgWarpGetPublicKey MessageType = 51
MsgWarpBatchSign MessageType = 52
// Quasar EXPORT methods (60-63): the two-tier consensus (v1.36) export
// frontier — Quasar = ⅔-by-stake, the reorg-safe finality a VM's
// `finalized`/`safe` tags and cross-chain (warp) export gate must resolve
// to instead of the reorgable Nova accept tip — carried across the plugin
// process boundary. OPTIONAL: only a VM that advertises CapQuasarExport in
// the Initialize handshake (see vm.go) is ever sent these; a generic VM is
// never asked. Kept < 0x40 so the response/error flags OR in cleanly.
MsgSetQuasarFinalized MessageType = 60
MsgQuasarHeight MessageType = 61
// Response flag - set on response messages (bit 7 / high bit).
// All message types must be < 64 (0x40) to allow OR with this flag
// AND with MsgErrorFlag below.
MsgResponseFlag MessageType = 0x80
// Error flag - set on response messages that carry an error.
// Always co-occurs with MsgResponseFlag on the wire:
// success response: msgType | MsgResponseFlag
// error response: msgType | MsgResponseFlag | MsgErrorFlag
// Payload of an error response is the error string (length-prefixed
// after the request ID).
MsgErrorFlag MessageType = 0x40
)
// Buffer is a reusable byte buffer for zero-copy operations
type Buffer struct {
Data []byte
offset int
}
// BufferPool manages reusable buffers to minimize allocations
var BufferPool = sync.Pool{
New: func() interface{} {
return &Buffer{
Data: make([]byte, DefaultBufferSize),
}
},
}
// GetBuffer retrieves a buffer from the pool
func GetBuffer() *Buffer {
buf := BufferPool.Get().(*Buffer)
buf.Reset()
return buf
}
// PutBuffer returns a buffer to the pool
func PutBuffer(buf *Buffer) {
if buf != nil && cap(buf.Data) <= MaxMessageSize {
BufferPool.Put(buf)
}
}
// Reset prepares the buffer for reuse
func (b *Buffer) Reset() {
b.offset = 0
b.Data = b.Data[:cap(b.Data)]
}
// Grow ensures the buffer has at least n bytes available
func (b *Buffer) Grow(n int) {
if cap(b.Data) < n {
newData := make([]byte, n*2)
copy(newData, b.Data[:b.offset])
b.Data = newData
}
if len(b.Data) < n {
b.Data = b.Data[:cap(b.Data)]
}
}
// WriteUint8 writes a uint8 to the buffer
func (b *Buffer) WriteUint8(v uint8) {
b.Grow(b.offset + 1)
b.Data[b.offset] = v
b.offset++
}
// WriteUint16 writes a uint16 to the buffer (big-endian)
func (b *Buffer) WriteUint16(v uint16) {
b.Grow(b.offset + 2)
binary.BigEndian.PutUint16(b.Data[b.offset:], v)
b.offset += 2
}
// WriteUint32 writes a uint32 to the buffer (big-endian)
func (b *Buffer) WriteUint32(v uint32) {
b.Grow(b.offset + 4)
binary.BigEndian.PutUint32(b.Data[b.offset:], v)
b.offset += 4
}
// WriteUint64 writes a uint64 to the buffer (big-endian)
func (b *Buffer) WriteUint64(v uint64) {
b.Grow(b.offset + 8)
binary.BigEndian.PutUint64(b.Data[b.offset:], v)
b.offset += 8
}
// WriteInt32 writes an int32 to the buffer (big-endian)
func (b *Buffer) WriteInt32(v int32) {
b.WriteUint32(uint32(v))
}
// WriteInt64 writes an int64 to the buffer (big-endian)
func (b *Buffer) WriteInt64(v int64) {
b.WriteUint64(uint64(v))
}
// WriteBool writes a boolean to the buffer
func (b *Buffer) WriteBool(v bool) {
if v {
b.WriteUint8(1)
} else {
b.WriteUint8(0)
}
}
// WriteBytes writes a length-prefixed byte slice to the buffer
func (b *Buffer) WriteBytes(data []byte) {
b.WriteUint32(uint32(len(data)))
b.Grow(b.offset + len(data))
copy(b.Data[b.offset:], data)
b.offset += len(data)
}
// WriteString writes a length-prefixed string to the buffer
func (b *Buffer) WriteString(s string) {
b.WriteBytes([]byte(s))
}
// Bytes returns the written portion of the buffer
func (b *Buffer) Bytes() []byte {
return b.Data[:b.offset]
}
// Len returns the number of bytes written
func (b *Buffer) Len() int {
return b.offset
}
// Reader provides zero-copy reading from a byte slice
type Reader struct {
data []byte
offset int
}
// NewReader creates a new reader from a byte slice
func NewReader(data []byte) *Reader {
return &Reader{data: data}
}
// ReadUint8 reads a uint8 from the buffer
func (r *Reader) ReadUint8() (uint8, error) {
if r.offset+1 > len(r.data) {
return 0, io.ErrUnexpectedEOF
}
v := r.data[r.offset]
r.offset++
return v, nil
}
// ReadUint16 reads a uint16 from the buffer (big-endian)
func (r *Reader) ReadUint16() (uint16, error) {
if r.offset+2 > len(r.data) {
return 0, io.ErrUnexpectedEOF
}
v := binary.BigEndian.Uint16(r.data[r.offset:])
r.offset += 2
return v, nil
}
// ReadUint32 reads a uint32 from the buffer (big-endian)
func (r *Reader) ReadUint32() (uint32, error) {
if r.offset+4 > len(r.data) {
return 0, io.ErrUnexpectedEOF
}
v := binary.BigEndian.Uint32(r.data[r.offset:])
r.offset += 4
return v, nil
}
// ReadUint64 reads a uint64 from the buffer (big-endian)
func (r *Reader) ReadUint64() (uint64, error) {
if r.offset+8 > len(r.data) {
return 0, io.ErrUnexpectedEOF
}
v := binary.BigEndian.Uint64(r.data[r.offset:])
r.offset += 8
return v, nil
}
// ReadInt32 reads an int32 from the buffer (big-endian)
func (r *Reader) ReadInt32() (int32, error) {
v, err := r.ReadUint32()
return int32(v), err
}
// ReadInt64 reads an int64 from the buffer (big-endian)
func (r *Reader) ReadInt64() (int64, error) {
v, err := r.ReadUint64()
return int64(v), err
}
// ReadBool reads a boolean from the buffer
func (r *Reader) ReadBool() (bool, error) {
v, err := r.ReadUint8()
return v != 0, err
}
// ReadBytes reads a length-prefixed byte slice (zero-copy - returns slice into original buffer)
func (r *Reader) ReadBytes() ([]byte, error) {
length, err := r.ReadUint32()
if err != nil {
return nil, err
}
if r.offset+int(length) > len(r.data) {
return nil, io.ErrUnexpectedEOF
}
// Zero-copy: return slice into original buffer
data := r.data[r.offset : r.offset+int(length)]
r.offset += int(length)
return data, nil
}
// ReadString reads a length-prefixed string
func (r *Reader) ReadString() (string, error) {
data, err := r.ReadBytes()
if err != nil {
return "", err
}
return string(data), nil
}
// Remaining returns the number of unread bytes
func (r *Reader) Remaining() int {
return len(r.data) - r.offset
}
// ReadCount reads a uint32 element count and rejects any value larger than the
// number of unread bytes. Every element costs at least one byte on the wire, so
// a count exceeding Remaining() can never be satisfied by the payload. Rejecting
// it here stops a hostile count (e.g. 0xFFFFFFFF) from driving a huge
// make([]T, count) allocation before the per-element decode loop reaches EOF.
func (r *Reader) ReadCount() (uint32, error) {
n, err := r.ReadUint32()
if err != nil {
return 0, err
}
if uint64(n) > uint64(r.Remaining()) {
return 0, ErrInvalidMessage
}
return n, nil
}
// WriteMessage writes a complete ZAP message with header
func WriteMessage(w io.Writer, msgType MessageType, payload []byte) error {
if len(payload) > MaxMessageSize {
return ErrMessageTooLarge
}
header := make([]byte, HeaderSize)
binary.BigEndian.PutUint32(header[0:4], uint32(len(payload)))
header[4] = byte(msgType)
if _, err := w.Write(header); err != nil {
return err
}
if _, err := w.Write(payload); err != nil {
return err
}
return nil
}
// ReadMessage reads a complete ZAP message with header
func ReadMessage(r io.Reader) (MessageType, []byte, error) {
header := make([]byte, HeaderSize)
if _, err := io.ReadFull(r, header); err != nil {
return 0, nil, err
}
length := binary.BigEndian.Uint32(header[0:4])
msgType := MessageType(header[4])
if length > MaxMessageSize {
return 0, nil, ErrMessageTooLarge
}
payload := make([]byte, length)
if _, err := io.ReadFull(r, payload); err != nil {
return 0, nil, err
}
return msgType, payload, nil
}