feat(genesis-writer): reward pool and reward migration (superseded) - #498
Closed
rickyrombo wants to merge 2 commits into
Closed
feat(genesis-writer): reward pool and reward migration (superseded)#498rickyrombo wants to merge 2 commits into
rickyrombo wants to merge 2 commits into
Conversation
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
force-pushed
the
feat/genesis-writer-reward-projection
branch
from
August 10, 2026 23:23
9df7b08 to
449bf89
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Superseded. Being reworked to build reward transactions directly from table state rather than from replayed transactions. Will reopen as a new PR.