mirror of
https://github.com/luxfi/utxo.git
synced 2026-10-30 15:13:57 +00:00
198 lines
5.9 KiB
Go
198 lines
5.9 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package bls12381fx
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/cache/lru"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/ids"
|
|
log "github.com/luxfi/log"
|
|
"github.com/luxfi/timer/mockable"
|
|
"github.com/luxfi/vm/components/verify"
|
|
)
|
|
|
|
const verifyCacheSize = 256
|
|
|
|
// attestationSignCtx is the domain-separation tag for attestation aggregate
|
|
// signatures, per FIPS-style domain separation.
|
|
var attestationSignCtx = []byte("lux-x-chain-attestation-bls12381-v1")
|
|
|
|
var (
|
|
ErrWrongVMType = errors.New("wrong vm type")
|
|
ErrNotTransferable = errors.New("bls12381fx outputs are not transferable (attestation-only)")
|
|
ErrWrongOpType = errors.New("wrong operation type")
|
|
ErrWrongUTXOType = errors.New("wrong utxo type")
|
|
ErrWrongInputType = errors.New("wrong input type")
|
|
ErrWrongCredentialType = errors.New("wrong credential type")
|
|
ErrWrongNumberOfUTXOs = errors.New("wrong number of utxos for the operation")
|
|
ErrSignerBitmapPopcount = errors.New("signer bitmap popcount does not meet threshold")
|
|
ErrSignerBitmapOutOfRange = errors.New("signer bitmap references pubkey out of range")
|
|
)
|
|
|
|
// VM is the interface this Fx requires. ZAP-native: no runtime codec
|
|
// registration — wire schemas are compile-time static in wire.go.
|
|
type VM interface {
|
|
Clock() *mockable.Clock
|
|
Logger() log.Logger
|
|
}
|
|
|
|
var _ VM = (*TestVM)(nil)
|
|
|
|
// TestVM is a minimal VM for tests.
|
|
type TestVM struct {
|
|
Clk mockable.Clock
|
|
Log log.Logger
|
|
}
|
|
|
|
func (vm *TestVM) Clock() *mockable.Clock { return &vm.Clk }
|
|
func (vm *TestVM) Logger() log.Logger { return vm.Log }
|
|
|
|
// UnsignedTx is what this Fx is signing over.
|
|
type UnsignedTx interface{ Bytes() []byte }
|
|
|
|
type verifyKey = ids.ID
|
|
|
|
// Fx implements the BLS12-381 attestation-only feature extension.
|
|
type Fx struct {
|
|
VM VM
|
|
bootstrapped bool
|
|
verifyCache *lru.Cache[verifyKey, bool]
|
|
}
|
|
|
|
func (fx *Fx) Initialize(vmIntf interface{}) error {
|
|
vm, ok := vmIntf.(VM)
|
|
if !ok {
|
|
return ErrWrongVMType
|
|
}
|
|
fx.VM = vm
|
|
fx.verifyCache = lru.NewCache[verifyKey, bool](verifyCacheSize)
|
|
|
|
logr := fx.VM.Logger()
|
|
if !logr.IsZero() {
|
|
logr.Debug("initializing bls12381fx (attestation-only)")
|
|
}
|
|
// ZAP-native: wire schemas are compile-time static in bls12381fx/wire.go.
|
|
// No runtime codec registration needed.
|
|
return nil
|
|
}
|
|
|
|
func (*Fx) Bootstrapping() error { return nil }
|
|
|
|
func (fx *Fx) Bootstrapped() error {
|
|
fx.bootstrapped = true
|
|
return nil
|
|
}
|
|
|
|
// VerifyTransfer is intentionally unsupported. bls12381fx outputs are
|
|
// attestation records and cannot be spent as values. Use secp256k1fx,
|
|
// ed25519fx, secp256r1fx, mldsafx, slhdsafx, or schnorrfx for retail spends.
|
|
func (*Fx) VerifyTransfer(_, _, _, _ interface{}) error {
|
|
return ErrNotTransferable
|
|
}
|
|
|
|
// VerifyOperation verifies an AttestationInput + Credential against an
|
|
// AttestationOutput UTXO: the aggregate signature must validate under the
|
|
// aggregate of the pubkeys referenced by the signer bitmap, and the bitmap
|
|
// must reference at least Threshold pubkeys.
|
|
func (fx *Fx) VerifyOperation(txIntf, opIntf, credIntf interface{}, utxosIntf []interface{}) error {
|
|
_, ok := txIntf.(UnsignedTx)
|
|
if !ok {
|
|
return fmt.Errorf("%w: expected UnsignedTx, got %T", ErrWrongOpType, txIntf)
|
|
}
|
|
in, ok := opIntf.(*AttestationInput)
|
|
if !ok {
|
|
return ErrWrongInputType
|
|
}
|
|
cred, ok := credIntf.(*Credential)
|
|
if !ok {
|
|
return ErrWrongCredentialType
|
|
}
|
|
if len(utxosIntf) != 1 {
|
|
return ErrWrongNumberOfUTXOs
|
|
}
|
|
out, ok := utxosIntf[0].(*AttestationOutput)
|
|
if !ok {
|
|
return ErrWrongUTXOType
|
|
}
|
|
if err := verify.All(out, in, cred); err != nil {
|
|
return err
|
|
}
|
|
if !fx.bootstrapped {
|
|
return nil
|
|
}
|
|
return fx.verifyAttestation(in, cred, out)
|
|
}
|
|
|
|
// verifyAttestation performs the actual BLS aggregate signature check.
|
|
func (fx *Fx) verifyAttestation(in *AttestationInput, cred *Credential, out *AttestationOutput) error {
|
|
setBits := bitmapSetBits(in.Signers, len(out.PubKeys))
|
|
if uint32(len(setBits)) < out.Threshold {
|
|
return fmt.Errorf("%w: popcount %d < threshold %d",
|
|
ErrSignerBitmapPopcount, len(setBits), out.Threshold)
|
|
}
|
|
// Reject bits set beyond the pubkey slice.
|
|
totalSet := int(popcountBitmap(in.Signers))
|
|
if totalSet != len(setBits) {
|
|
return fmt.Errorf("%w: bitmap references bits beyond %d pubkeys",
|
|
ErrSignerBitmapOutOfRange, len(out.PubKeys))
|
|
}
|
|
|
|
// Cache lookup keyed on (bitmap, AttestedHash, AggSig) — distinct attestations get distinct keys.
|
|
cacheKey := verifyCacheKey(in.Signers, out.AttestedHash[:], cred.AggSig[:])
|
|
if valid, ok := fx.verifyCache.Get(cacheKey); ok {
|
|
if !valid {
|
|
return ErrInvalidAggSig
|
|
}
|
|
return nil
|
|
}
|
|
|
|
pks := make([]*bls.PublicKey, 0, len(setBits))
|
|
for _, i := range setBits {
|
|
pk, err := bls.PublicKeyFromCompressedBytes(out.PubKeys[i])
|
|
if err != nil {
|
|
fx.verifyCache.Put(cacheKey, false)
|
|
return fmt.Errorf("%w: pubkey %d: %v", ErrInvalidPubKeyLen, i, err)
|
|
}
|
|
pks = append(pks, pk)
|
|
}
|
|
aggPk, err := bls.AggregatePublicKeys(pks)
|
|
if err != nil {
|
|
fx.verifyCache.Put(cacheKey, false)
|
|
return fmt.Errorf("%w: %v", ErrAggregatePubKeys, err)
|
|
}
|
|
sig, err := bls.SignatureFromBytes(cred.AggSig[:])
|
|
if err != nil {
|
|
fx.verifyCache.Put(cacheKey, false)
|
|
return fmt.Errorf("%w: %v", ErrWrongAggSigLen, err)
|
|
}
|
|
|
|
msg := make([]byte, 0, len(attestationSignCtx)+len(out.AttestedHash))
|
|
msg = append(msg, attestationSignCtx...)
|
|
msg = append(msg, out.AttestedHash[:]...)
|
|
valid := bls.Verify(aggPk, sig, msg)
|
|
fx.verifyCache.Put(cacheKey, valid)
|
|
if !valid {
|
|
return ErrInvalidAggSig
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyCacheKey(bitmap, attestedHash, sig []byte) verifyKey {
|
|
a := hash.ComputeHash256(bitmap)
|
|
b := hash.ComputeHash256(attestedHash)
|
|
c := hash.ComputeHash256(sig)
|
|
combined := make([]byte, 0, len(a)+len(b)+len(c))
|
|
combined = append(combined, a...)
|
|
combined = append(combined, b...)
|
|
combined = append(combined, c...)
|
|
h := hash.ComputeHash256(combined)
|
|
var id ids.ID
|
|
copy(id[:], h)
|
|
return id
|
|
}
|