feat(dns): add configured mainchain fallback for chain alias discovery

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 20:07:29 +00:00
parent a59590d633
commit e8880bd530
2 changed files with 145 additions and 1 deletions

View file

@ -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{

View file

@ -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