Skip to content

feat(genesis-writer): reward pool and reward migration (superseded) - #498

Closed
rickyrombo wants to merge 2 commits into
mainfrom
feat/genesis-writer-reward-projection
Closed

feat(genesis-writer): reward pool and reward migration (superseded)#498
rickyrombo wants to merge 2 commits into
mainfrom
feat/genesis-writer-reward-projection

Conversation

@rickyrombo

@rickyrombo rickyrombo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Superseded. Being reworked to build reward transactions directly from table state rather than from replayed transactions. Will reopen as a new PR.

Base automatically changed from feat/genesis-writer-core-dsn to main August 10, 2026 23:23
The writer emitted reward transactions into core_transactions and nothing
else. core_reward_pools and core_rewards are written by the ABCI finalize
path, which genesis blocks never traverse — addTx appends raw bytes with no
processing — so replayed rewards arrived as transactions with no state, and
no state means no claim.

Project the reward tables the way projectBlockAuthState already projects the
auth tables: re-decode each transaction inside the block's postgres
transaction and apply its effects. The state effects are factored out of
finalizeCreateRewardPool / finalizeCreateReward / finalizeLegacyCreateReward
into shared writers so the writer and FinalizeBlock cannot drift. The two
projections now share one decode per transaction rather than one each; at
tens of millions of transactions that is not noise.

The projection deliberately does not re-run admission control. Signature,
deadline and pool-authorization gates are checks the source chain already
ran; re-running them against a different chain's heights and a later point on
the pool-authority timeline rejects valid history. Concretely, 442 of the 471
production rewards were signed by a pre-rotation key, so
checkPoolAuthorization would drop all of them.

Also emit all four pools as explicit CreateRewardPool transactions read from
the old chain's core_reward_pools, and stop replaying the two
SET_AUTHORITIES transactions. Only one of the four pools was ever created by
a transaction; the other three are derived by 00034_reward_pools.sql from
core_rewards as a per-manager union of claim authorities, which leaves the
pool set reconstructible only by re-running a migration against a table it
also rewrites. Creating each pool directly at its final authority set makes
the rotations redundant and retires the derivation.

That end-state creation is why the projection must not union a legacy
reward's inline claim authorities into its pool the way
finalizeLegacyCreateReward does. Both rotated pools were rotated off
launchpad keys derived from a leaked deterministic secret, and the rewards
still carrying those keys would fold them back in — handing attestation
authority back to the compromised keys and undoing the reason pools exist.

The synthesized creates carry empty signatures. A valid rm_owner_signature
requires the launchpad's ed25519 RM secret, and a valid envelope signature
requires a key in the pool's final authority set; neither is ours, and there
is no partially-signed form that validates. Genesis-range transactions are
authorized by genesis_migration_address / genesis_migration_end_height, not
by per-transaction signature validity — signAndMarshal already overwrites
Signer after signing — so an unsigned pool create is honest about what it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rickyrombo
rickyrombo force-pushed the feat/genesis-writer-reward-projection branch from 9df7b08 to 449bf89 Compare August 10, 2026 23:23
…actions

Follows the pool creates in the previous commit: with pools emitted as
transactions, the rewards attached to them are the remaining legacy artifact.
443 of the 471 production rewards are in the pre-pool wire format, so the new
chain would have been born depending on the legacy compat layer for its own
history.

Convert them. Each legacy CreateReward is rebuilt as a modern CreateReward and
signed fresh with the key of one of its pool's current authorities, supplied by
the operator. Signing fresh is what makes the conversion possible at all: the
legacy scheme signs a sha256 over a pipe-delimited string that includes
claim_authorities, while the modern envelope signs deterministic-proto bytes of
RewardBody. Carrying the old signature across does not fail loudly — secp256k1
recovery against the wrong digest returns a valid-looking but different address
— it silently writes a wrong core_rewards.sender, which is served publicly as
GetRewardResponse.sender.

Because the signer is a current pool authority, validateCreateRewardPool's and
validateCreateReward's signer-membership checks now pass for real. The pool
creates are signed the same way. rm_owner_signature stays empty on the pool
creates: it is ed25519 by the reward manager keypair, which only the
launchpad's deterministic secret can produce, and that is not something to
forge.

The 28 rewards already in the modern shape pass through byte-for-byte. Their
signers are the pools' current authorities already, so re-signing would
fabricate transactions to no purpose and would change hashes that still match
the old chain.

The reward manager binding is read from the old chain's
core_rewards.rewards_manager_pubkey rather than re-derived from
claim_authorities through launchpad_authority_rm, reproducing the source state
by construction instead of by recomputation. It is keyed on
(tx_hash, block_height): two byte-identical reward transactions were each
committed in two different blocks, so tx_hash alone is ambiguous. A scanned
reward with no row on the old chain is dropped rather than replayed, since
replaying it would invent state the source does not have.

Keys are taken per authority address rather than as the launchpad's master
deterministic secret. The secret derives every per-mint key through
DeriveEthAddressForMint, which lives in the API repo and keys off the mint — a
value this writer has no mapping to. Taking it would mean a cross-repo
dependency or a duplicated derivation plus a mint list, in exchange for
authority over every mint that has ever existed. They are accepted only via
GENESIS_REWARD_SIGNING_KEYS or --reward-signing-keys-file, never on argv, since
argv is readable through ps.

A missing key fails the run rather than falling back to the legacy bytes. A
fallback would turn one missing key into a chain quietly carrying the artifacts
this change exists to remove; --skip-rewards is the deliberate opt-out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rickyrombo rickyrombo changed the title feat(genesis-writer): materialize reward state and emit pool creates feat(genesis-writer): make rewards first-class on the new chain Aug 10, 2026
@rickyrombo rickyrombo changed the title feat(genesis-writer): make rewards first-class on the new chain feat(genesis-writer): reward pool and reward migration (superseded) Aug 11, 2026
@rickyrombo rickyrombo closed this Aug 11, 2026
@rickyrombo
rickyrombo deleted the feat/genesis-writer-reward-projection branch August 11, 2026 01:43
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.

1 participant