From d2d399703c95eb2553a70eaec3cdf9dfe293d9ae Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 20:12:54 +0000 Subject: [PATCH] feat(dns): fallback to chain client after mainchain fallback fails Co-Authored-By: Virgil --- mainchain_test.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++ service.go | 3 +- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/mainchain_test.go b/mainchain_test.go index 606dbd3..bcee327 100644 --- a/mainchain_test.go +++ b/mainchain_test.go @@ -290,6 +290,107 @@ func TestServiceDiscoverFromMainchainAliasesFallsBackToFallbackChainAliasDiscove } } +func TestServiceDiscoverFromMainchainAliasesFallsBackToChainClientWhenFallbackChainAliasDiscovererFails(t *testing.T) { + var chainAliasCalls int32 + var fallbackChainAliasCalls int32 + var hsdTreeRootCalls int32 + var hsdAliasCalls 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 "get_all_alias_details": + atomic.AddInt32(&chainAliasCalls, 1) + responseWriter.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": []any{ + map[string]any{ + "hns": "gateway.charon.lthn", + }, + map[string]any{ + "hns": "node.charon.lthn", + }, + }, + }) + case "getblockchaininfo": + atomic.AddInt32(&hsdTreeRootCalls, 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(&hsdAliasCalls, 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() + + chainClient := NewMainchainAliasClient(MainchainClientOptions{ + URL: server.URL, + }) + hsdClient := NewHSDClient(HSDClientOptions{ + URL: server.URL, + }) + service := NewService(ServiceOptions{ + ChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + return nil, errors.New("blockchain service unavailable") + }, + FallbackChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + atomic.AddInt32(&fallbackChainAliasCalls, 1) + return nil, errors.New("fallback chain alias unavailable") + }, + }) + + if err := service.DiscoverFromMainchainAliases(context.Background(), chainClient, hsdClient); err != nil { + t.Fatalf("expected fallback chain alias discoverer to fail over to chain client: %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(&fallbackChainAliasCalls) != 1 { + t.Fatalf("expected fallback chain alias discoverer to run and fail, got %d", atomic.LoadInt32(&fallbackChainAliasCalls)) + } + if atomic.LoadInt32(&chainAliasCalls) != 1 { + t.Fatalf("expected chain alias client fallback to be used, got %d", atomic.LoadInt32(&chainAliasCalls)) + } + if atomic.LoadInt32(&hsdTreeRootCalls) != 1 || atomic.LoadInt32(&hsdAliasCalls) != 2 { + t.Fatalf("expected fallback to still trigger RPC calls, got treeRoot=%d nameResource=%d", atomic.LoadInt32(&hsdTreeRootCalls), atomic.LoadInt32(&hsdAliasCalls)) + } +} + func TestServiceDiscoverFromMainchainAliasesFallsBackToChainClientWhenPrimaryDiscovererFails(t *testing.T) { var chainAliasCalls int32 var hsdTreeRootCalls int32 diff --git a/service.go b/service.go index 035798d..4ec73d4 100644 --- a/service.go +++ b/service.go @@ -180,7 +180,7 @@ func (service *Service) DiscoverFromMainchainAliases(ctx context.Context, chainC return err } - aliases, err := discoverAliases( + aliases, err := discoverAliasesWithFallback( ctx, func(ctx context.Context) ([]string, error) { if service.chainAliasDiscoverer != nil { @@ -200,6 +200,7 @@ func (service *Service) DiscoverFromMainchainAliases(ctx context.Context, chainC } return nil, nil }, + chainClient, ) if err != nil { return err