Files
zeekay 3e8821a3d5 decomplect(warp): one warp (luxfi/warp v1.23.0) + one signed-message helper across bridgevm/zkvm/thresholdvm; deps evm v1.99.48 + precompile v0.16.0
Standardize all of chains on github.com/luxfi/warp v1.23.0 (the standalone
ZAP-native warp that precompile/evm/bridgevm already use). zkvm/fhe and
thresholdvm/fhe are migrated OFF github.com/luxfi/node/vms/platformvm/warp;
there are now ZERO platformvm/warp imports in chains.

The build->sign->wrap sequence (previously copy-pasted in three places and
split across two warp libraries) now lives in exactly one place:
internal/warpmsg.BuildSigned(signer, networkID, sourceChainID, payload)
-> (*warp.Envelope, error). It builds a content Message (NewMessage), signs
it over the Beam domain with the node BLS key, and wraps the signature at
validator bit index 0 into a single-signer Envelope. All three send sites
call it:
  - bridgevm/vm.go        reshare-request gossip
  - zkvm/fhe/coprocessor   FHE task-result callback
  - thresholdvm/fhe/relayer decryption fulfillment

v1.23.0 split the signature from the Message: the signed/transmittable unit
is now *warp.Envelope (Message + Beam BitSetSignature + PQ lanes), not a
combined Message. The receive path moves with it: onMessage callbacks and
WarpHandler.HandleMessage now carry *warp.Envelope and read authenticated
content from env.Message. MPC/BLS single-signer semantics are preserved
exactly (bit-0 signer, Beam-domain BLS, receiver aggregates/verifies the
BitSetSignature against the canonical validator set via warp.VerifyEnvelope).

CGO_ENABLED=0 build/vet clean; bridgevm/zkvm/thresholdvm test suites green.
2026-06-27 21:49:08 -07:00

