From e8880bd5301d9adac7aabf9611bf3544fd8fd2d4 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 20:07:29 +0000 Subject: [PATCH] feat(dns): add configured mainchain fallback for chain alias discovery Co-Authored-By: Virgil --- service.go | 54 ++++++++++++++++++++++++++++- service_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/service.go b/service.go index 50ea1e3..23b9eb8 100644 --- a/service.go +++ b/service.go @@ -41,6 +41,7 @@ type Service struct { records map[string]NameRecords reverseIndex map[string][]string treeRoot string + mainchainAliasClient *MainchainAliasClient discoverer func() (map[string]NameRecords, error) fallbackDiscoverer func() (map[string]NameRecords, error) chainAliasDiscoverer func(context.Context) ([]string, error) @@ -54,6 +55,7 @@ type ServiceOptions struct { Records map[string]NameRecords Discoverer func() (map[string]NameRecords, error) FallbackDiscoverer func() (map[string]NameRecords, error) + MainchainAliasClient *MainchainAliasClient ChainAliasDiscoverer func(context.Context) ([]string, error) FallbackChainAliasDiscoverer func(context.Context) ([]string, error) TreeRootCheckInterval time.Duration @@ -74,6 +76,7 @@ func NewService(options ServiceOptions) *Service { records: cached, reverseIndex: buildReverseIndex(cached), treeRoot: treeRoot, + mainchainAliasClient: options.MainchainAliasClient, discoverer: options.Discoverer, fallbackDiscoverer: options.FallbackDiscoverer, chainAliasDiscoverer: options.ChainAliasDiscoverer, @@ -87,7 +90,12 @@ func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HS return fmt.Errorf("hsd client is required") } - aliases, err := discoverAliases(ctx, service.chainAliasDiscoverer, service.fallbackChainAliasDiscoverer) + aliases, err := discoverAliasesWithFallback( + ctx, + service.chainAliasDiscoverer, + service.fallbackChainAliasDiscoverer, + service.mainchainAliasClient, + ) if err != nil { return err } @@ -97,6 +105,50 @@ func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HS return service.discoverFromChainAliasesUsingTreeRoot(ctx, aliases, client) } +func discoverAliasesWithFallback( + ctx context.Context, + discoverer func(context.Context) ([]string, error), + fallback func(context.Context) ([]string, error), + mainchainClient *MainchainAliasClient, +) ([]string, error) { + if discoverer == nil { + if fallback != nil { + aliases, err := fallback(ctx) + if err == nil { + return aliases, nil + } + if mainchainClient == nil { + return nil, err + } + return mainchainClient.GetAllAliasDetails(ctx) + } + if mainchainClient == nil { + return nil, nil + } + return mainchainClient.GetAllAliasDetails(ctx) + } + + aliases, err := discoverer(ctx) + if err == nil { + return aliases, nil + } + if fallback == nil { + if mainchainClient == nil { + return nil, err + } + return mainchainClient.GetAllAliasDetails(ctx) + } + + fallbackAliases, fallbackErr := fallback(ctx) + if fallbackErr == nil { + return fallbackAliases, nil + } + if mainchainClient == nil { + return nil, fallbackErr + } + return mainchainClient.GetAllAliasDetails(ctx) +} + // DiscoverFromMainchainAliases updates records from main-chain aliases resolved through HSD. // // service.DiscoverFromMainchainAliases(context.Background(), dns.NewMainchainAliasClient(dns.MainchainClientOptions{ diff --git a/service_test.go b/service_test.go index 0bc09bf..798e9b8 100644 --- a/service_test.go +++ b/service_test.go @@ -440,6 +440,98 @@ func TestServiceDiscoverFromChainAliasesUsesFallbackWhenPrimaryFails(t *testing. } } +func TestServiceDiscoverFromChainAliasesFallsBackToMainchainClientWhenDiscovererFails(t *testing.T) { + var chainAliasCalls int32 + 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 "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(&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 query: %#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") + }, + MainchainAliasClient: NewMainchainAliasClient(MainchainClientOptions{ + URL: server.URL, + }), + }) + hsdClient := NewHSDClient(HSDClientOptions{ + URL: server.URL, + }) + if err := service.DiscoverFromChainAliases(context.Background(), hsdClient); err != nil { + t.Fatalf("expected chain alias discovery to complete: %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(&chainAliasCalls) != 1 { + t.Fatalf("expected one chain alias call, got %d", atomic.LoadInt32(&chainAliasCalls)) + } + if atomic.LoadInt32(&treeRootCalls) != 1 || atomic.LoadInt32(&nameResourceCalls) != 2 { + t.Fatalf("expected one tree-root and two name-resource calls, got treeRoot=%d nameResource=%d", atomic.LoadInt32(&treeRootCalls), atomic.LoadInt32(&nameResourceCalls)) + } +} + func TestServiceDiscoverFromChainAliasesSkipsRefreshWhenTreeRootUnchanged(t *testing.T) { var treeRootCalls int32 var nameResourceCalls int32 -- 2.45.3