From fbc46259e4530857168884ca0c37966d3cf508a8 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 20:04:31 +0000 Subject: [PATCH] feat(dns): wire chain alias discoverer path for mainchain discovery Implemented DiscoverFromMainchainAliases to honor ServiceOptions chain alias discoverers and fallbacks before direct chain RPC, matching RFC chain-action dependency behavior. Added regression tests for action-based discovery and fallback usage. Co-Authored-By: Virgil --- mainchain_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++ service.go | 23 +++++-- 2 files changed, 167 insertions(+), 5 deletions(-) diff --git a/mainchain_test.go b/mainchain_test.go index d6e39e3..d8a8e73 100644 --- a/mainchain_test.go +++ b/mainchain_test.go @@ -3,6 +3,7 @@ package dns import ( "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "sync/atomic" @@ -140,3 +141,151 @@ func TestServiceDiscoverFromMainchainAliasesUsesMainchainThenHSD(t *testing.T) { } } +func TestServiceDiscoverFromMainchainAliasesUsesActionDiscoverer(t *testing.T) { + var treeRootCalls int32 + var nameResourceCalls int32 + + server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + var payload struct { + Method string `json:"method"` + Params []any `json:"params"` + } + if err := json.NewDecoder(request.Body).Decode(&payload); err != nil { + t.Fatalf("unexpected request payload: %v", err) + } + + switch payload.Method { + case "getblockchaininfo": + atomic.AddInt32(&treeRootCalls, 1) + responseWriter.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "tree_root": "chain-root-1", + }, + }) + case "getnameresource": + atomic.AddInt32(&nameResourceCalls, 1) + switch payload.Params[0] { + case "gateway.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "a": []string{"10.10.10.10"}, + }, + }) + case "node.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "aaaa": []string{"2600:1f1c:7f0:4f01::2"}, + }, + }) + default: + t.Fatalf("unexpected alias lookup: %#v", payload.Params) + } + default: + t.Fatalf("unexpected method: %s", payload.Method) + } + })) + defer server.Close() + + service := NewService(ServiceOptions{ + ChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + return []string{"gateway.charon.lthn", "node.charon.lthn"}, nil + }, + }) + hsdClient := NewHSDClient(HSDClientOptions{ + URL: server.URL, + }) + + if err := service.DiscoverFromMainchainAliases(context.Background(), nil, hsdClient); err != nil { + t.Fatalf("expected discover from chain alias discoverer: %v", err) + } + + gateway, ok := service.Resolve("gateway.charon.lthn") + if !ok || len(gateway.A) != 1 || gateway.A[0] != "10.10.10.10" { + t.Fatalf("expected gateway A record, got %#v (ok=%t)", gateway, ok) + } + node, ok := service.Resolve("node.charon.lthn") + if !ok || len(node.AAAA) != 1 || node.AAAA[0] != "2600:1f1c:7f0:4f01::2" { + t.Fatalf("expected node AAAA record, got %#v (ok=%t)", node, ok) + } + + if atomic.LoadInt32(&treeRootCalls) != 1 || atomic.LoadInt32(&nameResourceCalls) != 2 { + t.Fatalf("expected tree-root and name-resource calls, got treeRoot=%d nameResource=%d", atomic.LoadInt32(&treeRootCalls), atomic.LoadInt32(&nameResourceCalls)) + } +} + +func TestServiceDiscoverFromMainchainAliasesFallsBackToFallbackChainAliasDiscoverer(t *testing.T) { + var treeRootCalls int32 + var nameResourceCalls int32 + + server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + var payload struct { + Method string `json:"method"` + Params []any `json:"params"` + } + if err := json.NewDecoder(request.Body).Decode(&payload); err != nil { + t.Fatalf("unexpected request payload: %v", err) + } + + switch payload.Method { + case "getblockchaininfo": + atomic.AddInt32(&treeRootCalls, 1) + responseWriter.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "tree_root": "chain-root-1", + }, + }) + case "getnameresource": + atomic.AddInt32(&nameResourceCalls, 1) + switch payload.Params[0] { + case "gateway.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "a": []string{"10.10.10.10"}, + }, + }) + case "node.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "aaaa": []string{"2600:1f1c:7f0:4f01::2"}, + }, + }) + default: + t.Fatalf("unexpected alias lookup: %#v", payload.Params) + } + default: + t.Fatalf("unexpected method: %s", payload.Method) + } + })) + defer server.Close() + + service := NewService(ServiceOptions{ + ChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + return nil, errors.New("blockchain service unavailable") + }, + FallbackChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + return []string{"gateway.charon.lthn", "node.charon.lthn"}, nil + }, + }) + hsdClient := NewHSDClient(HSDClientOptions{ + URL: server.URL, + }) + + if err := service.DiscoverFromMainchainAliases(context.Background(), nil, hsdClient); err != nil { + t.Fatalf("expected fallback discover from chain alias discoverer: %v", err) + } + + gateway, ok := service.Resolve("gateway.charon.lthn") + if !ok || len(gateway.A) != 1 || gateway.A[0] != "10.10.10.10" { + t.Fatalf("expected gateway A record, got %#v (ok=%t)", gateway, ok) + } + node, ok := service.Resolve("node.charon.lthn") + if !ok || len(node.AAAA) != 1 || node.AAAA[0] != "2600:1f1c:7f0:4f01::2" { + t.Fatalf("expected node AAAA record, got %#v (ok=%t)", node, ok) + } + + if atomic.LoadInt32(&treeRootCalls) != 1 || atomic.LoadInt32(&nameResourceCalls) != 2 { + t.Fatalf("expected fallback discovery to still trigger RPC calls, got treeRoot=%d nameResource=%d", atomic.LoadInt32(&treeRootCalls), atomic.LoadInt32(&nameResourceCalls)) + } +} diff --git a/service.go b/service.go index 17fc545..f544427 100644 --- a/service.go +++ b/service.go @@ -105,11 +105,24 @@ func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HS // URL: "http://127.0.0.1:14037", // })) func (service *Service) DiscoverFromMainchainAliases(ctx context.Context, chainClient *MainchainAliasClient, hsdClient *HSDClient) error { - if chainClient == nil { - return fmt.Errorf("mainchain alias client is required") - } - - aliases, err := chainClient.GetAllAliasDetails(ctx) + aliases, err := discoverAliases( + ctx, + func(ctx context.Context) ([]string, error) { + if service.chainAliasDiscoverer != nil { + return service.chainAliasDiscoverer(ctx) + } + if chainClient == nil { + return nil, nil + } + return chainClient.GetAllAliasDetails(ctx) + }, + func(ctx context.Context) ([]string, error) { + if service.fallbackChainAliasDiscoverer != nil { + return service.fallbackChainAliasDiscoverer(ctx) + } + return nil, nil + }, + ) if err != nil { return err } -- 2.45.3