616 lines
14 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package fhe
import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"sync"
"time"
"github.com/luxfi/chains/internal/warpmsg"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/warp"
"github.com/luxfi/warp/payload"
)
// OpCode represents an FHE operation code
type OpCode uint8
const (
OpAdd OpCode = iota
OpSub
OpMul
OpNeg
OpLt
OpGt
OpLte
OpGte
OpEq
OpNe
OpNot
OpAnd
OpOr
OpXor
OpSelect
OpMin
OpMax
OpShl
OpShr
OpEncrypt
OpDecrypt
OpVerifyInput
)
// String returns the operation name
func (op OpCode) String() string {
names := []string{
"add", "sub", "mul", "neg",
"lt", "gt", "lte", "gte", "eq", "ne",
"not", "and", "or", "xor",
"select", "min", "max", "shl", "shr",
"encrypt", "decrypt", "verify_input",
}
if int(op) < len(names) {
return names[op]
}
return "unknown"
}
// Task represents an FHE computation task submitted to the coprocessor
type Task struct {
// ID is a unique task identifier
ID [32]byte
// Op is the operation to perform
Op OpCode
// Inputs are handles to input ciphertexts
Inputs [][32]byte
// ScalarInputs are plaintext scalar inputs (for mul_plain, shl, etc.)
ScalarInputs []uint64
// ResultType is the expected result type
ResultType EncryptedType
// Requester is the C-Chain contract that submitted this task
Requester [20]byte
// SourceChain is the chain that submitted this task
SourceChain ids.ID
// Callback is the function to call with the result
Callback [20]byte
CallbackSelector [4]byte
// Submitted is when the task was created
Submitted time.Time
// Status is the current task status
Status TaskStatus
// Result is the output handle (set when completed)
Result [32]byte
// Error is set if the task failed
Error string
}
// TaskStatus represents the status of a task
type TaskStatus uint8
const (
TaskPending TaskStatus = iota
TaskProcessing
TaskCompleted
TaskFailed
)
// WarpCallback handles sending results back via Warp messaging
type WarpCallback struct {
logger log.Logger
networkID uint32
chainID ids.ID
signer warp.Signer
onMessage func(context.Context, *warp.Envelope) error
}
// NewWarpCallback creates a new Warp callback handler
func NewWarpCallback(
logger log.Logger,
networkID uint32,
chainID ids.ID,
signer warp.Signer,
onMessage func(context.Context, *warp.Envelope) error,
) *WarpCallback {
return &WarpCallback{
logger: logger,
networkID: networkID,
chainID: chainID,
signer: signer,
onMessage: onMessage,
}
}
// SendTaskResult sends a task result back to the source chain
func (w *WarpCallback) SendTaskResult(ctx context.Context, task *Task) error {
if w.onMessage == nil {
return nil
}
// Encode callback data
data := encodeTaskCallback(task)
// Create addressed call to the callback contract
addressedCall, err := payload.NewAddressedCall(task.Callback[:], data)
if err != nil {
return fmt.Errorf("create addressed call: %w", err)
}
// Build, sign, and wrap the result as a single-signer Warp envelope.
// warpmsg.BuildSigned is the one place that performs build→sign→wrap.
env, err := warpmsg.BuildSigned(w.signer, w.networkID, w.chainID, addressedCall.Bytes())
if err != nil {
return fmt.Errorf("build signed warp message: %w", err)
}
// Send via message handler
if err := w.onMessage(ctx, env); err != nil {
return fmt.Errorf("send warp message: %w", err)
}
w.logger.Info("Sent task result via Warp",
"taskID", fmt.Sprintf("%x", task.ID[:8]),
"callback", fmt.Sprintf("%x", task.Callback),
"success", task.Status == TaskCompleted,
)
return nil
}
// encodeTaskCallback creates ABI-encoded callback data
func encodeTaskCallback(task *Task) []byte {
// callback(bytes32 taskId, bool success, bytes32 resultHandle)
// Selector + taskId + success + resultHandle
data := make([]byte, 4+32+32+32)
// Copy selector
copy(data[0:4], task.CallbackSelector[:])
// Copy task ID
copy(data[4:36], task.ID[:])
// Success flag (padded to 32 bytes)
if task.Status == TaskCompleted {
data[67] = 1
}
// Copy result handle
copy(data[68:100], task.Result[:])
return data
}
// Coprocessor is the FHE computation coprocessor
// It receives tasks from C-Chain, computes on Z-Chain, and returns results
type Coprocessor struct {
processor *Processor
warpCallback *WarpCallback
logger log.Logger
// Task queue
pending chan *Task
completed map[[32]byte]*Task
mu sync.RWMutex
// Configuration
maxQueueSize int
workerCount int
// Metrics
tasksProcessed uint64
tasksCompleted uint64
tasksFailed uint64
// Context for shutdown
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// NewCoprocessor creates a new FHE coprocessor
func NewCoprocessor(processor *Processor, logger log.Logger, maxQueueSize, workerCount int) *Coprocessor {
ctx, cancel := context.WithCancel(context.Background())
return &Coprocessor{
processor: processor,
logger: logger,
pending: make(chan *Task, maxQueueSize),
completed: make(map[[32]byte]*Task),
maxQueueSize: maxQueueSize,
workerCount: workerCount,
ctx: ctx,
cancel: cancel,
}
}
// SetWarpCallback sets the Warp callback handler for sending results
func (c *Coprocessor) SetWarpCallback(callback *WarpCallback) {
c.warpCallback = callback
}
// Start starts the coprocessor workers
func (c *Coprocessor) Start() {
for i := 0; i < c.workerCount; i++ {
c.wg.Add(1)
go c.worker(i)
}
c.logger.Info("FHE Coprocessor started", "workers", c.workerCount)
}
// Stop stops the coprocessor
func (c *Coprocessor) Stop() {
c.cancel()
c.wg.Wait()
c.logger.Info("FHE Coprocessor stopped")
}
// SubmitTask submits a task for processing
func (c *Coprocessor) SubmitTask(task *Task) error {
select {
case c.pending <- task:
return nil
default:
return errors.New("task queue full")
}
}
// GetTaskResult retrieves a completed task result
func (c *Coprocessor) GetTaskResult(taskID [32]byte) (*Task, error) {
c.mu.RLock()
defer c.mu.RUnlock()
task, ok := c.completed[taskID]
if !ok {
return nil, errors.New("task not found")
}
return task, nil
}
// CreateTask creates a new task with a unique ID
func (c *Coprocessor) CreateTask(op OpCode, inputs [][32]byte, scalars []uint64, resultType EncryptedType) *Task {
// Generate task ID
h := sha256.New()
h.Write([]byte{byte(op)})
for _, input := range inputs {
h.Write(input[:])
}
for _, scalar := range scalars {
binary.Write(h, binary.BigEndian, scalar)
}
binary.Write(h, binary.BigEndian, time.Now().UnixNano())
var id [32]byte
copy(id[:], h.Sum(nil))
return &Task{
ID: id,
Op: op,
Inputs: inputs,
ScalarInputs: scalars,
ResultType: resultType,
Submitted: time.Now(),
Status: TaskPending,
}
}
// worker processes tasks from the queue
func (c *Coprocessor) worker(id int) {
defer c.wg.Done()
for {
select {
case <-c.ctx.Done():
return
case task := <-c.pending:
c.processTask(task)
}
}
}
// processTask executes a single task
func (c *Coprocessor) processTask(task *Task) {
task.Status = TaskProcessing
c.tasksProcessed++
var result *Ciphertext
var err error
// Get input ciphertexts
inputs := make([]*Ciphertext, len(task.Inputs))
for i, handle := range task.Inputs {
inputs[i], err = c.processor.GetCiphertext(handle)
if err != nil {
c.completeTask(task, nil, fmt.Errorf("input %d not found: %w", i, err))
return
}
}
// Execute operation
switch task.Op {
case OpAdd:
if len(inputs) != 2 {
err = errors.New("add requires 2 inputs")
} else {
result, err = c.processor.Add(inputs[0], inputs[1])
}
case OpSub:
if len(inputs) != 2 {
err = errors.New("sub requires 2 inputs")
} else {
result, err = c.processor.Sub(inputs[0], inputs[1])
}
case OpMul:
if len(inputs) == 2 {
result, err = c.processor.Mul(inputs[0], inputs[1])
} else if len(inputs) == 1 && len(task.ScalarInputs) == 1 {
result, err = c.processor.MulPlain(inputs[0], task.ScalarInputs[0])
} else {
err = errors.New("mul requires 2 ciphertext inputs or 1 ciphertext + 1 scalar")
}
case OpNeg:
if len(inputs) != 1 {
err = errors.New("neg requires 1 input")
} else {
result, err = c.processor.Neg(inputs[0])
}
case OpLt:
if len(inputs) != 2 {
err = errors.New("lt requires 2 inputs")
} else {
result, err = c.processor.Lt(inputs[0], inputs[1])
}
case OpGt:
if len(inputs) != 2 {
err = errors.New("gt requires 2 inputs")
} else {
result, err = c.processor.Gt(inputs[0], inputs[1])
}
case OpLte:
if len(inputs) != 2 {
err = errors.New("lte requires 2 inputs")
} else {
result, err = c.processor.Lte(inputs[0], inputs[1])
}
case OpGte:
if len(inputs) != 2 {
err = errors.New("gte requires 2 inputs")
} else {
result, err = c.processor.Gte(inputs[0], inputs[1])
}
case OpEq:
if len(inputs) != 2 {
err = errors.New("eq requires 2 inputs")
} else {
result, err = c.processor.Eq(inputs[0], inputs[1])
}
case OpNe:
if len(inputs) != 2 {
err = errors.New("ne requires 2 inputs")
} else {
result, err = c.processor.Ne(inputs[0], inputs[1])
}
case OpNot:
if len(inputs) != 1 {
err = errors.New("not requires 1 input")
} else {
result, err = c.processor.Not(inputs[0])
}
case OpAnd:
if len(inputs) != 2 {
err = errors.New("and requires 2 inputs")
} else {
result, err = c.processor.And(inputs[0], inputs[1])
}
case OpOr:
if len(inputs) != 2 {
err = errors.New("or requires 2 inputs")
} else {
result, err = c.processor.Or(inputs[0], inputs[1])
}
case OpXor:
if len(inputs) != 2 {
err = errors.New("xor requires 2 inputs")
} else {
result, err = c.processor.Xor(inputs[0], inputs[1])
}
case OpSelect:
if len(inputs) != 3 {
err = errors.New("select requires 3 inputs (condition, ifTrue, ifFalse)")
} else {
result, err = c.processor.Select(inputs[0], inputs[1], inputs[2])
}
case OpMin:
if len(inputs) != 2 {
err = errors.New("min requires 2 inputs")
} else {
result, err = c.processor.Min(inputs[0], inputs[1])
}
case OpMax:
if len(inputs) != 2 {
err = errors.New("max requires 2 inputs")
} else {
result, err = c.processor.Max(inputs[0], inputs[1])
}
case OpShl:
if len(inputs) != 1 || len(task.ScalarInputs) != 1 {
err = errors.New("shl requires 1 ciphertext and 1 scalar")
} else {
result, err = c.processor.Shl(inputs[0], uint(task.ScalarInputs[0]))
}
case OpShr:
if len(inputs) != 1 || len(task.ScalarInputs) != 1 {
err = errors.New("shr requires 1 ciphertext and 1 scalar")
} else {
result, err = c.processor.Shr(inputs[0], uint(task.ScalarInputs[0]))
}
default:
err = fmt.Errorf("unknown operation: %d", task.Op)
}
c.completeTask(task, result, err)
}
// completeTask marks a task as complete and stores the result
func (c *Coprocessor) completeTask(task *Task, result *Ciphertext, err error) {
if err != nil {
task.Status = TaskFailed
task.Error = err.Error()
c.tasksFailed++
c.logger.Error("Task failed",
"taskID", fmt.Sprintf("%x", task.ID[:8]),
"op", task.Op.String(),
"error", err,
)
} else {
task.Status = TaskCompleted
task.Result = result.Handle
c.tasksCompleted++
c.logger.Debug("Task completed",
"taskID", fmt.Sprintf("%x", task.ID[:8]),
"op", task.Op.String(),
"result", fmt.Sprintf("%x", task.Result[:8]),
)
}
c.mu.Lock()
c.completed[task.ID] = task
c.mu.Unlock()
// Send callback to C-Chain via Warp messaging
if c.warpCallback != nil && task.Callback != [20]byte{} {
if err := c.warpCallback.SendTaskResult(c.ctx, task); err != nil {
c.logger.Error("Failed to send Warp callback", "error", err)
}
}
}
// Stats returns coprocessor statistics
func (c *Coprocessor) Stats() (processed, completed, failed uint64, queueLen int) {
return c.tasksProcessed, c.tasksCompleted, c.tasksFailed, len(c.pending)
}
// TaskRequest is the cross-chain message format for submitting tasks
type TaskRequest struct {
// SourceChain is the chain that submitted the request
SourceChain ids.ID
// RequestID is unique within the source chain
RequestID uint64
// Op is the FHE operation
Op OpCode
// InputHandles are ciphertext handles
InputHandles [][32]byte
// ScalarInputs for operations that need them
ScalarInputs []uint64
// ResultType is the expected output type
ResultType EncryptedType
// Callback information
CallbackContract [20]byte
CallbackSelector [4]byte
}
// TaskResponse is the cross-chain response format
type TaskResponse struct {
// RequestID matches the original request
RequestID uint64
// Success indicates if the operation succeeded
Success bool
// ResultHandle is the ciphertext handle (if success)
ResultHandle [32]byte
// Error message (if not success)
Error string
}
// HandleCrossChainRequest processes a task request from C-Chain
func (c *Coprocessor) HandleCrossChainRequest(req *TaskRequest) *TaskResponse {
task := c.CreateTask(req.Op, req.InputHandles, req.ScalarInputs, req.ResultType)
task.Callback = req.CallbackContract
task.CallbackSelector = req.CallbackSelector
task.SourceChain = req.SourceChain
if err := c.SubmitTask(task); err != nil {
return &TaskResponse{
RequestID: req.RequestID,
Success: false,
Error: err.Error(),
}
}
// Synchronous wait; async Warp callback not yet wired
timeout := time.After(30 * time.Second)
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-timeout:
return &TaskResponse{
RequestID: req.RequestID,
Success: false,
Error: "timeout waiting for task completion",
}
case <-ticker.C:
result, err := c.GetTaskResult(task.ID)
if err == nil {
if result.Status == TaskCompleted {
return &TaskResponse{
RequestID: req.RequestID,
Success: true,
ResultHandle: result.Result,
}
} else if result.Status == TaskFailed {
return &TaskResponse{
RequestID: req.RequestID,
Success: false,
Error: result.Error,
}
}
}
}
}
}