Skip to content

serviceability: add ip_verifier_authority_pk to GlobalState and rotate it via SetAuthority - #4207

Merged
elitegreg merged 2 commits into
mainfrom
gm/globalstate-ip-verifier-authority
Aug 19, 2026
Merged

serviceability: add ip_verifier_authority_pk to GlobalState and rotate it via SetAuthority#4207
elitegreg merged 2 commits into
mainfrom
gm/globalstate-ip-verifier-authority

Conversation

@elitegreg

Copy link
Copy Markdown
Contributor

Closes #4196. Part of RFC-27 (rfcs/rfc27-ip-verification.md); tracker #4194.

Summary

  • GlobalState carries ip_verifier_authority_pk, the trust root for RFC-27 IP ownership proof validation, so the verifier key rotates without a program upgrade.
  • SetAuthority gains an optional ip_verifier_authority_pk; authorization is unchanged (GLOBALSTATE_ADMIN permission or the legacy foundation path).
  • doublezero global-config authority set --ip-verifier-authority <pubkey|me> rotates the key, and authority get reports it in both the table and JSON views.
  • The Rust SDK SetAuthorityCommand forwards the new field.
  • Pubkey::default() means "no verifier configured". Enforcement, which lands with the onchain validation issue, must treat that as a hard reject rather than as "any signature passes".

Details

The field is append-only, so accounts written before it existed deserialize with Pubkey::default() through the same unwrap_or_default() path feed_authority_pk uses. On the instruction side, BorshDeserializeIncremental keeps transactions built against the old argument layout decoding, with the new field defaulting to None.

SDK deserializer work for Go, TypeScript, and Python is tracked separately. The global_state fixture is regenerated here because CI's make check-fixtures diffs the goldens against the Rust producer; all three readers ignore trailing bytes and skip meta fields they do not map, so they stay green until that work lands. Running the generator also refreshed its Cargo.lock from 0.31.0 to 0.36.0.

smartcontract/cli/src/init.rs needed no change: it sends InitGlobalState and carries no authority arguments, and the program's initializer seeds the new field to Pubkey::default().

Testing Verification

  • New tests/setauthority_test.rs: rotating the verifier key leaves the other authorities, the foundation allowlist, and the feature flags untouched; a second rotation replaces the previous key; and a later SetAuthority passing None does not clear it. It also asserts a freshly initialized GlobalState reads Pubkey::default(), the state enforcement must reject.
  • Backwards compatibility against the committed base64 vector: it reads back Pubkey::default(), and re-serialized with the field set it round-trips with every other field identical. Note that this stored vector is old enough that its bytes end at user_airdrop_lamports, so appending 32 bytes to it would not have exercised the new field.
  • SetAuthorityArgs unit tests: a pre-field four-option encoding still decodes with ip_verifier_authority_pk: None, and the new field round-trips.
  • Go, TypeScript, and Python SDK suites pass against the regenerated fixture.

@elitegreg
elitegreg marked this pull request as ready for review August 18, 2026 00:56
@elitegreg
elitegreg enabled auto-merge (squash) August 18, 2026 00:56
@elitegreg
elitegreg force-pushed the gm/globalstate-ip-verifier-authority branch from 389fe02 to 07b614c Compare August 18, 2026 18:50
…e it via SetAuthority

The RFC-27 verifier public key is the trust root for IP ownership proof
validation. Storing it in onchain global state lets it rotate without a
program upgrade.

The field is appended to GlobalState, so accounts written before it existed
deserialize with Pubkey::default() through the same unwrap_or_default() path
feed_authority_pk uses. Pubkey::default() means no verifier is configured,
which enforcement must treat as a hard reject rather than as a bypass.

SetAuthority gains an optional ip_verifier_authority_pk;
BorshDeserializeIncremental keeps transactions built against the old argument
layout decoding, with the new field defaulting to None. Authorization is
unchanged: GLOBALSTATE_ADMIN or the legacy foundation path.

The CLI surfaces the key through global-config authority set/get, and the
Rust SDK SetAuthority command builder forwards it. SDK deserializer work for
Go, TypeScript, and Python is tracked separately; the regenerated
global_state fixture already carries the field for that work to land against.
@elitegreg
elitegreg force-pushed the gm/globalstate-ip-verifier-authority branch from 07b614c to f2eb557 Compare August 18, 2026 20:25

@juan-malbeclabs juan-malbeclabs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the change is sound and safe to land.

I read every hunk and traced the surrounding code: the GlobalState layout and its TryFrom, process_set_authority, try_acc_writeresize_account_if_needed, the borsh-incremental derive, the SDK fixture readers, and the remaining GlobalState { .. } literals in the tree.

The thing I specifically went looking for — the account growing 32 bytes on the first GlobalState write after the upgrade — is safe. Every processor that calls try_acc_write(&globalstate, ..) (setauthority, setairdrop, setfeatureflags, the four allowlist add/remove, and the five create_* index bumps) takes system_program as a fixed account, so resize_account_if_needed's invoke(transfer) always has it available. The append-only field, the unwrap_or_default() read path, and the BorshDeserializeIncremental instruction path all behave as advertised, and the legacy-vector test is a good way to pin that down.

No correctness bugs. The inline comments are all non-blocking — the first one is the only one I'd actually push for, and it's a three-line guard. They cluster around one theme: the field documents Pubkey::default() as a meaningful sentinel ("no verifier configured → enforcement must hard-reject"), but nothing in the write path, the read path, or the SDK goldens treats it as one yet. Worth closing before RFC-27 enforcement starts depending on that sentinel.

Ran locally: cargo test -p doublezero-serviceability --lib globalstate (9 passed) and --test setauthority_test (2 passed).

Comment thread smartcontract/cli/src/globalconfig/authority/get.rs Outdated
Comment thread sdk/serviceability/testdata/fixtures/global_state.json
Comment thread smartcontract/programs/doublezero-serviceability/tests/setauthority_test.rs Outdated
Comment thread smartcontract/programs/doublezero-serviceability/src/authorize.rs Outdated
- CLI `authority get` renders an unset verifier as `not set` (`null` under
  `--json`) instead of the base58 all-zero key, which read as a configured value.
- Go SDK `GlobalState` reads `IpVerifierAuthorityPK`, and `fixture_test.go` gains
  GlobalState cases so the golden's trailing field is actually validated.
- New SetAuthority test writes a legacy-sized GlobalState account, covering the
  32-byte grow and rent top-up the first post-upgrade write takes.
- Extract `init_globalstate` into test_helpers instead of duplicating it.
- Drop redundant `ip_verifier_authority_pk: Pubkey::default()` inits in
  authorize.rs tests.
@elitegreg
elitegreg merged commit 2a85917 into main Aug 19, 2026
67 of 75 checks passed
@elitegreg
elitegreg deleted the gm/globalstate-ip-verifier-authority branch August 19, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

serviceability: add ip_verifier_authority_pk to GlobalState and rotate it via SetAuthority

2 participants