mirror of
https://github.com/luxfi/utxo.git
synced 2026-07-27 03:39:23 +00:00
feat: complete mldsafx — ML-DSA-65 PQ signing for X-Chain UTXO
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
func TestCredentialVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sig := make([]byte, MLDSA65SigLen)
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
require.NoError(cred.Verify())
|
||||
}
|
||||
|
||||
func TestCredentialVerifyNil(t *testing.T) {
|
||||
var cred *Credential
|
||||
require.ErrorIs(t, cred.Verify(), ErrNilCredential)
|
||||
}
|
||||
|
||||
func TestCredentialVerifyWrongSigLen(t *testing.T) {
|
||||
sig := make([]byte, 100) // wrong length
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
require.ErrorIs(t, cred.Verify(), ErrMismatchedSecLevel)
|
||||
}
|
||||
|
||||
func TestCredentialVerifyBadSecLevel(t *testing.T) {
|
||||
cred := &Credential{
|
||||
Level: SecurityLevel(99),
|
||||
Sigs: [][]byte{make([]byte, 100)},
|
||||
}
|
||||
require.ErrorIs(t, cred.Verify(), ErrInvalidSecLevel)
|
||||
}
|
||||
|
||||
func TestCredentialVerifyMultipleSigs(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sig1 := make([]byte, MLDSA65SigLen)
|
||||
sig2 := make([]byte, MLDSA65SigLen)
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig1, sig2},
|
||||
}
|
||||
require.NoError(cred.Verify())
|
||||
}
|
||||
|
||||
func TestCredentialVerifyMLDSA44(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sig := make([]byte, MLDSA44SigLen)
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA44,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
require.NoError(cred.Verify())
|
||||
}
|
||||
|
||||
func TestCredentialVerifyMLDSA87(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sig := make([]byte, MLDSA87SigLen)
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA87,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
require.NoError(cred.Verify())
|
||||
}
|
||||
|
||||
func TestNewCredential65(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(err)
|
||||
|
||||
msg := []byte("test")
|
||||
sig, err := sk.Sign(rand.Reader, msg, nil)
|
||||
require.NoError(err)
|
||||
|
||||
cred, err := NewCredential65([][]byte{sig})
|
||||
require.NoError(err)
|
||||
require.Equal(SecLevelMLDSA65, cred.Level)
|
||||
require.Len(cred.Sigs, 1)
|
||||
}
|
||||
|
||||
func TestCredentialJSON(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sig := make([]byte, MLDSA65SigLen)
|
||||
for i := range sig {
|
||||
sig[i] = byte(i % 256)
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
data, err := cred.MarshalJSON()
|
||||
require.NoError(err)
|
||||
|
||||
var decoded Credential
|
||||
require.NoError(decoded.UnmarshalJSON(data))
|
||||
require.Equal(SecLevelMLDSA65, decoded.Level)
|
||||
require.Len(decoded.Sigs, 1)
|
||||
require.Equal(sig, decoded.Sigs[0])
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"github.com/luxfi/vm/fx"
|
||||
)
|
||||
|
||||
const Name = "mldsafx"
|
||||
|
||||
var _ fx.Factory = (*Factory)(nil)
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func (*Factory) New() any {
|
||||
return &Fx{}
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/luxfi/vm/components/verify"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWrongVMType = errors.New("wrong vm type")
|
||||
ErrWrongTxType = errors.New("wrong tx type")
|
||||
ErrWrongOpType = errors.New("wrong operation type")
|
||||
ErrWrongUTXOType = errors.New("wrong utxo type")
|
||||
ErrWrongInputType = errors.New("wrong input type")
|
||||
ErrWrongCredentialType = errors.New("wrong credential type")
|
||||
ErrWrongOwnerType = errors.New("wrong owner type")
|
||||
ErrMismatchedAmounts = errors.New("utxo amount and input amount are not equal")
|
||||
ErrWrongNumberOfUTXOs = errors.New("wrong number of utxos for the operation")
|
||||
ErrWrongMintCreated = errors.New("wrong mint output created from the operation")
|
||||
ErrTimelocked = errors.New("output is time locked")
|
||||
ErrTooManySigners = errors.New("input has more signers than expected")
|
||||
ErrTooFewSigners = errors.New("input has less signers than expected")
|
||||
ErrInputOutputIndexOutOfBounds = errors.New("input referenced a nonexistent address in the output")
|
||||
ErrInputCredentialSignersMismatch = errors.New("input expected a different number of signers than provided in the credential")
|
||||
ErrWrongSig = errors.New("wrong signature")
|
||||
)
|
||||
|
||||
// Fx describes the ML-DSA feature extension for post-quantum secure UTXO spending
|
||||
type Fx struct {
|
||||
VM VM
|
||||
bootstrapped bool
|
||||
}
|
||||
|
||||
func (fx *Fx) Initialize(vmIntf interface{}) error {
|
||||
if err := fx.InitializeVM(vmIntf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := fx.VM.Logger()
|
||||
if !log.IsZero() {
|
||||
log.Debug("initializing mldsafx")
|
||||
}
|
||||
|
||||
if fx.VM == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := fx.VM.CodecRegistry()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
if err := c.RegisterType(&TransferInput{}); err != nil && !strings.Contains(err.Error(), "duplicate type registration") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := c.RegisterType(&MintOutput{}); err != nil && !strings.Contains(err.Error(), "duplicate type registration") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := c.RegisterType(&TransferOutput{}); err != nil && !strings.Contains(err.Error(), "duplicate type registration") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := c.RegisterType(&MintOperation{}); err != nil && !strings.Contains(err.Error(), "duplicate type registration") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := c.RegisterType(&Credential{}); err != nil && !strings.Contains(err.Error(), "duplicate type registration") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (fx *Fx) InitializeVM(vmIntf interface{}) error {
|
||||
vm, ok := vmIntf.(VM)
|
||||
if !ok {
|
||||
return ErrWrongVMType
|
||||
}
|
||||
fx.VM = vm
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Fx) Bootstrapping() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fx *Fx) Bootstrapped() error {
|
||||
fx.bootstrapped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyPermission returns nil iff [credIntf] proves that [ownerIntf] assents to [txIntf]
|
||||
func (fx *Fx) VerifyPermission(txIntf, inIntf, credIntf, ownerIntf interface{}) error {
|
||||
tx, ok := txIntf.(UnsignedTx)
|
||||
if !ok {
|
||||
return ErrWrongTxType
|
||||
}
|
||||
in, ok := inIntf.(*Input)
|
||||
if !ok {
|
||||
return ErrWrongInputType
|
||||
}
|
||||
cred, ok := credIntf.(*Credential)
|
||||
if !ok {
|
||||
return ErrWrongCredentialType
|
||||
}
|
||||
owner, ok := ownerIntf.(*OutputOwners)
|
||||
if !ok {
|
||||
return ErrWrongOwnerType
|
||||
}
|
||||
if err := verify.All(in, cred, owner); err != nil {
|
||||
return err
|
||||
}
|
||||
return fx.VerifyCredentials(tx, in, cred, owner)
|
||||
}
|
||||
|
||||
func (fx *Fx) VerifyOperation(txIntf, opIntf, credIntf interface{}, utxosIntf []interface{}) error {
|
||||
tx, ok := txIntf.(UnsignedTx)
|
||||
if !ok {
|
||||
return ErrWrongTxType
|
||||
}
|
||||
op, ok := opIntf.(*MintOperation)
|
||||
if !ok {
|
||||
return ErrWrongOpType
|
||||
}
|
||||
cred, ok := credIntf.(*Credential)
|
||||
if !ok {
|
||||
return ErrWrongCredentialType
|
||||
}
|
||||
if len(utxosIntf) != 1 {
|
||||
return ErrWrongNumberOfUTXOs
|
||||
}
|
||||
out, ok := utxosIntf[0].(*MintOutput)
|
||||
if !ok {
|
||||
return ErrWrongUTXOType
|
||||
}
|
||||
return fx.verifyOperation(tx, op, cred, out)
|
||||
}
|
||||
|
||||
func (fx *Fx) verifyOperation(tx UnsignedTx, op *MintOperation, cred *Credential, utxo *MintOutput) error {
|
||||
if err := verify.All(op, cred, utxo); err != nil {
|
||||
return err
|
||||
}
|
||||
if !utxo.OutputOwners.Equals(&op.MintOutput.OutputOwners) {
|
||||
return ErrWrongMintCreated
|
||||
}
|
||||
return fx.VerifyCredentials(tx, &op.MintInput, cred, &utxo.OutputOwners)
|
||||
}
|
||||
|
||||
func (fx *Fx) VerifyTransfer(txIntf, inIntf, credIntf, utxoIntf interface{}) error {
|
||||
tx, ok := txIntf.(UnsignedTx)
|
||||
if !ok {
|
||||
return ErrWrongTxType
|
||||
}
|
||||
in, ok := inIntf.(*TransferInput)
|
||||
if !ok {
|
||||
return ErrWrongInputType
|
||||
}
|
||||
cred, ok := credIntf.(*Credential)
|
||||
if !ok {
|
||||
return ErrWrongCredentialType
|
||||
}
|
||||
out, ok := utxoIntf.(*TransferOutput)
|
||||
if !ok {
|
||||
return ErrWrongUTXOType
|
||||
}
|
||||
return fx.VerifySpend(tx, in, cred, out)
|
||||
}
|
||||
|
||||
// VerifySpend ensures that the utxo can be sent to any address
|
||||
func (fx *Fx) VerifySpend(utx UnsignedTx, in *TransferInput, cred *Credential, utxo *TransferOutput) error {
|
||||
if err := verify.All(utxo, in, cred); err != nil {
|
||||
return err
|
||||
}
|
||||
if utxo.Amt != in.Amt {
|
||||
return fmt.Errorf("%w: %d != %d", ErrMismatchedAmounts, utxo.Amt, in.Amt)
|
||||
}
|
||||
return fx.VerifyCredentials(utx, &in.Input, cred, &utxo.OutputOwners)
|
||||
}
|
||||
|
||||
// VerifyCredentials ensures that the output can be spent by the input with the
|
||||
// credential. ML-DSA-65 signatures are verified directly against the public key
|
||||
// stored in OutputOwners.Addrs.
|
||||
func (fx *Fx) VerifyCredentials(utx UnsignedTx, in *Input, cred *Credential, out *OutputOwners) error {
|
||||
numSigs := len(in.SigIndices)
|
||||
switch {
|
||||
case out.Locktime > fx.VM.Clock().Unix():
|
||||
return ErrTimelocked
|
||||
case out.Threshold < uint32(numSigs):
|
||||
return ErrTooManySigners
|
||||
case out.Threshold > uint32(numSigs):
|
||||
return ErrTooFewSigners
|
||||
case numSigs != len(cred.Sigs):
|
||||
return ErrInputCredentialSignersMismatch
|
||||
case !fx.bootstrapped: // disable signature verification during bootstrapping
|
||||
return nil
|
||||
}
|
||||
|
||||
txBytes := utx.Bytes()
|
||||
for i, index := range in.SigIndices {
|
||||
if index >= uint32(len(out.Addrs)) {
|
||||
return ErrInputOutputIndexOutOfBounds
|
||||
}
|
||||
|
||||
sig := cred.Sigs[i]
|
||||
pkBytes := out.Addrs[index]
|
||||
|
||||
// Reconstruct public key and verify ML-DSA-65 signature
|
||||
pk, err := mldsa.PublicKeyFromBytes(pkBytes, mldsaMode(out.Level))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: invalid public key at index %d: %v", ErrWrongSig, index, err)
|
||||
}
|
||||
|
||||
if !pk.VerifySignature(txBytes, sig) {
|
||||
// Derive address for error message
|
||||
addressBytes := hash.PubkeyBytesToAddress(pkBytes)
|
||||
return fmt.Errorf("%w: ML-DSA-65 verification failed for address %x",
|
||||
ErrWrongSig, addressBytes)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateOutput creates a new output with the provided control group worth
|
||||
// the specified amount
|
||||
func (*Fx) CreateOutput(amount uint64, ownerIntf interface{}) (interface{}, error) {
|
||||
owner, ok := ownerIntf.(*OutputOwners)
|
||||
if !ok {
|
||||
return nil, ErrWrongOwnerType
|
||||
}
|
||||
if err := owner.Verify(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TransferOutput{
|
||||
Amt: amount,
|
||||
OutputOwners: *owner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mldsaMode converts SecurityLevel to mldsa.Mode
|
||||
func mldsaMode(level SecurityLevel) mldsa.Mode {
|
||||
switch level {
|
||||
case SecLevelMLDSA44:
|
||||
return mldsa.MLDSA44
|
||||
case SecLevelMLDSA65:
|
||||
return mldsa.MLDSA65
|
||||
case SecLevelMLDSA87:
|
||||
return mldsa.MLDSA87
|
||||
default:
|
||||
return mldsa.MLDSA65
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/codec/linearcodec"
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/luxfi/ids"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
func newTestFx(t *testing.T) (*Fx, *mldsa.PrivateKey, []byte) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(err)
|
||||
|
||||
vm := &TestVM{
|
||||
Codec: linearcodec.NewDefault(),
|
||||
Log: log.NewNoOpLogger(),
|
||||
}
|
||||
vm.Clk.Set(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
fx := &Fx{}
|
||||
require.NoError(fx.Initialize(vm))
|
||||
require.NoError(fx.Bootstrapping())
|
||||
require.NoError(fx.Bootstrapped())
|
||||
|
||||
pkBytes := sk.PublicKey.Bytes()
|
||||
return fx, sk, pkBytes
|
||||
}
|
||||
|
||||
func TestFxInitialize(t *testing.T) {
|
||||
vm := TestVM{
|
||||
Codec: linearcodec.NewDefault(),
|
||||
Log: log.NewNoOpLogger(),
|
||||
}
|
||||
fx := Fx{}
|
||||
require.NoError(t, fx.Initialize(&vm))
|
||||
}
|
||||
|
||||
func TestFxInitializeInvalid(t *testing.T) {
|
||||
fx := Fx{}
|
||||
err := fx.Initialize(nil)
|
||||
require.ErrorIs(t, err, ErrWrongVMType)
|
||||
}
|
||||
|
||||
func TestFxVerifyTransfer(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, sk, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte{0, 1, 2, 3, 4, 5}
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig, err := sk.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
out := &TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
},
|
||||
}
|
||||
in := &TransferInput{
|
||||
Amt: 1,
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
require.NoError(fx.VerifyTransfer(tx, in, cred, out))
|
||||
}
|
||||
|
||||
func TestFxVerifyTransferWrongSig(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, _, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte{0, 1, 2, 3, 4, 5}
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
// Create a bad signature
|
||||
badSig := make([]byte, mldsa.MLDSA65SignatureSize)
|
||||
|
||||
out := &TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
},
|
||||
}
|
||||
in := &TransferInput{
|
||||
Amt: 1,
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{badSig},
|
||||
}
|
||||
|
||||
err := fx.VerifyTransfer(tx, in, cred, out)
|
||||
require.ErrorIs(err, ErrWrongSig)
|
||||
}
|
||||
|
||||
func TestFxVerifyTransferMismatchedAmounts(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, sk, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte{0, 1, 2, 3, 4, 5}
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig, err := sk.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
out := &TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
},
|
||||
}
|
||||
in := &TransferInput{
|
||||
Amt: 2, // mismatched
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
err = fx.VerifyTransfer(tx, in, cred, out)
|
||||
require.ErrorIs(err, ErrMismatchedAmounts)
|
||||
}
|
||||
|
||||
func TestFxVerifyTransferTimelocked(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, sk, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte{0, 1, 2, 3, 4, 5}
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig, err := sk.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
out := &TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: uint64(time.Date(2099, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()),
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
},
|
||||
}
|
||||
in := &TransferInput{
|
||||
Amt: 1,
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
err = fx.VerifyTransfer(tx, in, cred, out)
|
||||
require.ErrorIs(err, ErrTimelocked)
|
||||
}
|
||||
|
||||
func TestFxVerifyCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, sk, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte("test transaction")
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig, err := sk.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
out := &OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
}
|
||||
in := &Input{
|
||||
SigIndices: []uint32{0},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
require.NoError(fx.VerifyCredentials(tx, in, cred, out))
|
||||
}
|
||||
|
||||
func TestFxVerifyCredentialsMultiSig(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, _, _ := newTestFx(t)
|
||||
|
||||
sk1, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(err)
|
||||
sk2, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(err)
|
||||
|
||||
pk1 := sk1.PublicKey.Bytes()
|
||||
pk2 := sk2.PublicKey.Bytes()
|
||||
|
||||
// Sort keys lexicographically
|
||||
if string(pk1) > string(pk2) {
|
||||
pk1, pk2 = pk2, pk1
|
||||
sk1, sk2 = sk2, sk1
|
||||
}
|
||||
|
||||
txBytes := []byte("multi-sig tx")
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig1, err := sk1.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
sig2, err := sk2.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
out := &OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 2,
|
||||
Addrs: [][]byte{pk1, pk2},
|
||||
}
|
||||
in := &Input{
|
||||
SigIndices: []uint32{0, 1},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig1, sig2},
|
||||
}
|
||||
|
||||
require.NoError(fx.VerifyCredentials(tx, in, cred, out))
|
||||
}
|
||||
|
||||
func TestFxVerifyPermission(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, sk, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte("permission tx")
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig, err := sk.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
owner := &OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
}
|
||||
in := &Input{
|
||||
SigIndices: []uint32{0},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
require.NoError(fx.VerifyPermission(tx, in, cred, owner))
|
||||
}
|
||||
|
||||
func TestFxVerifyPermissionWrongTypes(t *testing.T) {
|
||||
fx, _, _ := newTestFx(t)
|
||||
|
||||
require.ErrorIs(t, fx.VerifyPermission("bad", nil, nil, nil), ErrWrongTxType)
|
||||
tx := &TestTx{}
|
||||
require.ErrorIs(t, fx.VerifyPermission(tx, "bad", nil, nil), ErrWrongInputType)
|
||||
require.ErrorIs(t, fx.VerifyPermission(tx, &Input{}, "bad", nil), ErrWrongCredentialType)
|
||||
require.ErrorIs(t, fx.VerifyPermission(tx, &Input{}, &Credential{Level: SecLevelMLDSA65}, "bad"), ErrWrongOwnerType)
|
||||
}
|
||||
|
||||
func TestFxVerifyTransferBootstrapping(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(err)
|
||||
pkBytes := sk.PublicKey.Bytes()
|
||||
|
||||
vm := &TestVM{
|
||||
Codec: linearcodec.NewDefault(),
|
||||
Log: log.NewNoOpLogger(),
|
||||
}
|
||||
vm.Clk.Set(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
fx := &Fx{}
|
||||
require.NoError(fx.Initialize(vm))
|
||||
require.NoError(fx.Bootstrapping())
|
||||
// NOT calling Bootstrapped — should skip sig verification
|
||||
|
||||
txBytes := []byte{0, 1, 2, 3, 4, 5}
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
// Use a bad signature — should still pass during bootstrap
|
||||
badSig := make([]byte, mldsa.MLDSA65SignatureSize)
|
||||
|
||||
out := &TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
},
|
||||
}
|
||||
in := &TransferInput{
|
||||
Amt: 1,
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{badSig},
|
||||
}
|
||||
|
||||
require.NoError(fx.VerifyTransfer(tx, in, cred, out))
|
||||
}
|
||||
|
||||
func TestFxCreateOutput(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, _, pkBytes := newTestFx(t)
|
||||
|
||||
owner := &OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
}
|
||||
|
||||
result, err := fx.CreateOutput(100, owner)
|
||||
require.NoError(err)
|
||||
|
||||
out, ok := result.(*TransferOutput)
|
||||
require.True(ok)
|
||||
require.Equal(uint64(100), out.Amt)
|
||||
}
|
||||
|
||||
func TestFxVerifyOperation(t *testing.T) {
|
||||
require := require.New(t)
|
||||
fx, sk, pkBytes := newTestFx(t)
|
||||
|
||||
txBytes := []byte("mint operation tx")
|
||||
tx := &TestTx{UnsignedBytes: txBytes}
|
||||
|
||||
sig, err := sk.Sign(rand.Reader, txBytes, nil)
|
||||
require.NoError(err)
|
||||
|
||||
owners := OutputOwners{
|
||||
Level: SecLevelMLDSA65,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addrs: [][]byte{pkBytes},
|
||||
}
|
||||
|
||||
utxo := &MintOutput{OutputOwners: owners}
|
||||
op := &MintOperation{
|
||||
MintInput: Input{SigIndices: []uint32{0}},
|
||||
MintOutput: MintOutput{
|
||||
OutputOwners: owners,
|
||||
},
|
||||
TransferOutput: TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: owners,
|
||||
},
|
||||
}
|
||||
cred := &Credential{
|
||||
Level: SecLevelMLDSA65,
|
||||
Sigs: [][]byte{sig},
|
||||
}
|
||||
|
||||
require.NoError(fx.VerifyOperation(tx, op, cred, []interface{}{utxo}))
|
||||
}
|
||||
|
||||
func TestFxAddressDerivation(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(err)
|
||||
|
||||
pkBytes := sk.PublicKey.Bytes()
|
||||
addressBytes := hash.PubkeyBytesToAddress(pkBytes)
|
||||
addr, err := ids.ToShortID(addressBytes)
|
||||
require.NoError(err)
|
||||
require.NotEqual(ids.ShortEmpty, addr)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/math"
|
||||
"github.com/luxfi/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// CostPerSignature is the compute cost per ML-DSA signature verification.
|
||||
// Higher than secp256k1 due to larger signatures and lattice math.
|
||||
CostPerSignature uint64 = 3000
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilInput = errors.New("nil input")
|
||||
ErrInputIndicesNotSortedUnique = errors.New("address indices not sorted and unique")
|
||||
)
|
||||
|
||||
// Input references signature indices into the credential for spending.
|
||||
type Input struct {
|
||||
SigIndices []uint32 `serialize:"true" json:"signatureIndices"`
|
||||
}
|
||||
|
||||
func (in *Input) Cost() (uint64, error) {
|
||||
numSigs := uint64(len(in.SigIndices))
|
||||
return math.Mul64(numSigs, CostPerSignature)
|
||||
}
|
||||
|
||||
// Verify this input is syntactically valid
|
||||
func (in *Input) Verify() error {
|
||||
switch {
|
||||
case in == nil:
|
||||
return ErrNilInput
|
||||
case !utils.IsSortedAndUniqueOrdered(in.SigIndices):
|
||||
return ErrInputIndicesNotSortedUnique
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInputVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
in := &Input{
|
||||
SigIndices: []uint32{0, 1, 2},
|
||||
}
|
||||
require.NoError(in.Verify())
|
||||
}
|
||||
|
||||
func TestInputVerifyNil(t *testing.T) {
|
||||
var in *Input
|
||||
require.ErrorIs(t, in.Verify(), ErrNilInput)
|
||||
}
|
||||
|
||||
func TestInputVerifyUnsorted(t *testing.T) {
|
||||
in := &Input{
|
||||
SigIndices: []uint32{2, 0, 1},
|
||||
}
|
||||
require.ErrorIs(t, in.Verify(), ErrInputIndicesNotSortedUnique)
|
||||
}
|
||||
|
||||
func TestInputVerifyDuplicate(t *testing.T) {
|
||||
in := &Input{
|
||||
SigIndices: []uint32{0, 0, 1},
|
||||
}
|
||||
require.ErrorIs(t, in.Verify(), ErrInputIndicesNotSortedUnique)
|
||||
}
|
||||
|
||||
func TestInputCost(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
in := &Input{
|
||||
SigIndices: []uint32{0, 1},
|
||||
}
|
||||
cost, err := in.Cost()
|
||||
require.NoError(err)
|
||||
require.Equal(uint64(6000), cost)
|
||||
}
|
||||
|
||||
func TestTransferInputVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
in := &TransferInput{
|
||||
Amt: 100,
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
require.NoError(in.Verify())
|
||||
}
|
||||
|
||||
func TestTransferInputVerifyNoValue(t *testing.T) {
|
||||
in := &TransferInput{
|
||||
Amt: 0,
|
||||
Input: Input{
|
||||
SigIndices: []uint32{0},
|
||||
},
|
||||
}
|
||||
require.ErrorIs(t, in.Verify(), ErrNoValueInput)
|
||||
}
|
||||
|
||||
func TestTransferInputVerifyNil(t *testing.T) {
|
||||
var in *TransferInput
|
||||
require.ErrorIs(t, in.Verify(), ErrNilInput)
|
||||
}
|
||||
|
||||
func TestTransferInputAmount(t *testing.T) {
|
||||
in := &TransferInput{Amt: 42}
|
||||
require.Equal(t, uint64(42), in.Amount())
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/keychain"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/vm/components/verify"
|
||||
)
|
||||
|
||||
var (
|
||||
errCantSpend = errors.New("unable to spend this UTXO")
|
||||
|
||||
_ keychain.Signer = (*mldsaSigner)(nil)
|
||||
_ keychain.Keychain = (*Keychain)(nil)
|
||||
)
|
||||
|
||||
// mldsaSigner wraps an ML-DSA private key to implement keychain.Signer
|
||||
type mldsaSigner struct {
|
||||
key *mldsa.PrivateKey
|
||||
}
|
||||
|
||||
func (s *mldsaSigner) SignHash(h []byte) ([]byte, error) {
|
||||
// ML-DSA signs messages directly, not hashes. Use the hash as the message.
|
||||
return s.key.Sign(rand.Reader, h, nil)
|
||||
}
|
||||
|
||||
func (s *mldsaSigner) Sign(msg []byte) ([]byte, error) {
|
||||
return s.key.Sign(rand.Reader, msg, nil)
|
||||
}
|
||||
|
||||
func (s *mldsaSigner) Address() ids.ShortID {
|
||||
pkBytes := s.key.PublicKey.Bytes()
|
||||
addressBytes := hash.PubkeyBytesToAddress(pkBytes)
|
||||
addr, _ := ids.ToShortID(addressBytes)
|
||||
return addr
|
||||
}
|
||||
|
||||
// Keychain is a collection of ML-DSA keys that can be used to spend outputs
|
||||
type Keychain struct {
|
||||
addrToKeyIndex map[ids.ShortID]int
|
||||
|
||||
Addrs set.Set[ids.ShortID]
|
||||
Keys []*mldsa.PrivateKey
|
||||
}
|
||||
|
||||
// NewKeychain returns a new keychain containing [keys]
|
||||
func NewKeychain(keys ...*mldsa.PrivateKey) *Keychain {
|
||||
kc := &Keychain{
|
||||
addrToKeyIndex: make(map[ids.ShortID]int),
|
||||
Addrs: make(set.Set[ids.ShortID]),
|
||||
}
|
||||
for _, key := range keys {
|
||||
kc.Add(key)
|
||||
}
|
||||
return kc
|
||||
}
|
||||
|
||||
// Add a new key to the key chain
|
||||
func (kc *Keychain) Add(key *mldsa.PrivateKey) {
|
||||
pkBytes := key.PublicKey.Bytes()
|
||||
addressBytes := hash.PubkeyBytesToAddress(pkBytes)
|
||||
addr, _ := ids.ToShortID(addressBytes)
|
||||
|
||||
if _, ok := kc.addrToKeyIndex[addr]; !ok {
|
||||
kc.addrToKeyIndex[addr] = len(kc.Keys)
|
||||
kc.Keys = append(kc.Keys, key)
|
||||
kc.Addrs.Add(addr)
|
||||
}
|
||||
}
|
||||
|
||||
// Get a key from the keychain. Returns keychain.Signer.
|
||||
func (kc Keychain) Get(id ids.ShortID) (keychain.Signer, bool) {
|
||||
if i, ok := kc.addrToKeyIndex[id]; ok {
|
||||
return &mldsaSigner{key: kc.Keys[i]}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Addresses returns the set of addresses this keychain manages
|
||||
func (kc Keychain) Addresses() set.Set[ids.ShortID] {
|
||||
return kc.Addrs
|
||||
}
|
||||
|
||||
// New generates a new ML-DSA-65 key pair and adds it to the keychain
|
||||
func (kc *Keychain) New() (*mldsa.PrivateKey, error) {
|
||||
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kc.Add(sk)
|
||||
return sk, nil
|
||||
}
|
||||
|
||||
// Spend attempts to create an input for the given output
|
||||
func (kc *Keychain) Spend(out verify.Verifiable, time uint64) (verify.Verifiable, []*mldsa.PrivateKey, error) {
|
||||
switch out := out.(type) {
|
||||
case *MintOutput:
|
||||
if sigIndices, keys, able := kc.Match(&out.OutputOwners, time); able {
|
||||
return &Input{
|
||||
SigIndices: sigIndices,
|
||||
}, keys, nil
|
||||
}
|
||||
return nil, nil, errCantSpend
|
||||
case *TransferOutput:
|
||||
if sigIndices, keys, able := kc.Match(&out.OutputOwners, time); able {
|
||||
return &TransferInput{
|
||||
Amt: out.Amt,
|
||||
Input: Input{
|
||||
SigIndices: sigIndices,
|
||||
},
|
||||
}, keys, nil
|
||||
}
|
||||
return nil, nil, errCantSpend
|
||||
}
|
||||
return nil, nil, fmt.Errorf("can't spend UTXO because it is unexpected type %T", out)
|
||||
}
|
||||
|
||||
// Match attempts to match a list of addresses up to the provided threshold
|
||||
func (kc *Keychain) Match(owners *OutputOwners, time uint64) ([]uint32, []*mldsa.PrivateKey, bool) {
|
||||
if time < owners.Locktime {
|
||||
return nil, nil, false
|
||||
}
|
||||
sigs := make([]uint32, 0, owners.Threshold)
|
||||
keys := make([]*mldsa.PrivateKey, 0, owners.Threshold)
|
||||
for i := uint32(0); i < uint32(len(owners.Addrs)) && uint32(len(keys)) < owners.Threshold; i++ {
|
||||
// Derive address from stored public key
|
||||
addressBytes := hash.PubkeyBytesToAddress(owners.Addrs[i])
|
||||
addr, err := ids.ToShortID(addressBytes)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if idx, exists := kc.addrToKeyIndex[addr]; exists {
|
||||
sigs = append(sigs, i)
|
||||
keys = append(keys, kc.Keys[idx])
|
||||
}
|
||||
}
|
||||
return sigs, keys, uint32(len(keys)) == owners.Threshold
|
||||
}
|
||||
|
||||
// get returns the raw private key for the given address
|
||||
func (kc Keychain) get(id ids.ShortID) (*mldsa.PrivateKey, bool) {
|
||||
if i, ok := kc.addrToKeyIndex[id]; ok {
|
||||
return kc.Keys[i], true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/vm/components/verify"
|
||||
)
|
||||
|
||||
var errNilMintOperation = errors.New("nil mint operation")
|
||||
|
||||
type MintOperation struct {
|
||||
MintInput Input `serialize:"true" json:"mintInput"`
|
||||
MintOutput MintOutput `serialize:"true" json:"mintOutput"`
|
||||
TransferOutput TransferOutput `serialize:"true" json:"transferOutput"`
|
||||
}
|
||||
|
||||
func (op *MintOperation) Cost() (uint64, error) {
|
||||
return op.MintInput.Cost()
|
||||
}
|
||||
|
||||
func (op *MintOperation) Outs() []verify.State {
|
||||
return []verify.State{&op.MintOutput, &op.TransferOutput}
|
||||
}
|
||||
|
||||
func (op *MintOperation) Verify() error {
|
||||
if op == nil {
|
||||
return errNilMintOperation
|
||||
}
|
||||
return verify.All(&op.MintInput, &op.MintOutput, &op.TransferOutput)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"github.com/luxfi/vm/components/verify"
|
||||
)
|
||||
|
||||
var _ verify.State = (*MintOutput)(nil)
|
||||
|
||||
type MintOutput struct {
|
||||
verify.IsState `serialize:"-" json:"-"`
|
||||
|
||||
OutputOwners `serialize:"true"`
|
||||
}
|
||||
|
||||
func (out *MintOutput) Verify() error {
|
||||
if out == nil {
|
||||
return ErrNilOutputOwners
|
||||
}
|
||||
return out.OutputOwners.Verify()
|
||||
}
|
||||
@@ -99,6 +99,24 @@ func (out *OutputOwners) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Equals returns true if the provided owners create the same condition
|
||||
func (out *OutputOwners) Equals(other *OutputOwners) bool {
|
||||
if out == other {
|
||||
return true
|
||||
}
|
||||
if out == nil || other == nil || out.Level != other.Level ||
|
||||
out.Locktime != other.Locktime || out.Threshold != other.Threshold ||
|
||||
len(out.Addrs) != len(other.Addrs) {
|
||||
return false
|
||||
}
|
||||
for i, addr := range out.Addrs {
|
||||
if string(addr) != string(other.Addrs[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// NewOutputOwners creates a new ML-DSA output owners
|
||||
func NewOutputOwners(level SecurityLevel, locktime uint64, threshold uint32, addrs [][]byte) (*OutputOwners, error) {
|
||||
out := &OutputOwners{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/runtime"
|
||||
)
|
||||
|
||||
var ErrNoValueInput = errors.New("input has no value")
|
||||
|
||||
type TransferInput struct {
|
||||
Amt uint64 `serialize:"true" json:"amount"`
|
||||
Input `serialize:"true"`
|
||||
}
|
||||
|
||||
func (*TransferInput) InitRuntime(*runtime.Runtime) {}
|
||||
|
||||
// Amount returns the quantity of the asset this input produces
|
||||
func (in *TransferInput) Amount() uint64 {
|
||||
return in.Amt
|
||||
}
|
||||
|
||||
// Verify this input is syntactically valid
|
||||
func (in *TransferInput) Verify() error {
|
||||
switch {
|
||||
case in == nil:
|
||||
return ErrNilInput
|
||||
case in.Amt == 0:
|
||||
return ErrNoValueInput
|
||||
default:
|
||||
return in.Input.Verify()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/vm/components/verify"
|
||||
)
|
||||
|
||||
var (
|
||||
_ verify.State = (*TransferOutput)(nil)
|
||||
|
||||
ErrNoValueOutput = errors.New("output has no value")
|
||||
)
|
||||
|
||||
type TransferOutput struct {
|
||||
verify.IsState `serialize:"-" json:"-"`
|
||||
|
||||
Amt uint64 `serialize:"true" json:"amount"`
|
||||
OutputOwners `serialize:"true"`
|
||||
}
|
||||
|
||||
// Amount returns the quantity of the asset this output consumes
|
||||
func (out *TransferOutput) Amount() uint64 {
|
||||
return out.Amt
|
||||
}
|
||||
|
||||
func (out *TransferOutput) Verify() error {
|
||||
switch {
|
||||
case out == nil:
|
||||
return ErrNilOutputOwners
|
||||
case out.Amt == 0:
|
||||
return ErrNoValueOutput
|
||||
default:
|
||||
return out.OutputOwners.Verify()
|
||||
}
|
||||
}
|
||||
|
||||
func (out *TransferOutput) Owners() interface{} {
|
||||
return &out.OutputOwners
|
||||
}
|
||||
|
||||
func (*TransferOutput) isState() {}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
// UnsignedTx that this Fx is supporting
|
||||
type UnsignedTx interface {
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
var _ UnsignedTx = (*TestTx)(nil)
|
||||
|
||||
// TestTx is a minimal implementation of a Tx
|
||||
type TestTx struct{ UnsignedBytes []byte }
|
||||
|
||||
// Bytes returns UnsignedBytes
|
||||
func (tx *TestTx) Bytes() []byte {
|
||||
return tx.UnsignedBytes
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mldsafx
|
||||
|
||||
import (
|
||||
"github.com/luxfi/codec"
|
||||
log "github.com/luxfi/log"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
)
|
||||
|
||||
// VM that this Fx must be run by
|
||||
type VM interface {
|
||||
CodecRegistry() codec.Registry
|
||||
Clock() *mockable.Clock
|
||||
Logger() log.Logger
|
||||
}
|
||||
|
||||
var _ VM = (*TestVM)(nil)
|
||||
|
||||
// TestVM is a minimal implementation of a VM
|
||||
type TestVM struct {
|
||||
Clk mockable.Clock
|
||||
Codec codec.Registry
|
||||
Log log.Logger
|
||||
}
|
||||
|
||||
func (vm *TestVM) Clock() *mockable.Clock {
|
||||
return &vm.Clk
|
||||
}
|
||||
|
||||
func (vm *TestVM) CodecRegistry() codec.Registry {
|
||||
return vm.Codec
|
||||
}
|
||||
|
||||
func (vm *TestVM) Logger() log.Logger {
|
||||
return vm.Log
|
||||
}
|
||||
Reference in New Issue
Block a user