Skip to content

Commit 42d996c

Browse files
qa: add retransmit metro test to qa.testnet (#4156)
## Summary of Changes * This PR reworks the `TestQA_MulticastSettlement`. * Deleted `e2e/qa_shred_settlement_test.go.` since it only contained one setup method that I moved into the `TestQA_MulticastSettlement` file. * It checks the env we are in, if mainnet, continue as normal, if testnet and program config retransmit flag off, continue as normal too. If testnet and program config retransmit flag on do the following: 1. Assert that when trying to buy a seat in a metro other than frankfurt fails 2. Try to buy a seat in frankfurt and assert that the user accesspass has access to these multicast groups: `edge-solana-retrans` * The PR deletes `e2e/qa_retransmit_only_settlement_test.go`. `TestQA_RetransmitOnlySettlement` drove the same positive path, so the reworked test covers it now. Its device selector, its metro-code formatter and its group assertion moved into the multicast test file. * The group codes now come from one flag, `-retransmit-group-codes`, which replaces `-leader-group-code` and `-retransmit-group-code`. The check requires the seat's groups to equal that list, so any extra group fails it. * `sdk/shreds/go/state.go` gains `IsRetransmitOnlyOnboardingEnforced`, which reads bit 7 of the program config flags. * `e2e/internal/qa/client_settlement.go` gains that same read, plus `ClosestNonRetransmitOnlyDevice` for the device the rejection step pays on. * A companion change in `malbeclabs/infra` passes `-retransmit-group-codes=edge-solana-retrans` in `qa.testnet.yml` and deletes `qa.retransmits.testnet.yml`. Merge both together, or the hourly job runs the group check without the codes it needs.
1 parent ea2000b commit 42d996c

9 files changed

Lines changed: 833 additions & 798 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ All notable changes to this project will be documented in this file.
2323
- Samples dropped because a partition's onchain account is full are now counted too, under `reason="account_full"` plus `submitter_account_full` on the errors counter. That path reports success to its caller, so a warning was the only trace, and its count was wrong: it reported the whole flushed partition rather than the samples actually lost. (#4145)
2424
- A failed submission now retries from the first unwritten sample rather than restarting at the beginning of the flushed partition, and only the unwritten remainder is requeued. Previously a mid-partition error made every subsequent attempt re-send batches that were already onchain, appending those samples a second time and pushing the account toward the sample cap it is measured against. (#4145)
2525
- Agent logs now identify the ledger RPC endpoint in use, and the peer count and the stale-program-data warning report state transitions instead of firing on every refresh. New lines name the resolved remote address of each connection (a bad load balancer address behind a hostname was invisible before), and a refresh that finds no peers is now a warning rather than a Debug line. The stale-cache warning also moves off the package-global `slog` onto the agent's own logger, so it is formatted and leveled with everything else. New: `doublezero_device_telemetry_agent_peers` gauge, and `pinger_epoch_fetch` on the errors counter for every exhausted epoch fetch. (#4147)
26+
- QA
27+
- Rework existing TestQA_MulticastSettlement and adapt it to the new `FLAG_RETRANSMIT_ONLY_ONBOARDING_ENFORCED_BIT` flag. Test now checks for this flag in the ProgramConfig solana account and depending on if it's on or off tries to assert that no new user can subscribe to a non retransmit-only metro unless that metro has the retransmit-only flag enabled in the MetroHistory account. (#4156)
2628

2729
## [v0.33.0](https://github.com/malbeclabs/doublezero/compare/client/v0.32.0...client/v0.33.0) - 2026-07-31
2830

e2e/internal/qa/client.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,21 @@ func (c *Client) GetServiceabilityUser(ctx context.Context) (*serviceability.Use
507507
return nil, fmt.Errorf("serviceability user not found for client IP %s on host %s", publicIP, c.Host)
508508
}
509509

510+
func (c *Client) GetMulticastServiceabilityUser(ctx context.Context) (*serviceability.User, error) {
511+
data, err := getProgramDataWithRetry(ctx, c.serviceability)
512+
if err != nil {
513+
return nil, fmt.Errorf("failed to get program data on host %s: %w", c.Host, err)
514+
}
515+
publicIP := c.publicIP.To4().String()
516+
for i := range data.Users {
517+
user := &data.Users[i]
518+
if net.IP(user.ClientIp[:]).String() == publicIP && user.UserType == serviceability.UserTypeMulticast {
519+
return user, nil
520+
}
521+
}
522+
return nil, fmt.Errorf("multicast serviceability user not found for client IP %s on host %s", publicIP, c.Host)
523+
}
524+
510525
func (c *Client) GetOwnerPubkey(ctx context.Context) (solana.PublicKey, error) {
511526
user, err := c.GetServiceabilityUser(ctx)
512527
if err != nil {

e2e/internal/qa/client_multicast.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,20 @@ func (c *Client) GetMulticastGroup(ctx context.Context, code string) (*Multicast
141141
return nil, nil
142142
}
143143

144+
// MulticastGroupCodes maps every multicast group pubkey to its code, so a caller
145+
// holding pubkeys off a user account can name them in a log or a failure.
146+
func (c *Client) MulticastGroupCodes(ctx context.Context) (map[solana.PublicKey]string, error) {
147+
data, err := getProgramDataWithRetry(ctx, c.serviceability)
148+
if err != nil {
149+
return nil, fmt.Errorf("failed to get program data on host %s: %w", c.Host, err)
150+
}
151+
codes := make(map[solana.PublicKey]string, len(data.MulticastGroups))
152+
for _, group := range data.MulticastGroups {
153+
codes[solana.PublicKeyFromBytes(group.PubKey[:])] = group.Code
154+
}
155+
return codes, nil
156+
}
157+
144158
func (c *Client) CreateMulticastGroup(ctx context.Context, code string, maxBandwidth string) (*MulticastGroup, error) {
145159
c.log.Debug("Creating multicast group", "host", c.Host, "code", code, "maxBandwidth", maxBandwidth)
146160
resp, err := c.grpcClient.CreateMulticastGroup(ctx, &pb.CreateMulticastGroupRequest{

e2e/internal/qa/client_settlement.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,30 @@ func (c *Client) ClosestRetransmitOnlyDevice(ctx context.Context) (*Device, map[
164164
return nil, retransmitOnly, nil
165165
}
166166

167+
device, err := c.closestDeviceInMetros(ctx, retransmitOnly, true)
168+
if err != nil {
169+
return nil, retransmitOnly, err
170+
}
171+
return device, retransmitOnly, nil
172+
}
173+
174+
// ClosestNonRetransmitOnlyDevice returns the reachable device with the lowest
175+
// average latency whose metro is not flagged retransmit-only. A nil device means
176+
// every reachable metro is flagged, so no metro is left to reject a new seat.
177+
func (c *Client) ClosestNonRetransmitOnlyDevice(ctx context.Context) (*Device, error) {
178+
retransmitOnly, err := c.RetransmitOnlyExchangeKeys(ctx)
179+
if err != nil {
180+
return nil, err
181+
}
182+
return c.closestDeviceInMetros(ctx, retransmitOnly, false)
183+
}
184+
185+
// closestDeviceInMetros returns the lowest-latency reachable device whose metro
186+
// membership in exchangeKeys equals want.
187+
func (c *Client) closestDeviceInMetros(ctx context.Context, exchangeKeys map[string]bool, want bool) (*Device, error) {
167188
latencies, err := c.GetLatency(ctx)
168189
if err != nil {
169-
return nil, retransmitOnly, fmt.Errorf("failed to get latency on host %s: %w", c.Host, err)
190+
return nil, fmt.Errorf("failed to get latency on host %s: %w", c.Host, err)
170191
}
171192

172193
var bestDevice *Device
@@ -176,7 +197,7 @@ func (c *Client) ClosestRetransmitOnlyDevice(ctx context.Context) (*Device, map[
176197
continue
177198
}
178199
device, ok := c.devices[l.DeviceCode]
179-
if !ok || !retransmitOnly[device.ExchangePubKey] {
200+
if !ok || exchangeKeys[device.ExchangePubKey] != want {
180201
continue
181202
}
182203
if l.AvgLatencyNs < bestAvg {
@@ -185,9 +206,10 @@ func (c *Client) ClosestRetransmitOnlyDevice(ctx context.Context) (*Device, map[
185206
}
186207
}
187208
if bestDevice != nil {
188-
c.log.Debug("Determined closest retransmit-only device", "host", c.Host, "deviceCode", bestDevice.Code, "avgLatencyNs", bestAvg)
209+
c.log.Debug("Determined closest device", "host", c.Host, "deviceCode", bestDevice.Code,
210+
"avgLatencyNs", bestAvg, "retransmitOnly", want)
189211
}
190-
return bestDevice, retransmitOnly, nil
212+
return bestDevice, nil
191213
}
192214

193215
// FeedSeatPrice calls the FeedSeatPrice RPC to query seat pricing for a single
@@ -769,6 +791,19 @@ func (c *Client) IsSeatProratingEnabled(ctx context.Context) (bool, error) {
769791
return cfg.IsProratedServiceEnabled(), nil
770792
}
771793

794+
func (c *Client) IsRetransmitOnlyOnboardingEnforced(ctx context.Context) (bool, error) {
795+
programID, err := solana.PublicKeyFromBase58(c.ShredSubscriptionProgramID)
796+
if err != nil {
797+
return false, fmt.Errorf("failed to parse shred subscription program ID %q: %w", c.ShredSubscriptionProgramID, err)
798+
}
799+
800+
cfg, err := c.shredsClient(programID).FetchProgramConfig(ctx)
801+
if err != nil {
802+
return false, fmt.Errorf("failed to fetch program config on host %s: %w", c.Host, err)
803+
}
804+
return cfg.IsRetransmitOnlyOnboardingEnforced(), nil
805+
}
806+
772807
// IsProgramPaused returns true if the shred-subscription program config has
773808
// the paused flag set. While paused, the oracle cannot ack instant seat
774809
// allocation requests, which leaves the seat un-withdrawable.

0 commit comments

Comments
 (0)