Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ All notable changes to this project will be documented in this file.

- SDK
- revdist Python SDK migrated to the async solana-py RPC API (solana-py 0.40.0 removed the sync `Client`). The `Client` read methods (`fetch_config`, `fetch_distribution`, etc.) are now coroutines and must be awaited; `new_rpc_client` returns an `AsyncClient`. (#3945)
- Serviceability
- The `AccessPass` `EdgeSeat` variant now carries a `Vec<FeedSeat>` payload (`feed_key` + per-feed cap) instead of being a bare marker. This changes the `AccessPass` borsh layout for EdgeSeat passes. (#1700)

### Added

- Serviceability
- `Feed` account: a catalog mapping `metro(exchange) → group-set`, managed by a catalog admin (`FEED_AUTHORITY` Permission or `FOUNDATION`) via `CreateFeed`/`UpdateFeed`/`DeleteFeed`. A feed with no metros imposes no restriction. (#1700)
- `SetAccessPassFeeds` provisions feed_keys (SKU seats) onto an EdgeSeat pass; the oracle calls it via its `ACCESS_PASS_ADMIN` Permission. (#1700)
- EdgeSeat multicast connect is metro-gated: a device whose exchange is not covered by any of the pass's feeds is rejected with `MetroMismatch`, and the matching feed's per-feed cap is enforced. (#1700)

### Changes

Expand Down
74 changes: 73 additions & 1 deletion sdk/serviceability/python/serviceability/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class AccountTypeEnum(IntEnum):
TENANT = 13
PERMISSION = 15
TOPOLOGY = 16
FEED = 18


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1023,6 +1024,15 @@ def from_bytes(cls, data: bytes) -> Tenant:
return t


@dataclass
class FeedSeat:
"""One purchased SKU seat on an EdgeSeat access pass."""

feed_key: Pubkey = Pubkey.default()
max_users: int = 0
current_users: int = 0


@dataclass
class AccessPass:
account_type: int = 0
Expand All @@ -1032,6 +1042,7 @@ class AccessPass:
associated_pubkey: Pubkey | None = None # for SolanaValidator, SolanaRPC
others_type_name: str = "" # for Others variant
others_key: str = "" # for Others variant
feed_seats: list[FeedSeat] = field(default_factory=list) # for EdgeSeat variant
client_ip: bytes = b"\x00" * 4
user_payer: Pubkey = Pubkey.default()
last_access_epoch: int = 0
Expand Down Expand Up @@ -1065,7 +1076,19 @@ def from_bytes(cls, data: bytes) -> AccessPass:
elif tag == 3:
ap.others_type_name = r.read_string()
ap.others_key = r.read_string()
# Prepaid (0) and EdgeSeat (4) carry no associated data.
# EdgeSeat carries a Vec<FeedSeat>: u32 count, then each FeedSeat is
# feed_key (32) + max_users (u16) + current_users (u16).
elif tag == 4:
count = r.read_u32()
ap.feed_seats = [
FeedSeat(
feed_key=_read_pubkey(r),
max_users=r.read_u16(),
current_users=r.read_u16(),
)
for _ in range(count)
]
# Prepaid (0) carries no associated data.
ap.client_ip = r.read_ipv4()
ap.user_payer = _read_pubkey(r)
ap.last_access_epoch = r.read_u64()
Expand Down Expand Up @@ -1182,3 +1205,52 @@ def from_bytes(cls, data: bytes) -> TopologyInfo:
t.flex_algo_number = r.read_u8()
t.constraint = TopologyConstraint(r.read_u8())
return t


# ---------------------------------------------------------------------------
# Feed
# ---------------------------------------------------------------------------


@dataclass
class FeedMetro:
"""Maps an exchange (metro) pubkey to the multicast groups joinable from it."""

exchange: Pubkey = Pubkey.default()
groups: list[Pubkey] = field(default_factory=list)


@dataclass
class Feed:
"""Serviceability catalog entry: the metro(exchange) -> group-set map for one SKU.

A Feed with an empty metros list imposes no metro restriction.
"""

account_type: int = 0
owner: Pubkey = Pubkey.default()
bump_seed: int = 0
code: str = ""
name: str = ""
reference_count: int = 0
metros: list[FeedMetro] = field(default_factory=list)
pub_key: Pubkey = Pubkey.default() # set from account address after deserialization

@classmethod
def from_bytes(cls, data: bytes) -> Feed:
r = DefensiveReader(data)
f = cls()
f.account_type = r.read_u8()
f.owner = _read_pubkey(r)
f.bump_seed = r.read_u8()
f.code = r.read_string()
f.name = r.read_string()
f.reference_count = r.read_u32()
# metros is a Vec<(Pubkey, Vec<Pubkey>)>: u32 count, then each entry is an
# exchange pubkey followed by a Vec<Pubkey> of joinable groups.
count = r.read_u32()
f.metros = [
FeedMetro(exchange=_read_pubkey(r), groups=_read_pubkey_vec(r))
for _ in range(count)
]
return f
49 changes: 48 additions & 1 deletion sdk/serviceability/python/serviceability/tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Contributor,
Device,
Exchange,
Feed,
GlobalConfig,
GlobalState,
Link,
Expand Down Expand Up @@ -501,6 +502,16 @@ def test_deserialize(self):
"Owner": ap.owner,
"BumpSeed": ap.bump_seed,
"AccessPassType": ap.access_pass_type_tag,
"EdgeSeatFeedSeatsLen": len(ap.feed_seats),
"EdgeSeatFeedSeat0FeedKey": ap.feed_seats[0].feed_key
if ap.feed_seats
else None,
"EdgeSeatFeedSeat0MaxUsers": ap.feed_seats[0].max_users
if ap.feed_seats
else None,
"EdgeSeatFeedSeat0CurrentUsers": ap.feed_seats[0].current_users
if ap.feed_seats
else None,
"UserPayer": ap.user_payer,
"ConnectionCount": ap.connection_count,
"Status": ap.status,
Expand All @@ -511,15 +522,51 @@ def test_deserialize(self):
"MaxMulticastUsers": ap.max_multicast_users,
},
)
# EdgeSeat is tag 4 and carries no payload; the seat is the user_payer.
# EdgeSeat is tag 4 and now carries a Vec<FeedSeat> payload.
assert ap.access_pass_type_tag == 4
assert ap.associated_pubkey is None
assert len(ap.feed_seats) == 1
assert ap.feed_seats[0].max_users == 7
assert ap.feed_seats[0].current_users == 3
assert ap.unicast_user_count == 2
assert ap.max_unicast_users == 4
assert ap.multicast_user_count == 1
assert ap.max_multicast_users == 3


class TestFixtureFeed:
def test_deserialize(self):
data, meta = _load_fixture("feed")
feed = Feed.from_bytes(data)
_assert_fields(
meta["fields"],
{
"AccountType": feed.account_type,
"Owner": feed.owner,
"BumpSeed": feed.bump_seed,
"Code": feed.code,
"Name": feed.name,
"ReferenceCount": feed.reference_count,
"MetrosLen": len(feed.metros),
"Metro0Exchange": feed.metros[0].exchange,
"Metro0GroupsLen": len(feed.metros[0].groups),
"Metro0Group0": feed.metros[0].groups[0],
"Metro0Group1": feed.metros[0].groups[1],
"Metro1Exchange": feed.metros[1].exchange,
"Metro1GroupsLen": len(feed.metros[1].groups),
"Metro1Group0": feed.metros[1].groups[0],
},
)
assert feed.account_type == 18
assert feed.bump_seed == 239
assert feed.code == "shreds"
assert feed.name == "Shreds"
assert feed.reference_count == 4
assert len(feed.metros) == 2
assert len(feed.metros[0].groups) == 2
assert len(feed.metros[1].groups) == 1


class TestFixtureAccessPassLegacyCapDefaults:
def test_deserialize(self):
# A pre-migration account lacks the 8 trailing cap bytes; counts default to 0 and caps to 1,
Expand Down
Binary file modified sdk/serviceability/testdata/fixtures/access_pass_edge_seat.bin
Binary file not shown.
20 changes: 20 additions & 0 deletions sdk/serviceability/testdata/fixtures/access_pass_edge_seat.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@
"value": "4",
"typ": "u8"
},
{
"name": "EdgeSeatFeedSeatsLen",
"value": "1",
"typ": "u32"
},
{
"name": "EdgeSeatFeedSeat0FeedKey",
"value": "Cyqa5AA25q85i8QRYpQUMwfh4jaCg78NwPu4biE9JCgP",
"typ": "pubkey"
},
{
"name": "EdgeSeatFeedSeat0MaxUsers",
"value": "7",
"typ": "u16"
},
{
"name": "EdgeSeatFeedSeat0CurrentUsers",
"value": "3",
"typ": "u16"
},
{
"name": "ClientIp",
"value": "0.0.0.0",
Expand Down
Binary file added sdk/serviceability/testdata/fixtures/feed.bin
Binary file not shown.
76 changes: 76 additions & 0 deletions sdk/serviceability/testdata/fixtures/feed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "Feed",
"account_type": 18,
"fields": [
{
"name": "AccountType",
"value": "18",
"typ": "u8"
},
{
"name": "Owner",
"value": "G5QKowXuCHtvVwwEx2f1YZxCyBZf9op5oJ5ukgVvTD6F",
"typ": "pubkey"
},
{
"name": "BumpSeed",
"value": "239",
"typ": "u8"
},
{
"name": "Code",
"value": "shreds",
"typ": "string"
},
{
"name": "Name",
"value": "Shreds",
"typ": "string"
},
{
"name": "ReferenceCount",
"value": "4",
"typ": "u32"
},
{
"name": "MetrosLen",
"value": "2",
"typ": "u32"
},
{
"name": "Metro0Exchange",
"value": "G9JjTSFz68Pdue4DTXvTSVhSEC8RrkuzPAPoFsovB1kb",
"typ": "pubkey"
},
{
"name": "Metro0GroupsLen",
"value": "2",
"typ": "u32"
},
{
"name": "Metro0Group0",
"value": "GDD96vz4yxtMKLBBy3BuLRSfVChCZi1ty2hgm57utpQw",
"typ": "pubkey"
},
{
"name": "Metro0Group1",
"value": "GH7YkRi9soP4j2JAUYTMEMBtkDFyGf7oYu1aGGRucd5H",
"typ": "pubkey"
},
{
"name": "Metro1Exchange",
"value": "GM1xPvSEmdsn8iR8z3io8Gw81DpjycDi8mKTmTjuLRjd",
"typ": "pubkey"
},
{
"name": "Metro1GroupsLen",
"value": "1",
"typ": "u32"
},
{
"name": "Metro1Group0",
"value": "GQvN3RAKfUNVYQY7VYzF2CgMGEPWgZKciddMGf3u4EPy",
"typ": "pubkey"
}
]
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading