Skip to content

Commit 5dbb53d

Browse files
authored
fix(api): harden PoC seed submission and versions endpoint
### PoC Seed Stability - Recover missed seed submissions after epoch-boundary lag and retry local seed restoration after transient failures (such as signer unavailability) to prevent validators from getting zeroed [`d1d6d03`](gonka-ai@d1d6d03), [`ea13258`](gonka-ai@ea13258), [`4601969`](gonka-ai@4601969) ### API - Fix: return `/v1/versions` with enriched DAPI version info (including ML node details) [`21ed5c6`](gonka-ai@21ed5c6) - Set default Node Manager gRPC port to 9400 to make sure it works on custom configurations [`6806e15`](gonka-ai@6806e15) ### Deployments - Update the default join stack configuration to use the latest `v0.2.15-post2` API image [`1c59f14`](gonka-ai@1c59f14)
1 parent 4fa6be0 commit 5dbb53d

19 files changed

Lines changed: 647 additions & 32 deletions

common/queryapi/tests/dapi_contract_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ import (
1515
// public handlers for read-only query routes. edge-api must expose the same keys
1616
// so existing clients keep working when the proxy steers traffic to edge-api.
1717
var dapiTopLevelKeys = map[string][]string{
18-
"GET /v1/status": {"status"},
19-
"GET /v1/models": {"object", "data"},
20-
"GET /v1/governance/models": {"models"},
21-
"GET /v1/governance/models-legacy": {"model"},
22-
"GET /v1/pricing": {"unit_of_compute_price", "models"},
23-
"GET /v1/versions": {"api_version", "node_version", "timestamp"},
24-
"GET /v1/epochs/{epoch}/participants": {"active_participants", "addresses", "active_participants_bytes", "proof_ops", "validators", "block", "excluded_participants"},
18+
"GET /v1/status": {"status"},
19+
"GET /v1/models": {"object", "data"},
20+
"GET /v1/governance/models": {"models"},
21+
"GET /v1/governance/models-legacy": {"model"},
22+
"GET /v1/pricing": {"unit_of_compute_price", "models"},
23+
"GET /v1/versions": {"api_version", "node_version", "timestamp"},
24+
"GET /v1/epochs/{epoch}/participants": {"active_participants", "addresses", "active_participants_bytes", "proof_ops", "validators", "block", "excluded_participants"},
2525
}
2626

2727
func TestResponseTopLevelKeysMatchDapiContract(t *testing.T) {
2828
cases := []struct {
29-
name string
30-
keys []string
31-
run func(t *testing.T) (int, []byte)
29+
name string
30+
keys []string
31+
run func(t *testing.T) (int, []byte)
3232
}{
3333
{
3434
name: "GET /v1/status",

common/queryapi/tests/routes_contract_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
// verify/debug paths.
1919
var canonicalPublicReadOnlyRoutes = []string{
2020
"/v1/status",
21-
"/v1/versions",
2221
"/v1/models",
2322
"/v1/governance/models",
2423
"/v1/governance/models-legacy",
@@ -37,6 +36,10 @@ var canonicalPublicReadOnlyRoutes = []string{
3736
"/v1/bridge/addresses",
3837
}
3938

39+
var edgeAPIOnlyRoutes = []string{
40+
"/v1/versions",
41+
}
42+
4043
// canonicalOptionalReadOnlyRoutes are CPU-heavy verify/debug helpers served by
4144
// edge-api but private on the proxy unless EDGE_API_EXPOSE_OPTIONAL_ROUTES=true.
4245
var canonicalOptionalReadOnlyRoutes = []string{
@@ -46,14 +49,18 @@ var canonicalOptionalReadOnlyRoutes = []string{
4649
"/v1/debug/verify/{height}",
4750
}
4851

49-
// canonicalReadOnlyRoutes is the full edge-api OpenAPI surface (public + optional).
52+
// canonicalReadOnlyRoutes is the full edge-api OpenAPI surface.
5053
var canonicalReadOnlyRoutes = append(
51-
append([]string(nil), canonicalPublicReadOnlyRoutes...),
54+
append(
55+
append([]string(nil), canonicalPublicReadOnlyRoutes...),
56+
edgeAPIOnlyRoutes...,
57+
),
5258
canonicalOptionalReadOnlyRoutes...,
5359
)
5460

5561
func TestReadOnlyRouteCount(t *testing.T) {
56-
assert.Len(t, canonicalPublicReadOnlyRoutes, 18)
62+
assert.Len(t, canonicalPublicReadOnlyRoutes, 17)
63+
assert.Len(t, edgeAPIOnlyRoutes, 1)
5764
assert.Len(t, canonicalOptionalReadOnlyRoutes, 4)
5865
assert.Len(t, canonicalReadOnlyRoutes, 22)
5966
}
@@ -72,6 +79,17 @@ func TestProxyEntrypointPublicRoutesMatchCanonical(t *testing.T) {
7279
want := append([]string(nil), canonicalPublicReadOnlyRoutes...)
7380
sort.Strings(want)
7481
assert.Equal(t, want, got)
82+
assert.NotContains(t, got, "/v1/versions")
83+
}
84+
85+
func TestProxyEntrypointSkipsVersionsOverride(t *testing.T) {
86+
_, file, _, ok := runtime.Caller(0)
87+
require.True(t, ok)
88+
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
89+
path := filepath.Join(repoRoot, "proxy", "entrypoint.sh")
90+
b, err := os.ReadFile(path)
91+
require.NoError(t, err)
92+
require.Contains(t, string(b), `if [ "$route" = "/v1/versions" ]; then`)
7593
}
7694

7795
func TestProxyEntrypointOptionalRoutesMatchCanonical(t *testing.T) {

common/queryapi/tests/status_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ func TestGetStatus_Returns200(t *testing.T) {
2222

2323
// -- GetVersions --
2424

25-
type stubNodeInfoServer struct{ cmtservice.UnimplementedServiceServer }
25+
type stubNodeInfoServer struct {
26+
cmtservice.UnimplementedServiceServer
27+
}
2628

2729
func (s *stubNodeInfoServer) GetNodeInfo(_ context.Context, _ *cmtservice.GetNodeInfoRequest) (*cmtservice.GetNodeInfoResponse, error) {
2830
return &cmtservice.GetNodeInfoResponse{
@@ -48,7 +50,9 @@ func TestGetVersions_Returns200(t *testing.T) {
4850
assert.Contains(t, body, `"timestamp"`)
4951
}
5052

51-
type errNodeInfoServer struct{ cmtservice.UnimplementedServiceServer }
53+
type errNodeInfoServer struct {
54+
cmtservice.UnimplementedServiceServer
55+
}
5256

5357
func (s *errNodeInfoServer) GetNodeInfo(_ context.Context, _ *cmtservice.GetNodeInfoRequest) (*cmtservice.GetNodeInfoResponse, error) {
5458
return nil, status.Error(codes.Unavailable, "node down")

decentralized-api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ RUN --mount=type=cache,id=go-build-cache3,target=/root/.cache/go-build \
5151

5252
COPY decentralized-api/. .
5353

54-
# ARG LDFLAGS
54+
ARG LDFLAGS
5555
RUN --mount=type=cache,id=go-build-cache3,target=/root/.cache/go-build \
5656
--mount=type=cache,id=go-mod-cache3,target=/go/pkg/mod \
5757
if [ "$BLST_PORTABLE" = "1" ]; then export CGO_CFLAGS="$CGO_CFLAGS -D__BLST_PORTABLE__"; fi; \

decentralized-api/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ define DOCKER_BUILD
4242
@echo " GOARCH: $(GOARCH)"
4343
@$(DOCKER_BUILD_PREFIX) \
4444
--platform $(PLATFORM) \
45+
--build-arg LDFLAGS='$(ldflags)' \
4546
--build-arg GOOS=$(GOOS) \
4647
--build-arg GOARCH=$(GOARCH) \
4748
--build-arg BLST_PORTABLE=$(BLST_PORTABLE) \
@@ -58,6 +59,7 @@ define DOCKER_BUILD_UPGRADE
5859
@echo " GOARCH: $(GOARCH)"
5960
@$(DOCKER_BUILD_UPGRADE_PREFIX) \
6061
--platform $(PLATFORM) \
62+
--build-arg LDFLAGS='$(ldflags)' \
6163
--build-arg GOOS=$(GOOS) \
6264
--build-arg GOARCH=$(GOARCH) \
6365
--build-arg DEVSHARD_VERSION=$(DEVSHARD_VERSION) \
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestDockerBuildPassesVersionMetadata(t *testing.T) {
12+
makefile, err := os.ReadFile("Makefile")
13+
require.NoError(t, err)
14+
require.Equal(t, 2, strings.Count(string(makefile), "\n\t\t--build-arg LDFLAGS='$(ldflags)' \\"))
15+
16+
dockerfile, err := os.ReadFile("Dockerfile")
17+
require.NoError(t, err)
18+
require.Contains(t, string(dockerfile), "\nARG LDFLAGS\n")
19+
require.Contains(t, string(dockerfile), `-ldflags "$LDFLAGS"`)
20+
}

decentralized-api/internal/event_listener/integration_test.go

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"os"
99
"strconv"
10+
"sync"
11+
"sync/atomic"
1012
"testing"
1113
"time"
1214

@@ -160,6 +162,7 @@ func (m *MockBrokerChainBridge) GetParams() (*types.QueryParamsResponse, error)
160162

161163
type MockRandomSeedManager struct {
162164
mock.Mock
165+
onGenerate func(epochIndex uint64)
163166
}
164167

165168
func (m *MockRandomSeedManager) ChangeCurrentSeed() {
@@ -176,16 +179,28 @@ func (m *MockRandomSeedManager) RequestMoney(epochIndex uint64) {
176179
}
177180

178181
func (m *MockRandomSeedManager) CreateNewSeed(epochIndex uint64) (*apiconfig.SeedInfo, error) {
179-
m.Called()
180-
return nil, nil
182+
m.Called(epochIndex)
183+
// Match the signature ListRandomSeeds returns after GenerateSeedInfo so
184+
// confirmSeedLocally can succeed the same way production restore does.
185+
return &apiconfig.SeedInfo{
186+
Seed: 1,
187+
EpochIndex: epochIndex,
188+
Signature: integrationTestSeedSignature,
189+
}, nil
181190
}
182191

183192
func (m *MockRandomSeedManager) GenerateSeedInfo(epochIndex uint64) {
184193
m.Called(epochIndex)
194+
if m.onGenerate != nil {
195+
m.onGenerate(epochIndex)
196+
}
185197
}
186198

187199
type MockQueryClient struct {
188200
mock.Mock
201+
listRandomSeedsCalls atomic.Int64
202+
mu sync.Mutex
203+
submittedByEpoch map[uint64]*types.RandomSeed
189204
}
190205

191206
func (m *MockQueryClient) EpochInfo(ctx context.Context, req *types.QueryEpochInfoRequest, opts ...grpc.CallOption) (*types.QueryEpochInfoResponse, error) {
@@ -201,6 +216,36 @@ func (m *MockQueryClient) Params(ctx context.Context, req *types.QueryParamsRequ
201216
return args.Get(0).(*types.QueryParamsResponse), args.Error(1)
202217
}
203218

219+
const integrationTestSeedParticipant = "some-address"
220+
const integrationTestSeedSignature = "integration-test-seed-signature"
221+
222+
// ListRandomSeeds is not testify-expectation based: ensureSeedSubmitted runs
223+
// asynchronously and can race ExpectedCalls = nil in setLatestEpoch.
224+
// After GenerateSeedInfo, the seed becomes visible here so later ensures take
225+
// the confirm path and stop resubmitting (same shape as production).
226+
func (m *MockQueryClient) ListRandomSeeds(ctx context.Context, req *types.QueryRandomSeedsRequest, opts ...grpc.CallOption) (*types.QueryRandomSeedsResponse, error) {
227+
m.listRandomSeedsCalls.Add(1)
228+
m.mu.Lock()
229+
defer m.mu.Unlock()
230+
if seed, ok := m.submittedByEpoch[req.EpochIndex]; ok {
231+
return &types.QueryRandomSeedsResponse{Seeds: []*types.RandomSeed{seed}}, nil
232+
}
233+
return &types.QueryRandomSeedsResponse{}, nil
234+
}
235+
236+
func (m *MockQueryClient) markSeedSubmitted(epochIndex uint64) {
237+
m.mu.Lock()
238+
defer m.mu.Unlock()
239+
if m.submittedByEpoch == nil {
240+
m.submittedByEpoch = make(map[uint64]*types.RandomSeed)
241+
}
242+
m.submittedByEpoch[epochIndex] = &types.RandomSeed{
243+
Participant: integrationTestSeedParticipant,
244+
EpochIndex: epochIndex,
245+
Signature: integrationTestSeedSignature,
246+
}
247+
}
248+
204249
// Test setup helpers
205250

206251
type IntegrationTestSetup struct {
@@ -219,7 +264,11 @@ func createIntegrationTestSetup(reconcilialtionConfig *MlNodeReconciliationConfi
219264
os.Setenv("ENFORCED_MODEL_ID", "disabled")
220265

221266
mockQueryClient := &MockQueryClient{}
222-
mockSeedManager := &MockRandomSeedManager{}
267+
mockSeedManager := &MockRandomSeedManager{
268+
onGenerate: func(epochIndex uint64) {
269+
mockQueryClient.markSeedSubmitted(epochIndex)
270+
},
271+
}
223272

224273
phaseTracker := &chainphase.ChainPhaseTracker{}
225274

@@ -446,7 +495,19 @@ func (setup *IntegrationTestSetup) simulateBlock(height int64) error {
446495
Height: height,
447496
Hash: fmt.Sprintf("hash-%d", height),
448497
}
449-
return setup.Dispatcher.ProcessNewBlock(context.Background(), blockInfo)
498+
err := setup.Dispatcher.ProcessNewBlock(context.Background(), blockInfo)
499+
setup.waitForSeedEnsureIdle()
500+
return err
501+
}
502+
503+
func (setup *IntegrationTestSetup) waitForSeedEnsureIdle() {
504+
deadline := time.Now().Add(2 * time.Second)
505+
for time.Now().Before(deadline) {
506+
if !setup.Dispatcher.seedEnsureInFlight.Load() {
507+
return
508+
}
509+
time.Sleep(5 * time.Millisecond)
510+
}
450511
}
451512

452513
func (setup *IntegrationTestSetup) getNodeClient(nodeId string, port int) *mlnodeclient.MockClient {
@@ -653,6 +714,8 @@ func TestRegularPocScenario(t *testing.T) {
653714
assertNodeClient(t, expected, node2Client)
654715
i++
655716
}
717+
require.GreaterOrEqual(t, setup.MockQueryClient.listRandomSeedsCalls.Load(), int64(1),
718+
"seed ensure should query ListRandomSeeds at least once during PoC window")
656719

657720
pocValStart := i
658721
pocValEnd := pocValStart + setup.EpochParams.PocValidationDelay + setup.EpochParams.PocValidationDuration

0 commit comments

Comments
 (0)