mirror of
https://github.com/luxfi/relay.git
synced 2026-10-30 15:20:05 +00:00
239 lines
4.9 KiB
Go
239 lines
4.9 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package vm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
// Block represents a block in the RelayVM chain
|
|
type Block struct {
|
|
ParentID_ ids.ID `json:"parentId"`
|
|
BlockHeight uint64 `json:"height"`
|
|
BlockTimestamp int64 `json:"timestamp"`
|
|
Messages []*Message `json:"messages"`
|
|
Receipts []*MessageReceipt `json:"receipts,omitempty"`
|
|
StateRoot []byte `json:"stateRoot"`
|
|
|
|
// Cached values
|
|
ID_ ids.ID
|
|
bytes []byte
|
|
status choices.Status
|
|
vm *VM
|
|
}
|
|
|
|
// ID returns the block ID
|
|
func (b *Block) ID() ids.ID {
|
|
if b.ID_ == ids.Empty {
|
|
b.ID_ = b.computeID()
|
|
}
|
|
return b.ID_
|
|
}
|
|
|
|
// computeID computes the block ID
|
|
func (b *Block) computeID() ids.ID {
|
|
h := sha256.New()
|
|
h.Write(b.ParentID_[:])
|
|
binary.Write(h, binary.BigEndian, b.BlockHeight)
|
|
binary.Write(h, binary.BigEndian, b.BlockTimestamp)
|
|
|
|
// Include message IDs
|
|
for _, msg := range b.Messages {
|
|
msgID := msg.ID
|
|
h.Write(msgID[:])
|
|
}
|
|
|
|
// Include receipt hashes
|
|
for _, receipt := range b.Receipts {
|
|
h.Write(receipt.MessageID[:])
|
|
h.Write(receipt.ResultHash)
|
|
}
|
|
|
|
// Include state root
|
|
h.Write(b.StateRoot)
|
|
|
|
return ids.ID(h.Sum(nil))
|
|
}
|
|
|
|
// ParentID returns the parent block ID
|
|
func (b *Block) ParentID() ids.ID {
|
|
return b.ParentID_
|
|
}
|
|
|
|
// Parent is an alias for ParentID for compatibility
|
|
func (b *Block) Parent() ids.ID {
|
|
return b.ParentID_
|
|
}
|
|
|
|
// Height returns the block height
|
|
func (b *Block) Height() uint64 {
|
|
return b.BlockHeight
|
|
}
|
|
|
|
// Timestamp returns the block timestamp
|
|
func (b *Block) Timestamp() time.Time {
|
|
return time.Unix(b.BlockTimestamp, 0)
|
|
}
|
|
|
|
// Status returns the block status
|
|
func (b *Block) Status() uint8 {
|
|
return uint8(b.status)
|
|
}
|
|
|
|
// Verify verifies the block
|
|
func (b *Block) Verify(ctx context.Context) error {
|
|
// Verify height
|
|
if b.BlockHeight == 0 && b.ParentID_ != ids.Empty {
|
|
return errors.New("invalid genesis block")
|
|
}
|
|
|
|
// Verify timestamp is not too far in future
|
|
if b.BlockTimestamp > time.Now().Unix()+60 {
|
|
return errors.New("block timestamp too far in future")
|
|
}
|
|
|
|
// Verify parent exists and heights are consecutive
|
|
if b.BlockHeight > 0 {
|
|
parent, err := b.vm.GetBlock(ctx, b.ParentID_)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.BlockHeight != parent.Height()+1 {
|
|
return errors.New("non-consecutive block heights")
|
|
}
|
|
|
|
if b.BlockTimestamp < parent.Timestamp().Unix() {
|
|
return errors.New("block timestamp before parent")
|
|
}
|
|
}
|
|
|
|
// Verify all messages
|
|
for _, msg := range b.Messages {
|
|
if err := b.verifyMessage(msg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Block) verifyMessage(msg *Message) error {
|
|
// Verify message has valid channel
|
|
_, err := b.vm.GetChannel(msg.ChannelID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify message size
|
|
if len(msg.Payload) > b.vm.config.MaxMessageSize {
|
|
return errMessageTooLarge
|
|
}
|
|
|
|
// Verify message hasn't timed out
|
|
if msg.Timeout > 0 && time.Now().Unix() > msg.Timeout {
|
|
return errors.New("message timeout exceeded")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Accept accepts the block
|
|
func (b *Block) Accept(ctx context.Context) error {
|
|
b.status = choices.Accepted
|
|
|
|
b.vm.mu.Lock()
|
|
defer b.vm.mu.Unlock()
|
|
|
|
// Update VM state
|
|
b.vm.lastAccepted = b
|
|
b.vm.lastAcceptedID = b.ID()
|
|
|
|
// Save last accepted
|
|
id := b.ID()
|
|
if err := b.vm.db.Put(lastAcceptedKey, id[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save block
|
|
blockBytes := b.Bytes()
|
|
if blockBytes == nil {
|
|
return errors.New("failed to serialize block")
|
|
}
|
|
|
|
if err := b.vm.db.Put(id[:], blockBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process messages
|
|
for _, msg := range b.Messages {
|
|
msg.State = MessageDelivered
|
|
msg.ConfirmedAt = b.BlockTimestamp
|
|
|
|
// Remove from pending
|
|
destMsgs := b.vm.pendingMsgs[msg.DestChain]
|
|
for i, m := range destMsgs {
|
|
if m.ID == msg.ID {
|
|
b.vm.pendingMsgs[msg.DestChain] = append(destMsgs[:i], destMsgs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Persist message
|
|
msgBytes, _ := json.Marshal(msg)
|
|
msgKey := append(messagePrefix, msg.ID[:]...)
|
|
b.vm.db.Put(msgKey, msgBytes)
|
|
}
|
|
|
|
// Remove from pending blocks
|
|
delete(b.vm.pendingBlocks, b.ID())
|
|
|
|
b.vm.log.Info("Block accepted",
|
|
log.Uint64("height", b.BlockHeight),
|
|
log.String("id", b.ID().String()),
|
|
log.Int("messages", len(b.Messages)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reject rejects the block
|
|
func (b *Block) Reject(ctx context.Context) error {
|
|
b.status = choices.Rejected
|
|
|
|
b.vm.mu.Lock()
|
|
defer b.vm.mu.Unlock()
|
|
|
|
// Remove from pending blocks
|
|
delete(b.vm.pendingBlocks, b.ID())
|
|
|
|
// Messages remain in pending pool for next block attempt
|
|
|
|
return nil
|
|
}
|
|
|
|
// Bytes returns the block bytes
|
|
func (b *Block) Bytes() []byte {
|
|
if b.bytes != nil {
|
|
return b.bytes
|
|
}
|
|
|
|
bytes, err := json.Marshal(b)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
b.bytes = bytes
|
|
return bytes
|
|
}
|