go-blockchain/wallet/builder_test.go
Claude 0408d2f3fa
Some checks failed
Security Scan / security (push) Successful in 9s
Test / Test (push) Failing after 15s
refactor(types): change TxOutputBare.Target to TxOutTarget interface
Prepares for HF1 output target types (TxOutMultisig, TxOutHTLC).
All call sites updated to type-assert TxOutToKey where needed.

Modified files:
- types/transaction.go: TxOutputBare.Target is now TxOutTarget
- wire/transaction.go: encode/decode use type switch on target
- chain/ring.go: type-assert target to TxOutToKey for key extraction
- wallet/scanner.go: type-assert target before key comparison
- tui/explorer_model.go: type-assert target for display
- wire/transaction_test.go: type-assert in assertions
- wallet/builder_test.go: type-assert in assertions

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:23:27 +00:00

232 lines
6.1 KiB
Go

// 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 (
"testing"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/crypto"
"forge.lthn.ai/core/go-blockchain/types"
)
// mockRingSelector returns fixed ring members for testing.
type mockRingSelector struct{}
func (m *mockRingSelector) SelectRing(amount uint64, realGlobalIndex uint64,
ringSize int) ([]RingMember, error) {
members := make([]RingMember, ringSize)
for i := range members {
pub, _, _ := crypto.GenerateKeys()
members[i] = RingMember{
PublicKey: types.PublicKey(pub),
GlobalIndex: uint64(i * 100),
}
}
return members, nil
}
// makeTestSource generates a Transfer with fresh ephemeral keys and a key image.
func makeTestSource(amount uint64, globalIndex uint64) Transfer {
pub, sec, _ := crypto.GenerateKeys()
ki, _ := crypto.GenerateKeyImage(pub, sec)
return Transfer{
Amount: amount,
GlobalIndex: globalIndex,
EphemeralKey: KeyPair{
Public: types.PublicKey(pub),
Secret: types.SecretKey(sec),
},
KeyImage: types.KeyImage(ki),
}
}
func TestV1BuilderBasic(t *testing.T) {
signer := &NLSAGSigner{}
selector := &mockRingSelector{}
builder := NewV1Builder(signer, selector)
sender, err := GenerateAccount()
if err != nil {
t.Fatalf("generate sender: %v", err)
}
recipient, err := GenerateAccount()
if err != nil {
t.Fatalf("generate recipient: %v", err)
}
source := makeTestSource(5000, 42)
req := &BuildRequest{
Sources: []Transfer{source},
Destinations: []Destination{{Address: recipient.Address(), Amount: 3000}},
Fee: 1000,
SenderAddress: sender.Address(),
}
tx, err := builder.Build(req)
if err != nil {
t.Fatalf("Build: %v", err)
}
// Version must be 1.
if tx.Version != types.VersionPreHF4 {
t.Errorf("version: got %d, want %d", tx.Version, types.VersionPreHF4)
}
// One input.
if len(tx.Vin) != 1 {
t.Fatalf("inputs: got %d, want 1", len(tx.Vin))
}
inp, ok := tx.Vin[0].(types.TxInputToKey)
if !ok {
t.Fatalf("input type: got %T, want TxInputToKey", tx.Vin[0])
}
if inp.Amount != 5000 {
t.Errorf("input amount: got %d, want 5000", inp.Amount)
}
// Ring size = decoys + real output.
expectedRingSize := int(config.DefaultDecoySetSize) + 1
if len(inp.KeyOffsets) != expectedRingSize {
t.Errorf("ring size: got %d, want %d", len(inp.KeyOffsets), expectedRingSize)
}
// Key offsets must be sorted by global index.
for j := 1; j < len(inp.KeyOffsets); j++ {
if inp.KeyOffsets[j].GlobalIndex < inp.KeyOffsets[j-1].GlobalIndex {
t.Errorf("key offsets not sorted at index %d: %d < %d",
j, inp.KeyOffsets[j].GlobalIndex, inp.KeyOffsets[j-1].GlobalIndex)
}
}
// Two outputs: destination (3000) + change (1000).
if len(tx.Vout) != 2 {
t.Fatalf("outputs: got %d, want 2", len(tx.Vout))
}
out0, ok := tx.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("output 0 type: got %T, want TxOutputBare", tx.Vout[0])
}
if out0.Amount != 3000 {
t.Errorf("output 0 amount: got %d, want 3000", out0.Amount)
}
out1, ok := tx.Vout[1].(types.TxOutputBare)
if !ok {
t.Fatalf("output 1 type: got %T, want TxOutputBare", tx.Vout[1])
}
if out1.Amount != 1000 {
t.Errorf("output 1 amount: got %d, want 1000", out1.Amount)
}
// Output keys must be non-zero and unique.
toKey0, ok := out0.Target.(types.TxOutToKey)
if !ok {
t.Fatalf("output 0 target type: got %T, want TxOutToKey", out0.Target)
}
toKey1, ok := out1.Target.(types.TxOutToKey)
if !ok {
t.Fatalf("output 1 target type: got %T, want TxOutToKey", out1.Target)
}
if toKey0.Key == (types.PublicKey{}) {
t.Error("output 0 key is zero")
}
if toKey1.Key == (types.PublicKey{}) {
t.Error("output 1 key is zero")
}
if toKey0.Key == toKey1.Key {
t.Error("output keys are identical; derivation broken")
}
// One signature set per input.
if len(tx.Signatures) != 1 {
t.Fatalf("signature sets: got %d, want 1", len(tx.Signatures))
}
if len(tx.Signatures[0]) != expectedRingSize {
t.Errorf("signatures[0] length: got %d, want %d",
len(tx.Signatures[0]), expectedRingSize)
}
// Extra must be non-empty.
if len(tx.Extra) == 0 {
t.Error("extra is empty")
}
// Attachment must be non-empty (varint(0) = 1 byte).
if len(tx.Attachment) == 0 {
t.Error("attachment is empty")
}
// Serialisation must succeed.
raw, err := SerializeTransaction(tx)
if err != nil {
t.Fatalf("SerializeTransaction: %v", err)
}
if len(raw) == 0 {
t.Error("serialised transaction is empty")
}
}
func TestV1BuilderInsufficientFunds(t *testing.T) {
signer := &NLSAGSigner{}
selector := &mockRingSelector{}
builder := NewV1Builder(signer, selector)
sender, _ := GenerateAccount()
recipient, _ := GenerateAccount()
source := makeTestSource(1000, 10)
req := &BuildRequest{
Sources: []Transfer{source},
Destinations: []Destination{{Address: recipient.Address(), Amount: 2000}},
Fee: 500,
SenderAddress: sender.Address(),
}
_, err := builder.Build(req)
if err == nil {
t.Fatal("expected error for insufficient funds")
}
}
func TestV1BuilderExactAmount(t *testing.T) {
signer := &NLSAGSigner{}
selector := &mockRingSelector{}
builder := NewV1Builder(signer, selector)
sender, _ := GenerateAccount()
recipient, _ := GenerateAccount()
// Source exactly covers destination + fee: no change output.
source := makeTestSource(3000, 55)
req := &BuildRequest{
Sources: []Transfer{source},
Destinations: []Destination{{Address: recipient.Address(), Amount: 2000}},
Fee: 1000,
SenderAddress: sender.Address(),
}
tx, err := builder.Build(req)
if err != nil {
t.Fatalf("Build: %v", err)
}
// Exactly one output (no change).
if len(tx.Vout) != 1 {
t.Fatalf("outputs: got %d, want 1", len(tx.Vout))
}
out, ok := tx.Vout[0].(types.TxOutputBare)
if !ok {
t.Fatalf("output type: got %T, want TxOutputBare", tx.Vout[0])
}
if out.Amount != 2000 {
t.Errorf("output amount: got %d, want 2000", out.Amount)
}
}