go-blockchain/wallet/ring_test.go
Claude 7e31e706c5
feat(wallet): RPCRingSelector for decoy output selection
RingSelector interface with RPCRingSelector that fetches random outputs
from the daemon, excludes the real output and duplicates, and returns
the requested ring size.

Co-Authored-By: Charon <charon@lethean.io>
2026-02-20 23:20:15 +00:00

126 lines
3 KiB
Go

// 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 (
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"forge.lthn.ai/core/go-blockchain/rpc"
"forge.lthn.ai/core/go-blockchain/types"
)
func TestRPCRingSelector(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
type entry struct {
GlobalIndex uint64 `json:"global_index"`
PublicKey string `json:"public_key"`
}
resp := struct {
Outs []entry `json:"outs"`
Status string `json:"status"`
}{Status: "OK"}
for i := 0; i < 15; i++ {
var key types.PublicKey
key[0] = byte(i + 1)
resp.Outs = append(resp.Outs, entry{
GlobalIndex: uint64((i + 1) * 100),
PublicKey: hex.EncodeToString(key[:]),
})
}
json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
client := rpc.NewClient(srv.URL)
selector := NewRPCRingSelector(client)
members, err := selector.SelectRing(1000, 500, 10)
if err != nil {
t.Fatal(err)
}
if len(members) != 10 {
t.Fatalf("got %d ring members, want 10", len(members))
}
seen := make(map[uint64]bool)
for _, m := range members {
if seen[m.GlobalIndex] {
t.Fatalf("duplicate global index %d", m.GlobalIndex)
}
seen[m.GlobalIndex] = true
}
for _, m := range members {
if m.GlobalIndex == 500 {
t.Fatal("real global index should be excluded from decoys")
}
}
}
func TestRPCRingSelectorExcludesReal(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
type entry struct {
GlobalIndex uint64 `json:"global_index"`
PublicKey string `json:"public_key"`
}
resp := struct {
Outs []entry `json:"outs"`
Status string `json:"status"`
}{Status: "OK"}
for i := 0; i < 15; i++ {
var key types.PublicKey
key[0] = byte(i + 1)
gidx := uint64((i + 1) * 100)
if i == 3 {
gidx = 42 // this is the real output
}
resp.Outs = append(resp.Outs, entry{
GlobalIndex: gidx,
PublicKey: hex.EncodeToString(key[:]),
})
}
json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
client := rpc.NewClient(srv.URL)
selector := NewRPCRingSelector(client)
members, err := selector.SelectRing(1000, 42, 10)
if err != nil {
t.Fatal(err)
}
for _, m := range members {
if m.GlobalIndex == 42 {
t.Fatal("real output should be excluded")
}
}
}
func TestRPCRingSelectorInsufficientDecoys(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := struct {
Outs []struct{} `json:"outs"`
Status string `json:"status"`
}{Status: "OK"}
json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
client := rpc.NewClient(srv.URL)
selector := NewRPCRingSelector(client)
_, err := selector.SelectRing(1000, 0, 10)
if err == nil {
t.Fatal("expected insufficient decoys error")
}
}