mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
Squashed batch covering:
- protocols/pulsar/: lattice threshold lane (round-based wrapper for
github.com/luxfi/pulsar kernel); doc.go documents Photon/Lumen/Beam/
Pulsar/Pulse/Prism/Horizon/Quasar vocabulary stack from LP-105
- protocols/lens/: planned curve threshold sister kernel (design
intent only; mirrors Pulsar shape; replaces stub LSS-FROST)
- protocols/lss/lss_pulsar.go + tests: LSS-Pulsar adapter
(DynamicResharePulsar + PulsarSnapshotManager + BuildActivationTranscript).
Now SHA3-suite-aware: PulsarConfig carries NebulaRoot, HashSuiteID,
ImplementationVersion as optional transcript-binding fields that
flow through to the activation message.
10/10 acceptance tests pass:
1. GroupKey preservation
2. KeyEraID preservation
3. Generation +1
4. RollbackFrom=0 on forward
5. t_old != t_new
6. Disjoint set rotation
7. Valid signature under unchanged GroupKey
8. Pairwise material regenerated
9. Rollback semantics
10. Error surface
Plus 1 new test for BuildActivationTranscript Nebula+suite fields.
- protocols/lss/lss_frost.go: marked DEPRECATED (placeholder Sign()/
Refresh(), single-process simulation, hardcoded ChainKey/RID); will
be replaced by lss_lens.go.
- corona KAT cross-oracle vs Go reference (N=16 deterministic seeds)
- corona+protocol+harness hardening against parallel-run flakes
- lss/adapters: drop NewXRPL/NewCardano panic; ship real XRPL address
derivation
- corona SignWithConfig API expansion + test fixes
- v1.6.5 changelog
- go.sum h1 hashes for luxfi/log and 4 deps
Architecture (LP-105 / pulsar/DESIGN.md):
LSS owns lifecycle (Generation, Rollback, snapshots, dealer/
coordinator role separation). Pulsar owns lattice math (R_q shares,
lattice Pedersen commits, Sign1/Sign2/Combine, Pulsar-SHA3 hash
profile). The lss_pulsar adapter wires them. lss_lens (planned) does
the same for curve math.
go.mod adds local replace github.com/luxfi/pulsar => ../pulsar.
371 lines
10 KiB
Go
371 lines
10 KiB
Go
// Package adapters - Solana adapter for Ed25519 threshold signatures
|
|
package adapters
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/threshold/pkg/math/curve"
|
|
)
|
|
|
|
// SolanaAdapter implements SignerAdapter for Solana
|
|
type SolanaAdapter struct {
|
|
group curve.Curve
|
|
}
|
|
|
|
// NewSolanaAdapter creates a new Solana adapter
|
|
func NewSolanaAdapter() *SolanaAdapter {
|
|
return &SolanaAdapter{
|
|
group: curve.Ed25519{},
|
|
}
|
|
}
|
|
|
|
// Digest computes Solana transaction digest
|
|
func (s *SolanaAdapter) Digest(tx interface{}) ([]byte, error) {
|
|
switch v := tx.(type) {
|
|
case *SolanaTransaction:
|
|
return s.digestTransaction(v)
|
|
case *SolanaMessage:
|
|
return s.digestMessage(v)
|
|
case []byte:
|
|
// Raw message bytes
|
|
return v, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported Solana transaction type: %T", tx)
|
|
}
|
|
}
|
|
|
|
// digestTransaction computes digest for Solana transaction
|
|
func (s *SolanaAdapter) digestTransaction(tx *SolanaTransaction) ([]byte, error) {
|
|
// Serialize message for signing
|
|
message := s.serializeMessage(tx.Message)
|
|
|
|
// Hash the message for signing
|
|
hash := sha256.Sum256(message)
|
|
return hash[:], nil
|
|
}
|
|
|
|
// digestMessage computes digest for Solana message
|
|
func (s *SolanaAdapter) digestMessage(msg *SolanaMessage) ([]byte, error) {
|
|
serialized := s.serializeMessage(msg)
|
|
// Hash the serialized message for signing
|
|
hash := sha256.Sum256(serialized)
|
|
return hash[:], nil
|
|
}
|
|
|
|
// SignEC creates Ed25519 partial signature for Solana
|
|
func (s *SolanaAdapter) SignEC(digest []byte, share Share) (PartialSig, error) {
|
|
// This would integrate with FROST protocol for Ed25519
|
|
// For testing, provide a placeholder R value
|
|
z, err := coerceScalar(s.group, share.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("solana: %w", err)
|
|
}
|
|
return &EdDSAPartialSig{
|
|
PartyID: share.ID,
|
|
R: s.group.NewBasePoint(), // Placeholder for testing
|
|
Z: z,
|
|
}, nil
|
|
}
|
|
|
|
// AggregateEC combines Ed25519 partial signatures
|
|
func (s *SolanaAdapter) AggregateEC(parts []PartialSig) (FullSig, error) {
|
|
if len(parts) == 0 {
|
|
return nil, errors.New("no partial signatures")
|
|
}
|
|
|
|
var r curve.Point
|
|
z := s.group.NewScalar()
|
|
expectedCurve := s.group.Name()
|
|
|
|
for i, part := range parts {
|
|
eddsaPart, ok := part.(*EdDSAPartialSig)
|
|
if !ok {
|
|
return nil, errors.New("invalid Ed25519 partial signature")
|
|
}
|
|
|
|
if eddsaPart.Z == nil {
|
|
return nil, fmt.Errorf("solana: ed25519 partial[%d] has nil scalar Z", i)
|
|
}
|
|
if got := eddsaPart.Z.Curve().Name(); got != expectedCurve {
|
|
return nil, fmt.Errorf("solana: ed25519 partial[%d] scalar Z on wrong curve (got %s, want %s)", i, got, expectedCurve)
|
|
}
|
|
|
|
if r == nil && eddsaPart.R != nil {
|
|
r = eddsaPart.R
|
|
}
|
|
|
|
z = z.Add(eddsaPart.Z)
|
|
}
|
|
|
|
return &EdDSAFullSig{
|
|
R: r,
|
|
Z: z,
|
|
}, nil
|
|
}
|
|
|
|
// Encode formats Ed25519 signature for Solana
|
|
func (s *SolanaAdapter) Encode(full FullSig) ([]byte, error) {
|
|
eddsaSig, ok := full.(*EdDSAFullSig)
|
|
if !ok {
|
|
return nil, errors.New("invalid Ed25519 signature")
|
|
}
|
|
|
|
// Ed25519 signature: R (32 bytes) || z (32 bytes)
|
|
sig := make([]byte, 64)
|
|
|
|
// Copy R
|
|
rBytes, _ := eddsaSig.R.MarshalBinary()
|
|
// Skip format byte if present (first byte is 0x02, 0x03, or 0x04)
|
|
if len(rBytes) == 33 && (rBytes[0] == 0x02 || rBytes[0] == 0x03 || rBytes[0] == 0x04) {
|
|
rBytes = rBytes[1:] // Skip format byte
|
|
}
|
|
if len(rBytes) != 32 {
|
|
return nil, fmt.Errorf("invalid R length: %d", len(rBytes))
|
|
}
|
|
copy(sig[:32], rBytes)
|
|
|
|
// Copy z
|
|
zBytes, _ := eddsaSig.Z.MarshalBinary()
|
|
if len(zBytes) > 32 {
|
|
return nil, fmt.Errorf("invalid z length: %d", len(zBytes))
|
|
}
|
|
copy(sig[32+(32-len(zBytes)):], zBytes)
|
|
|
|
return sig, nil
|
|
}
|
|
|
|
// ValidateConfig validates Solana-specific configuration
|
|
func (s *SolanaAdapter) ValidateConfig(config *UnifiedConfig) error {
|
|
if config.SignatureScheme != SignatureEdDSA {
|
|
return errors.New("Solana requires Ed25519 signatures")
|
|
}
|
|
|
|
if _, ok := config.Group.(curve.Ed25519); !ok {
|
|
return errors.New("Solana requires Ed25519 curve")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// serializeMessage serializes a Solana message for signing
|
|
func (s *SolanaAdapter) serializeMessage(msg *SolanaMessage) []byte {
|
|
var buf []byte
|
|
|
|
// Number of signatures required
|
|
buf = append(buf, msg.NumRequiredSignatures)
|
|
|
|
// Number of read-only signed accounts
|
|
buf = append(buf, msg.NumReadonlySignedAccounts)
|
|
|
|
// Number of read-only unsigned accounts
|
|
buf = append(buf, msg.NumReadonlyUnsignedAccounts)
|
|
|
|
// Account keys (compact array)
|
|
buf = append(buf, s.encodeCompactArray(len(msg.AccountKeys))...)
|
|
for _, key := range msg.AccountKeys {
|
|
buf = append(buf, key[:]...)
|
|
}
|
|
|
|
// Recent blockhash
|
|
buf = append(buf, msg.RecentBlockhash[:]...)
|
|
|
|
// Instructions (compact array)
|
|
buf = append(buf, s.encodeCompactArray(len(msg.Instructions))...)
|
|
for _, inst := range msg.Instructions {
|
|
buf = append(buf, s.serializeInstruction(inst)...)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// serializeInstruction serializes a Solana instruction
|
|
func (s *SolanaAdapter) serializeInstruction(inst *SolanaInstruction) []byte {
|
|
var buf []byte
|
|
|
|
// Program ID index
|
|
buf = append(buf, inst.ProgramIDIndex)
|
|
|
|
// Account indices (compact array)
|
|
buf = append(buf, s.encodeCompactArray(len(inst.AccountIndices))...)
|
|
buf = append(buf, inst.AccountIndices...)
|
|
|
|
// Data (compact array)
|
|
buf = append(buf, s.encodeCompactArray(len(inst.Data))...)
|
|
buf = append(buf, inst.Data...)
|
|
|
|
return buf
|
|
}
|
|
|
|
// encodeCompactArray encodes length in Solana's compact format
|
|
func (s *SolanaAdapter) encodeCompactArray(length int) []byte {
|
|
// Compact-u16 encoding
|
|
if length < 0x7f {
|
|
return []byte{byte(length)}
|
|
} else if length < 0x3fff {
|
|
return []byte{
|
|
byte(length&0x7f | 0x80),
|
|
byte(length >> 7),
|
|
}
|
|
} else {
|
|
return []byte{
|
|
byte(length&0x7f | 0x80),
|
|
byte((length>>7)&0x7f | 0x80),
|
|
byte(length >> 14),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solana transaction structures
|
|
|
|
type SolanaTransaction struct {
|
|
Signatures []Signature
|
|
Message *SolanaMessage
|
|
}
|
|
|
|
type SolanaMessage struct {
|
|
NumRequiredSignatures byte
|
|
NumReadonlySignedAccounts byte
|
|
NumReadonlyUnsignedAccounts byte
|
|
AccountKeys [][32]byte
|
|
RecentBlockhash [32]byte
|
|
Instructions []*SolanaInstruction
|
|
}
|
|
|
|
type SolanaInstruction struct {
|
|
ProgramIDIndex byte
|
|
AccountIndices []byte
|
|
Data []byte
|
|
}
|
|
|
|
type Signature [64]byte
|
|
|
|
// CreateTransferInstruction creates a SOL transfer instruction
|
|
func (s *SolanaAdapter) CreateTransferInstruction(from, to [32]byte, lamports uint64) *SolanaInstruction {
|
|
// System program transfer instruction
|
|
// Instruction: 2 (transfer)
|
|
// Args: lamports (u64)
|
|
|
|
data := []byte{2, 0, 0, 0} // Transfer instruction
|
|
lamportBytes := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(lamportBytes, lamports)
|
|
data = append(data, lamportBytes...)
|
|
|
|
return &SolanaInstruction{
|
|
ProgramIDIndex: 0, // System program
|
|
AccountIndices: []byte{0, 1}, // From, To
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
// CreateTokenTransferInstruction creates SPL token transfer instruction
|
|
func (s *SolanaAdapter) CreateTokenTransferInstruction(amount uint64, decimals byte) *SolanaInstruction {
|
|
// SPL Token transfer instruction
|
|
// Instruction: 3 (transfer)
|
|
// Args: amount (u64)
|
|
|
|
data := []byte{3} // Transfer instruction
|
|
amountBytes := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(amountBytes, amount)
|
|
data = append(data, amountBytes...)
|
|
|
|
return &SolanaInstruction{
|
|
ProgramIDIndex: 2, // Token program
|
|
AccountIndices: []byte{0, 1, 2}, // Source, Dest, Authority
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
// VerifyEd25519Signature verifies an Ed25519 signature on-chain
|
|
func (s *SolanaAdapter) VerifyEd25519Signature(pubkey [32]byte, message []byte, signature [64]byte) bool {
|
|
// This would be verified by Solana's ed25519 program
|
|
// Address: Ed25519SigVerify111111111111111111111111111
|
|
return true // Placeholder
|
|
}
|
|
|
|
// ComputeProgramDerivedAddress computes a PDA for threshold wallets
|
|
func (s *SolanaAdapter) ComputeProgramDerivedAddress(programID [32]byte, seeds [][]byte) ([32]byte, byte, error) {
|
|
// PDA = hash(seeds || programID || bump)
|
|
// Find bump that produces off-curve point
|
|
|
|
for bump := byte(255); bump > 0; bump-- {
|
|
h := sha256.New()
|
|
for _, seed := range seeds {
|
|
h.Write(seed)
|
|
}
|
|
h.Write([]byte{bump})
|
|
h.Write([]byte("ProgramDerivedAddress"))
|
|
h.Write(programID[:])
|
|
|
|
hash := h.Sum(nil)
|
|
|
|
// Check if point is off-curve (valid PDA)
|
|
// Simplified check - actual would verify Ed25519 curve equation
|
|
if hash[31]&0x80 == 0 {
|
|
var pda [32]byte
|
|
copy(pda[:], hash)
|
|
return pda, bump, nil
|
|
}
|
|
}
|
|
|
|
return [32]byte{}, 0, errors.New("unable to find valid PDA")
|
|
}
|
|
|
|
// CreateMultisigAccount creates a Solana multisig account
|
|
func (s *SolanaAdapter) CreateMultisigAccount(signers [][32]byte, threshold byte) ([]byte, error) {
|
|
// Create multisig account data
|
|
// Format: [threshold, num_signers, signer1, signer2, ...]
|
|
|
|
data := []byte{threshold, byte(len(signers))}
|
|
for _, signer := range signers {
|
|
data = append(data, signer[:]...)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// EstimateComputeUnits estimates compute units for threshold operations
|
|
func (s *SolanaAdapter) EstimateComputeUnits(numSignatures int) uint32 {
|
|
// Base cost for Ed25519 verification
|
|
baseCost := uint32(900)
|
|
|
|
// Additional cost per signature
|
|
perSigCost := uint32(200)
|
|
|
|
return baseCost + uint32(numSignatures)*perSigCost
|
|
}
|
|
|
|
// GetRentExemptBalance calculates rent-exempt balance for account
|
|
func (s *SolanaAdapter) GetRentExemptBalance(dataSize int) uint64 {
|
|
// Solana rent calculation
|
|
// ~2 years of rent = 19.055441478439427 * dataSize + 128
|
|
rentPerByte := uint64(19055441478439427) // In lamports
|
|
baseRent := uint64(128 * 1000000000) // 128 SOL base
|
|
|
|
return (rentPerByte*uint64(dataSize))/1000000000000000 + baseRent
|
|
}
|
|
|
|
// SolanaConfig represents Solana-specific configuration
|
|
type SolanaConfig struct {
|
|
Cluster string // mainnet-beta, testnet, devnet
|
|
CommitmentLevel string // processed, confirmed, finalized
|
|
SkipPreflight bool
|
|
PreflightCommit string
|
|
MaxRetries int
|
|
MinContextSlot uint64
|
|
}
|
|
|
|
// GetDefaultConfig returns default Solana configuration
|
|
func GetDefaultSolanaConfig(cluster string) *SolanaConfig {
|
|
return &SolanaConfig{
|
|
Cluster: cluster,
|
|
CommitmentLevel: "confirmed",
|
|
SkipPreflight: false,
|
|
PreflightCommit: "confirmed",
|
|
MaxRetries: 3,
|
|
MinContextSlot: 0,
|
|
}
|
|
}
|