From 00f0b0b8d0aeeeab88e08835a28ba4c198a73657 Mon Sep 17 00:00:00 2001 From: Gabriel Paradiso Date: Wed, 13 May 2026 17:03:26 +0200 Subject: [PATCH 1/2] feat: if multiple vault dons are listed, select the correct one using the gatewayConfig.ShardedDONs.DonName --- .../gateway/handlers/vault/aggregator.go | 60 +++++++-- .../gateway/handlers/vault/aggregator_test.go | 116 ++++++++++++++++++ .../gateway/handlers/vault/handler.go | 5 +- .../gateway/handlers/vault/handler_test.go | 6 + 4 files changed, 179 insertions(+), 8 deletions(-) diff --git a/core/services/gateway/handlers/vault/aggregator.go b/core/services/gateway/handlers/vault/aggregator.go index c8747afed36..31b9f018e86 100644 --- a/core/services/gateway/handlers/vault/aggregator.go +++ b/core/services/gateway/handlers/vault/aggregator.go @@ -8,6 +8,7 @@ import ( "maps" "slices" "strconv" + "strings" "github.com/ethereum/go-ethereum/common" @@ -20,6 +21,11 @@ import ( type baseAggregator struct { capabilitiesRegistry capabilitiesRegistry + // vaultHandlerDonId is DONConfig.DonId for this vault handler (from ShardedDONConfig.DonName): + // the vault capability DON this gateway instance is wired to. + // When the registry lists multiple vault DONs, it selects the row where capabilities.DON.Name + // equals this string (v2), or if Name is empty (v1 sync) where decimal capabilities.DON.ID matches. + vaultHandlerDonId string } func (a *baseAggregator) Aggregate(ctx context.Context, l logger.Logger, resps map[string]jsonrpc.Response[json.RawMessage], currResp *jsonrpc.Response[json.RawMessage]) (*jsonrpc.Response[json.RawMessage], error) { @@ -47,15 +53,55 @@ func (a *baseAggregator) donForVaultCapability(ctx context.Context) (*capabiliti if err != nil { return nil, err } - // TODO: Support multiple vault capabilities in the capability registry. - // For the initial Smartcon deployment there will be exactly one Vault capability - // split across both DON families. - if len(dons) != 1 { - return nil, fmt.Errorf("expected exactly one DON for vault capability, found %d", len(dons)) + if len(dons) == 0 { + return nil, fmt.Errorf("no DON found for vault capability %s", vaultcommon.CapabilityID) + } + if len(dons) == 1 { + don := dons[0] + return &don, nil + } + + handlerDonId := strings.TrimSpace(a.vaultHandlerDonId) + if handlerDonId == "" { + return nil, fmt.Errorf("multiple DONs (%d) host vault capability %s but vault handler DonId is empty; set ShardedDONConfig.DonName so DONConfig.DonId matches the vault DON name or id in the registry (%s)", + len(dons), vaultcommon.CapabilityID, summarizeVaultRegistryDONs(dons)) } - don := dons[0] - return &don, nil + var matches []capabilities.DONWithNodes + for i := range dons { + d := dons[i] + if vaultDONMatchesHandlerDonId(&d.DON, handlerDonId) { + matches = append(matches, d) + } + } + switch len(matches) { + case 0: + return nil, fmt.Errorf("multiple DONs (%d) host vault capability %s but none match vault handler DonId %q; registry has %s", + len(dons), vaultcommon.CapabilityID, a.vaultHandlerDonId, summarizeVaultRegistryDONs(dons)) + case 1: + d := matches[0] + return &d, nil + default: + return nil, fmt.Errorf("%d DONs match vault handler DonId %q for vault capability %s", len(matches), a.vaultHandlerDonId, vaultcommon.CapabilityID) + } +} + +func vaultDONMatchesHandlerDonId(don *capabilities.DON, handlerDonId string) bool { + if don.Name != "" { + return don.Name == handlerDonId + } + return strconv.FormatUint(uint64(don.ID), 10) == handlerDonId +} + +func summarizeVaultRegistryDONs(dons []capabilities.DONWithNodes) string { + var b strings.Builder + for i, d := range dons { + if i > 0 { + b.WriteString("; ") + } + _, _ = fmt.Fprintf(&b, "name=%q id=%d", d.DON.Name, d.DON.ID) + } + return b.String() } func (a *baseAggregator) validateUsingQuorum(don capabilities.DON, resps map[string]jsonrpc.Response[json.RawMessage], l logger.Logger) (*jsonrpc.Response[json.RawMessage], error) { diff --git a/core/services/gateway/handlers/vault/aggregator_test.go b/core/services/gateway/handlers/vault/aggregator_test.go index b6c6f3a854e..a911e08154a 100644 --- a/core/services/gateway/handlers/vault/aggregator_test.go +++ b/core/services/gateway/handlers/vault/aggregator_test.go @@ -287,3 +287,119 @@ func TestAggregator_QuorumUnobtainable(t *testing.T) { _, err := agg.Aggregate(t.Context(), logger.Test(t), responses, resp3) require.ErrorContains(t, err, "failed to validate using quorum: quorum unobtainable") } + +func makeDONWithNodesForTest(t *testing.T, name string, id uint32, f uint8, memberOffset byte, nodeCount int) capabilities.DONWithNodes { + t.Helper() + nodes := make([]capabilities.Node, nodeCount) + members := make([]p2ptypes.PeerID, nodeCount) + for i := 0; i < nodeCount; i++ { + pid := p2ptypes.PeerID{} + pid[0] = memberOffset + byte(i) + pid[1] = byte(i) + nodes[i] = capabilities.Node{PeerID: &pid, Signer: [32]byte{}} + members[i] = pid + } + return capabilities.DONWithNodes{ + DON: capabilities.DON{ + Name: name, + ID: id, + F: f, + Members: members, + }, + Nodes: nodes, + } +} + +func TestAggregator_MultipleRegistryDONs_SelectsByVaultHandlerDonName(t *testing.T) { + donOther := makeDONWithNodesForTest(t, "staging-vault", 1, 2, 0x10, 7) + donMine := makeDONWithNodesForTest(t, "cre-reliability-vault", 2, 1, 0x20, 4) + mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donOther, donMine}} + agg := &baseAggregator{ + capabilitiesRegistry: mcr, + vaultHandlerDonId: "cre-reliability-vault", + } + + rm := json.RawMessage([]byte(`{}`)) + currResp := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "1", + Method: vaulttypes.MethodSecretsCreate, + Result: &rm, + } + responses := map[string]jsonrpc.Response[json.RawMessage]{ + "a": currResp, + "b": currResp, + "c": currResp, + } + resp, err := agg.Aggregate(t.Context(), logger.Test(t), responses, &currResp) + require.NoError(t, err) + require.Equal(t, currResp.ID, resp.ID) +} + +func TestAggregator_MultipleRegistryDONs_SelectsByIDWhenNameEmpty(t *testing.T) { + donOther := makeDONWithNodesForTest(t, "", 1, 2, 0x10, 7) + donMine := makeDONWithNodesForTest(t, "", 99, 1, 0x20, 4) + mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donOther, donMine}} + agg := &baseAggregator{ + capabilitiesRegistry: mcr, + vaultHandlerDonId: "99", + } + + rm := json.RawMessage([]byte(`{}`)) + currResp := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "1", + Method: vaulttypes.MethodSecretsCreate, + Result: &rm, + } + responses := map[string]jsonrpc.Response[json.RawMessage]{ + "a": currResp, + "b": currResp, + "c": currResp, + } + resp, err := agg.Aggregate(t.Context(), logger.Test(t), responses, &currResp) + require.NoError(t, err) + require.Equal(t, currResp.ID, resp.ID) +} + +func TestAggregator_MultipleRegistryDONs_NoMatchingVaultHandlerDonId(t *testing.T) { + donA := makeDONWithNodesForTest(t, "don-a", 1, 1, 0x10, 4) + donB := makeDONWithNodesForTest(t, "don-b", 2, 1, 0x20, 4) + mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donA, donB}} + agg := &baseAggregator{ + capabilitiesRegistry: mcr, + vaultHandlerDonId: "unknown-vault", + } + + rm := json.RawMessage([]byte(`{}`)) + currResp := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "1", + Method: vaulttypes.MethodSecretsCreate, + Result: &rm, + } + responses := map[string]jsonrpc.Response[json.RawMessage]{"a": currResp} + _, err := agg.Aggregate(t.Context(), logger.Test(t), responses, &currResp) + require.ErrorContains(t, err, "none match vault handler DonId") +} + +func TestAggregator_MultipleRegistryDONs_AmbiguousMatchingVaultHandlerDonId(t *testing.T) { + donA := makeDONWithNodesForTest(t, "same-name", 1, 1, 0x10, 4) + donB := makeDONWithNodesForTest(t, "same-name", 2, 1, 0x20, 4) + mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donA, donB}} + agg := &baseAggregator{ + capabilitiesRegistry: mcr, + vaultHandlerDonId: "same-name", + } + + rm := json.RawMessage([]byte(`{}`)) + currResp := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "1", + Method: vaulttypes.MethodSecretsCreate, + Result: &rm, + } + responses := map[string]jsonrpc.Response[json.RawMessage]{"a": currResp} + _, err := agg.Aggregate(t.Context(), logger.Test(t), responses, &currResp) + require.ErrorContains(t, err, "2 DONs match vault handler DonId") +} diff --git a/core/services/gateway/handlers/vault/handler.go b/core/services/gateway/handlers/vault/handler.go index 42a055b8554..ff95d5e9443 100644 --- a/core/services/gateway/handlers/vault/handler.go +++ b/core/services/gateway/handlers/vault/handler.go @@ -271,7 +271,10 @@ func newHandlerWithAuthorizer(methodConfig json.RawMessage, donConfig *config.DO jwtAuth: jwtAuth, stopCh: make(services.StopChan), metrics: metrics, - aggregator: &baseAggregator{capabilitiesRegistry: capabilitiesRegistry}, + aggregator: &baseAggregator{ + capabilitiesRegistry: capabilitiesRegistry, + vaultHandlerDonId: donConfig.DonId, + }, clock: clock, RequestValidator: vaultcap.NewRequestValidator(limiter, ciphertextLimiter, idKeyLengthLimiter, idOwnerLengthLimiter, idNamespaceLengthLimiter), }, nil diff --git a/core/services/gateway/handlers/vault/handler_test.go b/core/services/gateway/handlers/vault/handler_test.go index 3c99e9c1e22..b436e467589 100644 --- a/core/services/gateway/handlers/vault/handler_test.go +++ b/core/services/gateway/handlers/vault/handler_test.go @@ -127,11 +127,16 @@ func (m *mockAggregator) Aggregate(_ context.Context, _ logger.Logger, _ map[str type mockCapabilitiesRegistry struct { F uint8 Nodes []capabilities.Node + // DONs, if set, is returned as-is from DONsForCapability (for multi-DON tests). + DONs []capabilities.DONWithNodes } var owner = "test_owner" func (m *mockCapabilitiesRegistry) DONsForCapability(_ context.Context, _ string) ([]capabilities.DONWithNodes, error) { + if len(m.DONs) > 0 { + return m.DONs, nil + } members := make([]p2ptypes.PeerID, 0, len(m.Nodes)) for _, n := range m.Nodes { members = append(members, *n.PeerID) @@ -1009,6 +1014,7 @@ func TestVaultHandler_PublicKeyGet(t *testing.T) { mcr := &mockCapabilitiesRegistry{F: 1, Nodes: nodes} h.(*handler).aggregator = &baseAggregator{ capabilitiesRegistry: mcr, + vaultHandlerDonId: h.(*handler).donConfig.DonId, } don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) From fbb46eca07073fded96473295f6a03d11290a730 Mon Sep 17 00:00:00 2001 From: Gabriel Paradiso Date: Wed, 13 May 2026 17:35:41 +0200 Subject: [PATCH 2/2] fix: lint issues --- .../gateway/handlers/vault/aggregator.go | 31 +++++++++++-------- .../gateway/handlers/vault/aggregator_test.go | 8 ++--- .../gateway/handlers/vault/handler.go | 2 +- .../gateway/handlers/vault/handler_test.go | 2 +- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/core/services/gateway/handlers/vault/aggregator.go b/core/services/gateway/handlers/vault/aggregator.go index 31b9f018e86..f07ce2c0299 100644 --- a/core/services/gateway/handlers/vault/aggregator.go +++ b/core/services/gateway/handlers/vault/aggregator.go @@ -21,11 +21,14 @@ import ( type baseAggregator struct { capabilitiesRegistry capabilitiesRegistry - // vaultHandlerDonId is DONConfig.DonId for this vault handler (from ShardedDONConfig.DonName): - // the vault capability DON this gateway instance is wired to. - // When the registry lists multiple vault DONs, it selects the row where capabilities.DON.Name - // equals this string (v2), or if Name is empty (v1 sync) where decimal capabilities.DON.ID matches. - vaultHandlerDonId string + // vaultHandlerDonID scopes registry lookup when several vault DONs exist. + // + // Source: gateway job TOML [[gatewayConfig.ShardedDONs]] DonName (see deployment/cre/jobs/pkg/gateway_job.go), + // loaded as ShardedDONConfig.DonName and passed as DONConfig.DonId (handler_factory.shardedDONsToLegacy; + // DonId is a legacy field name for that string, not the on-chain uint32 id). + // + // Matching: capabilities.DON.Name when non-empty (v2), else decimal capabilities.DON.ID string (v1 sync). + vaultHandlerDonID string } func (a *baseAggregator) Aggregate(ctx context.Context, l logger.Logger, resps map[string]jsonrpc.Response[json.RawMessage], currResp *jsonrpc.Response[json.RawMessage]) (*jsonrpc.Response[json.RawMessage], error) { @@ -61,8 +64,8 @@ func (a *baseAggregator) donForVaultCapability(ctx context.Context) (*capabiliti return &don, nil } - handlerDonId := strings.TrimSpace(a.vaultHandlerDonId) - if handlerDonId == "" { + handlerDonID := strings.TrimSpace(a.vaultHandlerDonID) + if handlerDonID == "" { return nil, fmt.Errorf("multiple DONs (%d) host vault capability %s but vault handler DonId is empty; set ShardedDONConfig.DonName so DONConfig.DonId matches the vault DON name or id in the registry (%s)", len(dons), vaultcommon.CapabilityID, summarizeVaultRegistryDONs(dons)) } @@ -70,27 +73,29 @@ func (a *baseAggregator) donForVaultCapability(ctx context.Context) (*capabiliti var matches []capabilities.DONWithNodes for i := range dons { d := dons[i] - if vaultDONMatchesHandlerDonId(&d.DON, handlerDonId) { + if vaultDONMatchesHandlerDonID(&d.DON, handlerDonID) { matches = append(matches, d) } } switch len(matches) { case 0: return nil, fmt.Errorf("multiple DONs (%d) host vault capability %s but none match vault handler DonId %q; registry has %s", - len(dons), vaultcommon.CapabilityID, a.vaultHandlerDonId, summarizeVaultRegistryDONs(dons)) + len(dons), vaultcommon.CapabilityID, a.vaultHandlerDonID, summarizeVaultRegistryDONs(dons)) case 1: d := matches[0] return &d, nil default: - return nil, fmt.Errorf("%d DONs match vault handler DonId %q for vault capability %s", len(matches), a.vaultHandlerDonId, vaultcommon.CapabilityID) + return nil, fmt.Errorf("%d DONs match vault handler DonId %q for vault capability %s", len(matches), a.vaultHandlerDonID, vaultcommon.CapabilityID) } } -func vaultDONMatchesHandlerDonId(don *capabilities.DON, handlerDonId string) bool { +// vaultDONMatchesHandlerDonID reports whether don is the vault DON this handler is configured for. +// handlerDonID is vaultHandlerDonID (jobspec DonName / DONConfig.DonId); see struct comment. +func vaultDONMatchesHandlerDonID(don *capabilities.DON, handlerDonID string) bool { if don.Name != "" { - return don.Name == handlerDonId + return don.Name == handlerDonID } - return strconv.FormatUint(uint64(don.ID), 10) == handlerDonId + return strconv.FormatUint(uint64(don.ID), 10) == handlerDonID } func summarizeVaultRegistryDONs(dons []capabilities.DONWithNodes) string { diff --git a/core/services/gateway/handlers/vault/aggregator_test.go b/core/services/gateway/handlers/vault/aggregator_test.go index a911e08154a..7532f624d09 100644 --- a/core/services/gateway/handlers/vault/aggregator_test.go +++ b/core/services/gateway/handlers/vault/aggregator_test.go @@ -316,7 +316,7 @@ func TestAggregator_MultipleRegistryDONs_SelectsByVaultHandlerDonName(t *testing mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donOther, donMine}} agg := &baseAggregator{ capabilitiesRegistry: mcr, - vaultHandlerDonId: "cre-reliability-vault", + vaultHandlerDonID: "cre-reliability-vault", } rm := json.RawMessage([]byte(`{}`)) @@ -342,7 +342,7 @@ func TestAggregator_MultipleRegistryDONs_SelectsByIDWhenNameEmpty(t *testing.T) mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donOther, donMine}} agg := &baseAggregator{ capabilitiesRegistry: mcr, - vaultHandlerDonId: "99", + vaultHandlerDonID: "99", } rm := json.RawMessage([]byte(`{}`)) @@ -368,7 +368,7 @@ func TestAggregator_MultipleRegistryDONs_NoMatchingVaultHandlerDonId(t *testing. mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donA, donB}} agg := &baseAggregator{ capabilitiesRegistry: mcr, - vaultHandlerDonId: "unknown-vault", + vaultHandlerDonID: "unknown-vault", } rm := json.RawMessage([]byte(`{}`)) @@ -389,7 +389,7 @@ func TestAggregator_MultipleRegistryDONs_AmbiguousMatchingVaultHandlerDonId(t *t mcr := &mockCapabilitiesRegistry{DONs: []capabilities.DONWithNodes{donA, donB}} agg := &baseAggregator{ capabilitiesRegistry: mcr, - vaultHandlerDonId: "same-name", + vaultHandlerDonID: "same-name", } rm := json.RawMessage([]byte(`{}`)) diff --git a/core/services/gateway/handlers/vault/handler.go b/core/services/gateway/handlers/vault/handler.go index ff95d5e9443..9c90dcafd7d 100644 --- a/core/services/gateway/handlers/vault/handler.go +++ b/core/services/gateway/handlers/vault/handler.go @@ -273,7 +273,7 @@ func newHandlerWithAuthorizer(methodConfig json.RawMessage, donConfig *config.DO metrics: metrics, aggregator: &baseAggregator{ capabilitiesRegistry: capabilitiesRegistry, - vaultHandlerDonId: donConfig.DonId, + vaultHandlerDonID: donConfig.DonId, }, clock: clock, RequestValidator: vaultcap.NewRequestValidator(limiter, ciphertextLimiter, idKeyLengthLimiter, idOwnerLengthLimiter, idNamespaceLengthLimiter), diff --git a/core/services/gateway/handlers/vault/handler_test.go b/core/services/gateway/handlers/vault/handler_test.go index b436e467589..cbf44b7f322 100644 --- a/core/services/gateway/handlers/vault/handler_test.go +++ b/core/services/gateway/handlers/vault/handler_test.go @@ -1014,7 +1014,7 @@ func TestVaultHandler_PublicKeyGet(t *testing.T) { mcr := &mockCapabilitiesRegistry{F: 1, Nodes: nodes} h.(*handler).aggregator = &baseAggregator{ capabilitiesRegistry: mcr, - vaultHandlerDonId: h.(*handler).donConfig.DonId, + vaultHandlerDonID: h.(*handler).donConfig.DonId, } don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil)