From ff97d51550d058fed37b43a2201bd7eda2baa5c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 23:08:50 +0000 Subject: [PATCH] feat(wallet): NLSAGSigner with ring signature interface Signer interface abstracts signature generation for v1/v2+ extensibility. NLSAGSigner wraps the CGo ring signature functions for v0/v1 transactions. Co-Authored-By: Charon --- wallet/signer.go | 61 +++++++++++++++++++++ wallet/signer_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 wallet/signer.go create mode 100644 wallet/signer_test.go diff --git a/wallet/signer.go b/wallet/signer.go new file mode 100644 index 0000000..842e684 --- /dev/null +++ b/wallet/signer.go @@ -0,0 +1,61 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// You may obtain a copy of the licence at: +// +// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl +// +// SPDX-License-Identifier: EUPL-1.2 + +package wallet + +import ( + "fmt" + + "forge.lthn.ai/core/go-blockchain/crypto" + "forge.lthn.ai/core/go-blockchain/types" +) + +// Signer produces signatures for transaction inputs. +type Signer interface { + SignInput(prefixHash types.Hash, ephemeral KeyPair, + ring []types.PublicKey, realIndex int) ([]types.Signature, error) + Version() uint64 +} + +// NLSAGSigner signs using NLSAG ring signatures (v0/v1 transactions). +type NLSAGSigner struct{} + +// SignInput generates a ring signature for a single transaction input. It +// derives the key image from the ephemeral key pair, then produces one +// signature element per ring member. +func (s *NLSAGSigner) SignInput(prefixHash types.Hash, ephemeral KeyPair, + ring []types.PublicKey, realIndex int) ([]types.Signature, error) { + + ki, err := crypto.GenerateKeyImage( + [32]byte(ephemeral.Public), [32]byte(ephemeral.Secret)) + if err != nil { + return nil, fmt.Errorf("wallet: key image: %w", err) + } + + pubs := make([][32]byte, len(ring)) + for i, k := range ring { + pubs[i] = [32]byte(k) + } + + rawSigs, err := crypto.GenerateRingSignature( + [32]byte(prefixHash), ki, pubs, + [32]byte(ephemeral.Secret), realIndex) + if err != nil { + return nil, fmt.Errorf("wallet: ring signature: %w", err) + } + + sigs := make([]types.Signature, len(rawSigs)) + for i, rs := range rawSigs { + sigs[i] = types.Signature(rs) + } + return sigs, nil +} + +// Version returns the transaction version this signer targets. +func (s *NLSAGSigner) Version() uint64 { return 1 } diff --git a/wallet/signer_test.go b/wallet/signer_test.go new file mode 100644 index 0000000..ca8a3e6 --- /dev/null +++ b/wallet/signer_test.go @@ -0,0 +1,125 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package wallet + +import ( + "testing" + + "forge.lthn.ai/core/go-blockchain/crypto" + "forge.lthn.ai/core/go-blockchain/types" +) + +func TestNLSAGSignerRoundTrip(t *testing.T) { + pub, sec, err := crypto.GenerateKeys() + if err != nil { + t.Fatal(err) + } + ki, err := crypto.GenerateKeyImage(pub, sec) + if err != nil { + t.Fatal(err) + } + + // Build ring of 3 with our key at index 1. + ring := make([]types.PublicKey, 3) + for i := range ring { + p, _, err := crypto.GenerateKeys() + if err != nil { + t.Fatal(err) + } + ring[i] = types.PublicKey(p) + } + ring[1] = types.PublicKey(pub) + + var prefixHash types.Hash + prefixHash[0] = 0xFF + + signer := &NLSAGSigner{} + sigs, err := signer.SignInput(prefixHash, KeyPair{ + Public: types.PublicKey(pub), + Secret: types.SecretKey(sec), + }, ring, 1) + if err != nil { + t.Fatal(err) + } + if len(sigs) != 3 { + t.Fatalf("got %d sigs, want 3", len(sigs)) + } + + // Verify with crypto.CheckRingSignature. + pubs := make([][32]byte, len(ring)) + for i, k := range ring { + pubs[i] = [32]byte(k) + } + rawSigs := make([][64]byte, len(sigs)) + for i, s := range sigs { + rawSigs[i] = [64]byte(s) + } + if !crypto.CheckRingSignature([32]byte(prefixHash), ki, pubs, rawSigs) { + t.Fatal("ring signature verification failed") + } +} + +func TestNLSAGSignerVersion(t *testing.T) { + signer := &NLSAGSigner{} + if signer.Version() != 1 { + t.Fatalf("version = %d, want 1", signer.Version()) + } +} + +func TestNLSAGSignerLargeRing(t *testing.T) { + pub, sec, err := crypto.GenerateKeys() + if err != nil { + t.Fatal(err) + } + ki, err := crypto.GenerateKeyImage(pub, sec) + if err != nil { + t.Fatal(err) + } + + ring := make([]types.PublicKey, 10) + for i := range ring { + p, _, err := crypto.GenerateKeys() + if err != nil { + t.Fatal(err) + } + ring[i] = types.PublicKey(p) + } + ring[5] = types.PublicKey(pub) // real at index 5 + + var prefixHash types.Hash + for i := range prefixHash { + prefixHash[i] = byte(i) + } + + signer := &NLSAGSigner{} + sigs, err := signer.SignInput(prefixHash, KeyPair{ + Public: types.PublicKey(pub), + Secret: types.SecretKey(sec), + }, ring, 5) + if err != nil { + t.Fatal(err) + } + if len(sigs) != 10 { + t.Fatalf("got %d sigs, want 10", len(sigs)) + } + + pubs := make([][32]byte, len(ring)) + for i, k := range ring { + pubs[i] = [32]byte(k) + } + rawSigs := make([][64]byte, len(sigs)) + for i, s := range sigs { + rawSigs[i] = [64]byte(s) + } + if !crypto.CheckRingSignature([32]byte(prefixHash), ki, pubs, rawSigs) { + t.Fatal("large ring signature verification failed") + } +} + +func TestNLSAGSignerInterface(t *testing.T) { + // Compile-time check that NLSAGSigner satisfies the Signer interface. + var _ Signer = (*NLSAGSigner)(nil) +}