mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
700 lines
22 KiB
Go
700 lines
22 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package tchain provides T-Chain (Teleport) adapter for DAG indexing.
|
|
// T-Chain handles MPC threshold signatures for cross-chain teleportation.
|
|
package threshold
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/luxfi/indexer/dag"
|
|
"github.com/luxfi/indexer/storage"
|
|
)
|
|
|
|
const (
|
|
// DefaultRPCEndpoint for T-Chain on port 4700
|
|
DefaultRPCEndpoint = "http://localhost:9650/v1/chain/T/rpc"
|
|
// DefaultHTTPPort for T-Chain indexer API
|
|
DefaultHTTPPort = 4700
|
|
)
|
|
|
|
// SessionStatus for MPC signing sessions
|
|
type SessionStatus string
|
|
|
|
const (
|
|
SessionPending SessionStatus = "pending"
|
|
SessionActive SessionStatus = "active"
|
|
SessionCompleted SessionStatus = "completed"
|
|
SessionFailed SessionStatus = "failed"
|
|
SessionExpired SessionStatus = "expired"
|
|
)
|
|
|
|
// KeyShareStatus for distributed key shares
|
|
type KeyShareStatus string
|
|
|
|
const (
|
|
SharePending KeyShareStatus = "pending"
|
|
ShareActive KeyShareStatus = "active"
|
|
ShareRevoked KeyShareStatus = "revoked"
|
|
ShareRotated KeyShareStatus = "rotated"
|
|
)
|
|
|
|
// SigningSession represents an MPC threshold signing session
|
|
type SigningSession struct {
|
|
ID string `json:"id"`
|
|
Threshold uint32 `json:"threshold"` // t of n threshold
|
|
TotalShares uint32 `json:"totalShares"` // total participants
|
|
MessageHash string `json:"messageHash"` // hash being signed
|
|
Participants []string `json:"participants"` // node IDs participating
|
|
Signatures []string `json:"signatures"` // partial signatures collected
|
|
FinalSig string `json:"finalSig,omitempty"` // combined signature
|
|
Status SessionStatus `json:"status"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
|
ExpiresAt time.Time `json:"expiresAt"`
|
|
SourceChain string `json:"sourceChain,omitempty"`
|
|
DestChain string `json:"destChain,omitempty"`
|
|
}
|
|
|
|
// KeyShare represents a distributed key share for MPC
|
|
type KeyShare struct {
|
|
ID string `json:"id"`
|
|
PublicKey string `json:"publicKey"`
|
|
ShareIndex uint32 `json:"shareIndex"`
|
|
NodeID string `json:"nodeId"`
|
|
KeyGenID string `json:"keyGenId"` // key generation ceremony ID
|
|
Status KeyShareStatus `json:"status"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
RotatedAt *time.Time `json:"rotatedAt,omitempty"`
|
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
|
}
|
|
|
|
// KeyGeneration represents a distributed key generation ceremony
|
|
type KeyGeneration struct {
|
|
ID string `json:"id"`
|
|
Threshold uint32 `json:"threshold"`
|
|
TotalShares uint32 `json:"totalShares"`
|
|
PublicKey string `json:"publicKey"` // combined public key
|
|
Participants []string `json:"participants"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
|
}
|
|
|
|
// TeleportMessage represents a cross-chain teleport message
|
|
type TeleportMessage struct {
|
|
ID string `json:"id"`
|
|
SourceChain string `json:"sourceChain"`
|
|
DestChain string `json:"destChain"`
|
|
Sender string `json:"sender"`
|
|
Receiver string `json:"receiver"`
|
|
Payload json.RawMessage `json:"payload"`
|
|
Nonce uint64 `json:"nonce"`
|
|
SessionID string `json:"sessionId,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
SignedAt *time.Time `json:"signedAt,omitempty"`
|
|
DeliveredAt *time.Time `json:"deliveredAt,omitempty"`
|
|
}
|
|
|
|
// VertexData contains T-Chain specific vertex data
|
|
type VertexData struct {
|
|
Session *SigningSession `json:"session,omitempty"`
|
|
KeyShare *KeyShare `json:"keyShare,omitempty"`
|
|
KeyGen *KeyGeneration `json:"keyGen,omitempty"`
|
|
Message *TeleportMessage `json:"message,omitempty"`
|
|
}
|
|
|
|
// Adapter implements dag.Adapter for T-Chain MPC operations
|
|
type Adapter struct {
|
|
rpcEndpoint string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// Compile-time interface compliance check
|
|
var _ dag.Adapter = (*Adapter)(nil)
|
|
|
|
// NewAdapter creates a new T-Chain adapter
|
|
func New(rpcEndpoint string) *Adapter {
|
|
if rpcEndpoint == "" {
|
|
rpcEndpoint = DefaultRPCEndpoint
|
|
}
|
|
return &Adapter{
|
|
rpcEndpoint: rpcEndpoint,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// DefaultConfig returns default configuration for T-Chain indexer
|
|
func DefaultConfig() dag.Config {
|
|
return dag.Config{
|
|
ChainType: dag.ChainT,
|
|
ChainName: "T-Chain (Teleport)",
|
|
RPCEndpoint: DefaultRPCEndpoint,
|
|
RPCMethod: "tvm",
|
|
HTTPPort: DefaultHTTPPort,
|
|
PollInterval: 2 * time.Second,
|
|
}
|
|
}
|
|
|
|
// ParseVertex parses T-Chain vertex from RPC response
|
|
func (a *Adapter) ParseVertex(data json.RawMessage) (*dag.Vertex, error) {
|
|
var raw struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
ParentIDs []string `json:"parentIds"`
|
|
Height uint64 `json:"height"`
|
|
Epoch uint32 `json:"epoch"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Status string `json:"status"`
|
|
Data json.RawMessage `json:"data"`
|
|
// MPC-specific fields
|
|
SessionID string `json:"sessionId,omitempty"`
|
|
KeyGenID string `json:"keyGenId,omitempty"`
|
|
MessageID string `json:"messageId,omitempty"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return nil, fmt.Errorf("parse vertex: %w", err)
|
|
}
|
|
|
|
v := &dag.Vertex{
|
|
ID: raw.ID,
|
|
Type: raw.Type,
|
|
ParentIDs: raw.ParentIDs,
|
|
Height: raw.Height,
|
|
Epoch: raw.Epoch,
|
|
Timestamp: time.Unix(raw.Timestamp, 0),
|
|
Status: dag.Status(raw.Status),
|
|
Data: raw.Data,
|
|
Metadata: make(map[string]interface{}),
|
|
}
|
|
|
|
// Determine vertex type from content
|
|
if v.Type == "" {
|
|
v.Type = a.inferVertexType(raw.Data)
|
|
}
|
|
|
|
// Add MPC-specific metadata
|
|
if raw.SessionID != "" {
|
|
v.Metadata["sessionId"] = raw.SessionID
|
|
}
|
|
if raw.KeyGenID != "" {
|
|
v.Metadata["keyGenId"] = raw.KeyGenID
|
|
}
|
|
if raw.MessageID != "" {
|
|
v.Metadata["messageId"] = raw.MessageID
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// inferVertexType determines vertex type from data content
|
|
func (a *Adapter) inferVertexType(data json.RawMessage) string {
|
|
var probe map[string]interface{}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return "unknown"
|
|
}
|
|
|
|
if _, ok := probe["threshold"]; ok {
|
|
if _, ok := probe["messageHash"]; ok {
|
|
return "signing_session"
|
|
}
|
|
if _, ok := probe["publicKey"]; ok {
|
|
return "key_generation"
|
|
}
|
|
}
|
|
if _, ok := probe["shareIndex"]; ok {
|
|
return "key_share"
|
|
}
|
|
if _, ok := probe["sourceChain"]; ok {
|
|
return "teleport_message"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// GetRecentVertices fetches recent vertices via RPC
|
|
func (a *Adapter) GetRecentVertices(ctx context.Context, limit int) ([]json.RawMessage, error) {
|
|
req := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tvm.getRecentVertices",
|
|
"params": map[string]interface{}{
|
|
"limit": limit,
|
|
},
|
|
}
|
|
|
|
resp, err := a.rpcCall(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result struct {
|
|
Vertices []json.RawMessage `json:"vertices"`
|
|
}
|
|
if err := json.Unmarshal(resp, &result); err != nil {
|
|
return nil, fmt.Errorf("parse vertices: %w", err)
|
|
}
|
|
|
|
return result.Vertices, nil
|
|
}
|
|
|
|
// GetVertexByID fetches a specific vertex
|
|
func (a *Adapter) GetVertexByID(ctx context.Context, id string) (json.RawMessage, error) {
|
|
req := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tvm.getVertex",
|
|
"params": map[string]interface{}{
|
|
"id": id,
|
|
},
|
|
}
|
|
|
|
resp, err := a.rpcCall(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result struct {
|
|
Vertex json.RawMessage `json:"vertex"`
|
|
}
|
|
if err := json.Unmarshal(resp, &result); err != nil {
|
|
return nil, fmt.Errorf("parse vertex: %w", err)
|
|
}
|
|
|
|
return result.Vertex, nil
|
|
}
|
|
|
|
// GetSigningSession fetches a signing session by ID
|
|
func (a *Adapter) GetSigningSession(ctx context.Context, sessionID string) (*SigningSession, error) {
|
|
req := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tvm.getSigningSession",
|
|
"params": map[string]interface{}{
|
|
"sessionId": sessionID,
|
|
},
|
|
}
|
|
|
|
resp, err := a.rpcCall(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var session SigningSession
|
|
if err := json.Unmarshal(resp, &session); err != nil {
|
|
return nil, fmt.Errorf("parse session: %w", err)
|
|
}
|
|
|
|
return &session, nil
|
|
}
|
|
|
|
// GetKeyGeneration fetches a key generation ceremony by ID
|
|
func (a *Adapter) GetKeyGeneration(ctx context.Context, keyGenID string) (*KeyGeneration, error) {
|
|
req := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tvm.getKeyGeneration",
|
|
"params": map[string]interface{}{
|
|
"keyGenId": keyGenID,
|
|
},
|
|
}
|
|
|
|
resp, err := a.rpcCall(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var keyGen KeyGeneration
|
|
if err := json.Unmarshal(resp, &keyGen); err != nil {
|
|
return nil, fmt.Errorf("parse keygen: %w", err)
|
|
}
|
|
|
|
return &keyGen, nil
|
|
}
|
|
|
|
// GetActiveKeyShares fetches active key shares for a public key
|
|
func (a *Adapter) GetActiveKeyShares(ctx context.Context, publicKey string) ([]KeyShare, error) {
|
|
req := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tvm.getActiveKeyShares",
|
|
"params": map[string]interface{}{
|
|
"publicKey": publicKey,
|
|
},
|
|
}
|
|
|
|
resp, err := a.rpcCall(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result struct {
|
|
KeyShares []KeyShare `json:"keyShares"`
|
|
}
|
|
if err := json.Unmarshal(resp, &result); err != nil {
|
|
return nil, fmt.Errorf("parse key shares: %w", err)
|
|
}
|
|
|
|
return result.KeyShares, nil
|
|
}
|
|
|
|
// InitSchema creates T-Chain specific database tables
|
|
func (a *Adapter) InitSchema(ctx context.Context, store storage.Store) error {
|
|
err := store.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS tchain_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
threshold INTEGER NOT NULL,
|
|
total_shares INTEGER NOT NULL,
|
|
message_hash TEXT NOT NULL,
|
|
participants TEXT DEFAULT '[]',
|
|
signatures TEXT DEFAULT '[]',
|
|
final_sig TEXT,
|
|
status TEXT DEFAULT 'pending',
|
|
source_chain TEXT,
|
|
dest_chain TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
completed_at TIMESTAMP,
|
|
expires_at TIMESTAMP NOT NULL
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("create sessions table: %w", err)
|
|
}
|
|
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_sessions_status ON tchain_sessions(status)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_sessions_message ON tchain_sessions(message_hash)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_sessions_created ON tchain_sessions(created_at DESC)`)
|
|
|
|
err = store.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS tchain_key_shares (
|
|
id TEXT PRIMARY KEY,
|
|
public_key TEXT NOT NULL,
|
|
share_index INTEGER NOT NULL,
|
|
node_id TEXT NOT NULL,
|
|
keygen_id TEXT NOT NULL,
|
|
status TEXT DEFAULT 'pending',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
rotated_at TIMESTAMP,
|
|
expires_at TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("create key shares table: %w", err)
|
|
}
|
|
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_key_shares_pubkey ON tchain_key_shares(public_key)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_key_shares_node ON tchain_key_shares(node_id)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_key_shares_status ON tchain_key_shares(status)`)
|
|
|
|
err = store.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS tchain_key_generations (
|
|
id TEXT PRIMARY KEY,
|
|
threshold INTEGER NOT NULL,
|
|
total_shares INTEGER NOT NULL,
|
|
public_key TEXT,
|
|
participants TEXT DEFAULT '[]',
|
|
status TEXT DEFAULT 'pending',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
completed_at TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("create key generations table: %w", err)
|
|
}
|
|
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_key_gen_status ON tchain_key_generations(status)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_key_gen_pubkey ON tchain_key_generations(public_key)`)
|
|
|
|
err = store.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS tchain_messages (
|
|
id TEXT PRIMARY KEY,
|
|
source_chain TEXT NOT NULL,
|
|
dest_chain TEXT NOT NULL,
|
|
sender TEXT NOT NULL,
|
|
receiver TEXT NOT NULL,
|
|
payload TEXT,
|
|
nonce INTEGER NOT NULL,
|
|
session_id TEXT,
|
|
status TEXT DEFAULT 'pending',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
signed_at TIMESTAMP,
|
|
delivered_at TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("create messages table: %w", err)
|
|
}
|
|
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_messages_source ON tchain_messages(source_chain)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_messages_dest ON tchain_messages(dest_chain)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_messages_status ON tchain_messages(status)`)
|
|
_ = store.Exec(ctx, `CREATE INDEX IF NOT EXISTS idx_tchain_messages_session ON tchain_messages(session_id)`)
|
|
|
|
err = store.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS tchain_mpc_stats (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
total_sessions INTEGER DEFAULT 0,
|
|
active_sessions INTEGER DEFAULT 0,
|
|
completed_sessions INTEGER DEFAULT 0,
|
|
failed_sessions INTEGER DEFAULT 0,
|
|
total_key_shares INTEGER DEFAULT 0,
|
|
active_key_shares INTEGER DEFAULT 0,
|
|
total_key_gens INTEGER DEFAULT 0,
|
|
total_messages INTEGER DEFAULT 0,
|
|
pending_messages INTEGER DEFAULT 0,
|
|
delivered_messages INTEGER DEFAULT 0,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("create mpc stats table: %w", err)
|
|
}
|
|
|
|
_ = store.Exec(ctx, `INSERT INTO tchain_mpc_stats (id) VALUES (1) ON CONFLICT DO NOTHING`)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStats returns T-Chain specific statistics
|
|
func (a *Adapter) GetStats(ctx context.Context, store storage.Store) (map[string]interface{}, error) {
|
|
stats := make(map[string]interface{})
|
|
|
|
var totalSessions, activeSessions, completedSessions, failedSessions int64
|
|
var totalKeyShares, activeKeyShares, totalKeyGens int64
|
|
var totalMessages, pendingMessages, deliveredMessages int64
|
|
|
|
rows, _ := store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_sessions")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
totalSessions = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_sessions WHERE status='active'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
activeSessions = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_sessions WHERE status='completed'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
completedSessions = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_sessions WHERE status='failed'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
failedSessions = v
|
|
}
|
|
}
|
|
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_key_shares")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
totalKeyShares = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_key_shares WHERE status='active'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
activeKeyShares = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_key_generations")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
totalKeyGens = v
|
|
}
|
|
}
|
|
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_messages")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
totalMessages = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_messages WHERE status='pending'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
pendingMessages = v
|
|
}
|
|
}
|
|
rows, _ = store.Query(ctx, "SELECT COUNT(*) as cnt FROM tchain_messages WHERE status='delivered'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["cnt"].(int64); ok {
|
|
deliveredMessages = v
|
|
}
|
|
}
|
|
|
|
stats["mpc_stats"] = map[string]interface{}{
|
|
"total_sessions": totalSessions,
|
|
"active_sessions": activeSessions,
|
|
"completed_sessions": completedSessions,
|
|
"failed_sessions": failedSessions,
|
|
}
|
|
|
|
stats["key_stats"] = map[string]interface{}{
|
|
"total_key_shares": totalKeyShares,
|
|
"active_key_shares": activeKeyShares,
|
|
"total_key_gens": totalKeyGens,
|
|
}
|
|
|
|
stats["message_stats"] = map[string]interface{}{
|
|
"total_messages": totalMessages,
|
|
"pending_messages": pendingMessages,
|
|
"delivered_messages": deliveredMessages,
|
|
}
|
|
|
|
if totalSessions > 0 {
|
|
stats["signing_success_rate"] = float64(completedSessions) / float64(totalSessions) * 100.0
|
|
}
|
|
|
|
rows, _ = store.Query(ctx, "SELECT COALESCE(AVG(threshold), 0) as avg_threshold FROM tchain_key_generations WHERE status='completed'")
|
|
if len(rows) > 0 {
|
|
if v, ok := rows[0]["avg_threshold"].(float64); ok {
|
|
stats["avg_threshold"] = v
|
|
}
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// StoreSession stores a signing session
|
|
func (a *Adapter) StoreSession(ctx context.Context, store storage.Store, s *SigningSession) error {
|
|
participants, _ := json.Marshal(s.Participants)
|
|
signatures, _ := json.Marshal(s.Signatures)
|
|
|
|
return store.Exec(ctx, `
|
|
INSERT INTO tchain_sessions (id, threshold, total_shares, message_hash, participants, signatures, final_sig, status, source_chain, dest_chain, created_at, completed_at, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
threshold = EXCLUDED.threshold,
|
|
total_shares = EXCLUDED.total_shares,
|
|
message_hash = EXCLUDED.message_hash,
|
|
participants = EXCLUDED.participants,
|
|
signatures = EXCLUDED.signatures,
|
|
final_sig = EXCLUDED.final_sig,
|
|
status = EXCLUDED.status,
|
|
source_chain = EXCLUDED.source_chain,
|
|
dest_chain = EXCLUDED.dest_chain,
|
|
created_at = EXCLUDED.created_at,
|
|
completed_at = EXCLUDED.completed_at,
|
|
expires_at = EXCLUDED.expires_at
|
|
`, s.ID, s.Threshold, s.TotalShares, s.MessageHash, string(participants), string(signatures), s.FinalSig, s.Status, s.SourceChain, s.DestChain, s.CreatedAt, s.CompletedAt, s.ExpiresAt)
|
|
}
|
|
|
|
// StoreKeyShare stores a key share
|
|
func (a *Adapter) StoreKeyShare(ctx context.Context, store storage.Store, k *KeyShare) error {
|
|
return store.Exec(ctx, `
|
|
INSERT INTO tchain_key_shares (id, public_key, share_index, node_id, keygen_id, status, created_at, rotated_at, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
public_key = EXCLUDED.public_key,
|
|
share_index = EXCLUDED.share_index,
|
|
node_id = EXCLUDED.node_id,
|
|
keygen_id = EXCLUDED.keygen_id,
|
|
status = EXCLUDED.status,
|
|
created_at = EXCLUDED.created_at,
|
|
rotated_at = EXCLUDED.rotated_at,
|
|
expires_at = EXCLUDED.expires_at
|
|
`, k.ID, k.PublicKey, k.ShareIndex, k.NodeID, k.KeyGenID, k.Status, k.CreatedAt, k.RotatedAt, k.ExpiresAt)
|
|
}
|
|
|
|
// StoreKeyGeneration stores a key generation ceremony
|
|
func (a *Adapter) StoreKeyGeneration(ctx context.Context, store storage.Store, kg *KeyGeneration) error {
|
|
participants, _ := json.Marshal(kg.Participants)
|
|
|
|
return store.Exec(ctx, `
|
|
INSERT INTO tchain_key_generations (id, threshold, total_shares, public_key, participants, status, created_at, completed_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
threshold = EXCLUDED.threshold,
|
|
total_shares = EXCLUDED.total_shares,
|
|
public_key = EXCLUDED.public_key,
|
|
participants = EXCLUDED.participants,
|
|
status = EXCLUDED.status,
|
|
created_at = EXCLUDED.created_at,
|
|
completed_at = EXCLUDED.completed_at
|
|
`, kg.ID, kg.Threshold, kg.TotalShares, kg.PublicKey, string(participants), kg.Status, kg.CreatedAt, kg.CompletedAt)
|
|
}
|
|
|
|
// StoreMessage stores a teleport message
|
|
func (a *Adapter) StoreMessage(ctx context.Context, store storage.Store, m *TeleportMessage) error {
|
|
return store.Exec(ctx, `
|
|
INSERT INTO tchain_messages (id, source_chain, dest_chain, sender, receiver, payload, nonce, session_id, status, created_at, signed_at, delivered_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
source_chain = EXCLUDED.source_chain,
|
|
dest_chain = EXCLUDED.dest_chain,
|
|
sender = EXCLUDED.sender,
|
|
receiver = EXCLUDED.receiver,
|
|
payload = EXCLUDED.payload,
|
|
nonce = EXCLUDED.nonce,
|
|
session_id = EXCLUDED.session_id,
|
|
status = EXCLUDED.status,
|
|
created_at = EXCLUDED.created_at,
|
|
signed_at = EXCLUDED.signed_at,
|
|
delivered_at = EXCLUDED.delivered_at
|
|
`, m.ID, m.SourceChain, m.DestChain, m.Sender, m.Receiver, string(m.Payload), m.Nonce, m.SessionID, m.Status, m.CreatedAt, m.SignedAt, m.DeliveredAt)
|
|
}
|
|
|
|
// rpcCall makes an RPC call to T-Chain
|
|
func (a *Adapter) rpcCall(ctx context.Context, req map[string]interface{}) (json.RawMessage, error) {
|
|
body, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", a.rpcEndpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := a.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rpc call: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var rpcResp struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
if rpcResp.Error != nil {
|
|
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
|
}
|
|
|
|
return rpcResp.Result, nil
|
|
}
|
|
|
|
// ValidateSignature validates a threshold signature.
|
|
// Threshold signature verification requires the T-Chain node; it cannot be done client-side.
|
|
func ValidateSignature(_, _, _ string) (bool, error) {
|
|
return false, fmt.Errorf("threshold signature verification not implemented: must be performed by T-Chain node")
|
|
}
|
|
|
|
// ComputeThreshold computes required threshold for given parameters.
|
|
// Uses t-of-n threshold where t = (n * 2 / 3) + 1 for Byzantine fault tolerance.
|
|
func ComputeThreshold(totalShares uint32) uint32 {
|
|
if totalShares < 2 {
|
|
return 1
|
|
}
|
|
return (totalShares * 2 / 3) + 1
|
|
}
|