diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a50f657f..50e90f29 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -85,7 +85,8 @@ jobs: crate: cargo-sort - run: cargo sort --workspace --check - run: cargo fmt --all --check - - run: cargo clippy --all-features -- -D warnings -D clippy::all -D clippy::nursery -D clippy::integer_division -D clippy::arithmetic_side_effects -D clippy::style -D clippy::perf + - run: | + cargo clippy --all-features -- -D warnings -D clippy::all -D clippy::nursery -D clippy::integer_division -D clippy::arithmetic_side_effects -D clippy::style -D clippy::perf build: name: build @@ -105,11 +106,12 @@ jobs: run: cargo-build-sbf env: TIP_ROUTER_PROGRAM_ID: ${{ env.TIP_ROUTER_PROGRAM_ID }} + SBF_OUT_DIR: ${{ github.workspace }}/target/sbf-solana-solana/release - name: Upload MEV Tip Distribution NCN program uses: actions/upload-artifact@v4 with: name: jito_tip_router_program.so - path: target/sbf-solana-solana/release/jito_tip_router_program.so + path: target/sbf-solana-solana/release/ if-no-files-found: error # coverage: @@ -150,11 +152,13 @@ jobs: uses: actions/download-artifact@v4 with: name: jito_tip_router_program.so - path: target/sbf-solana-solana/release/ + path: integration_tests/tests/fixtures/ - uses: taiki-e/install-action@nextest - - run: cargo nextest run --all-features + # Test the non-BPF tests and the BPF tests separately + - run: cargo nextest run --all-features -E 'not test(bpf)' + - run: cargo nextest run --all-features -E 'test(bpf)' env: - SBF_OUT_DIR: ${{ github.workspace }}/target/sbf-solana-solana/release + SBF_OUT_DIR: ${{ github.workspace }}/integration_tests/tests/fixtures # create_release: # name: Create Release diff --git a/Cargo.lock b/Cargo.lock index 210961a3..0dfc9cd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1731,6 +1731,15 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "fast-math" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" +dependencies = [ + "ieee754", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -2216,6 +2225,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ieee754" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" + [[package]] name = "im" version = "15.1.0" @@ -2443,6 +2458,13 @@ dependencies = [ "thiserror", ] +[[package]] +name = "jito-tip-distribution-sdk" +version = "0.0.1" +dependencies = [ + "anchor-lang", +] + [[package]] name = "jito-tip-router-cli" version = "0.0.1" @@ -2501,6 +2523,7 @@ dependencies = [ "jito-restaking-sdk", "jito-vault-core", "jito-vault-sdk", + "meta-merkle-tree", "shank", "solana-program 1.18.26", "spl-associated-token-account", @@ -2513,12 +2536,15 @@ dependencies = [ name = "jito-tip-router-integration-tests" version = "0.0.1" dependencies = [ + "anchor-lang", "borsh 0.10.4", + "bytemuck", "jito-bytemuck", "jito-jsm-core", "jito-restaking-core", "jito-restaking-program", "jito-restaking-sdk", + "jito-tip-distribution-sdk", "jito-tip-router-client", "jito-tip-router-core", "jito-tip-router-program", @@ -2526,6 +2552,7 @@ dependencies = [ "jito-vault-program", "jito-vault-sdk", "log", + "meta-merkle-tree", "shank", "solana-program 1.18.26", "solana-program-test", @@ -2551,6 +2578,7 @@ dependencies = [ "jito-restaking-core", "jito-restaking-program", "jito-restaking-sdk", + "jito-tip-distribution-sdk", "jito-tip-router-core", "jito-vault-core", "jito-vault-program", @@ -2865,6 +2893,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "meta-merkle-tree" +version = "0.0.1" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "fast-math", + "hex", + "jito-bytemuck", + "jito-jsm-core", + "jito-restaking-core", + "jito-restaking-sdk", + "jito-tip-distribution-sdk", + "jito-vault-core", + "jito-vault-sdk", + "rand 0.8.5", + "serde", + "serde_json", + "shank", + "solana-program 1.18.26", + "solana-sdk", + "spl-associated-token-account", + "spl-math", + "spl-token", + "thiserror", +] + [[package]] name = "mime" version = "0.3.17" @@ -3922,9 +3977,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -3980,9 +4035,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -6098,9 +6153,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", diff --git a/Cargo.toml b/Cargo.toml index d8b6127e..3ed20cac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,10 @@ members = [ "clients/rust/jito_tip_router", "core", "integration_tests", + "meta_merkle_tree", "program", - "shank_cli" + "shank_cli", + "tip_distribution_sdk", ] resolver = "2" @@ -30,7 +32,7 @@ edition = "2021" readme = "README.md" [workspace.dependencies] -anchor-lang = { version = "0.30.1", features = ["idl-build"] } +anchor-lang = { version = "0.30.1" } anyhow = "1.0.86" assert_matches = "1.5.0" borsh = { version = "0.10.3" } @@ -41,13 +43,19 @@ clap = { version = "4.5.16", features = ["derive"] } const_str_to_pubkey = "0.1.1" envfile = "0.2.1" env_logger = "0.10.2" +fast-math = "0.1" +getrandom = { version = "0.1.16", features = ["custom"] } + +hex = "0.4.3" log = "0.4.22" matches = "0.1.10" num-derive = "0.4.2" num-traits = "0.2.19" proc-macro2 = "1.0.86" quote = "1.0.36" +rand = "0.8.5" serde = { version = "^1.0", features = ["derive"] } +serde_json = "1.0.102" serde_with = "3.9.0" shank = "0.4.2" shank_idl = "0.4.2" @@ -65,6 +73,9 @@ spl-token = { version = "4.0.0", features = ["no-entrypoint"] } syn = "2.0.72" thiserror = "1.0.57" tokio = { version = "1.36.0", features = ["full"] } +meta-merkle-tree = { path = "./meta_merkle_tree", version = "=0.0.1" } +jito-tip-distribution-sdk = { path = "./tip_distribution_sdk", version = "=0.0.1" } +# jito-tip-distribution = { default-features = false, features = ["no-entrypoint", "no-idl"], git = "https://github.com/jito-foundation/jito-programs", rev = "2849874101336e7ef6ee93bb64b1354d5e682bb9" } jito-tip-router-client = { path = "./clients/rust/jito_tip_router", version = "0.0.1" } jito-tip-router-core = { path = "./core", version = "=0.0.1" } jito-tip-router-program = { path = "./program", version = "=0.0.1" } diff --git a/clients/js/jito_tip_router/accounts/ballotBox.ts b/clients/js/jito_tip_router/accounts/ballotBox.ts index 447e1b51..b03e74c7 100644 --- a/clients/js/jito_tip_router/accounts/ballotBox.ts +++ b/clients/js/jito_tip_router/accounts/ballotBox.ts @@ -52,7 +52,7 @@ import { export type BallotBox = { discriminator: bigint; ncn: Address; - ncnEpoch: bigint; + epoch: bigint; bump: number; slotCreated: bigint; slotConsensusReached: bigint; @@ -67,7 +67,7 @@ export type BallotBox = { export type BallotBoxArgs = { discriminator: number | bigint; ncn: Address; - ncnEpoch: number | bigint; + epoch: number | bigint; bump: number; slotCreated: number | bigint; slotConsensusReached: number | bigint; @@ -83,7 +83,7 @@ export function getBallotBoxEncoder(): Encoder { return getStructEncoder([ ['discriminator', getU64Encoder()], ['ncn', getAddressEncoder()], - ['ncnEpoch', getU64Encoder()], + ['epoch', getU64Encoder()], ['bump', getU8Encoder()], ['slotCreated', getU64Encoder()], ['slotConsensusReached', getU64Encoder()], @@ -100,7 +100,7 @@ export function getBallotBoxDecoder(): Decoder { return getStructDecoder([ ['discriminator', getU64Decoder()], ['ncn', getAddressDecoder()], - ['ncnEpoch', getU64Decoder()], + ['epoch', getU64Decoder()], ['bump', getU8Decoder()], ['slotCreated', getU64Decoder()], ['slotConsensusReached', getU64Decoder()], diff --git a/clients/js/jito_tip_router/accounts/ncnConfig.ts b/clients/js/jito_tip_router/accounts/ncnConfig.ts index 6299e668..ef5e6b2c 100644 --- a/clients/js/jito_tip_router/accounts/ncnConfig.ts +++ b/clients/js/jito_tip_router/accounts/ncnConfig.ts @@ -46,6 +46,8 @@ export type NcnConfig = { ncn: Address; tieBreakerAdmin: Address; feeAdmin: Address; + validSlotsAfterConsensus: bigint; + epochsBeforeStall: bigint; fees: Fees; bump: number; reserved: Array; @@ -56,6 +58,8 @@ export type NcnConfigArgs = { ncn: Address; tieBreakerAdmin: Address; feeAdmin: Address; + validSlotsAfterConsensus: number | bigint; + epochsBeforeStall: number | bigint; fees: FeesArgs; bump: number; reserved: Array; @@ -67,6 +71,8 @@ export function getNcnConfigEncoder(): Encoder { ['ncn', getAddressEncoder()], ['tieBreakerAdmin', getAddressEncoder()], ['feeAdmin', getAddressEncoder()], + ['validSlotsAfterConsensus', getU64Encoder()], + ['epochsBeforeStall', getU64Encoder()], ['fees', getFeesEncoder()], ['bump', getU8Encoder()], ['reserved', getArrayEncoder(getU8Encoder(), { size: 127 })], @@ -79,6 +85,8 @@ export function getNcnConfigDecoder(): Decoder { ['ncn', getAddressDecoder()], ['tieBreakerAdmin', getAddressDecoder()], ['feeAdmin', getAddressDecoder()], + ['validSlotsAfterConsensus', getU64Decoder()], + ['epochsBeforeStall', getU64Decoder()], ['fees', getFeesDecoder()], ['bump', getU8Decoder()], ['reserved', getArrayDecoder(getU8Decoder(), { size: 127 })], diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index 74b44962..8daf53df 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -80,10 +80,24 @@ export const JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST = 0x2219; // 8729 export const JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL = 0x221a; // 8730 /** BallotTallyFull: Merkle root tally full */ export const JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL = 0x221b; // 8731 -/** ConsensusAlreadyReached: Consensus already reached */ +/** ConsensusAlreadyReached: Consensus already reached, cannot change vote */ export const JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED = 0x221c; // 8732 /** ConsensusNotReached: Consensus not reached */ export const JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED = 0x221d; // 8733 +/** EpochSnapshotNotFinalized: Epoch snapshot not finalized */ +export const JITO_TIP_ROUTER_ERROR__EPOCH_SNAPSHOT_NOT_FINALIZED = 0x221e; // 8734 +/** VotingNotValid: Voting not valid, too many slots after consensus reached */ +export const JITO_TIP_ROUTER_ERROR__VOTING_NOT_VALID = 0x221f; // 8735 +/** TieBreakerAdminInvalid: Tie breaker admin invalid */ +export const JITO_TIP_ROUTER_ERROR__TIE_BREAKER_ADMIN_INVALID = 0x2220; // 8736 +/** VotingNotFinalized: Voting not finalized */ +export const JITO_TIP_ROUTER_ERROR__VOTING_NOT_FINALIZED = 0x2221; // 8737 +/** TieBreakerNotInPriorVotes: Tie breaking ballot must be one of the prior votes */ +export const JITO_TIP_ROUTER_ERROR__TIE_BREAKER_NOT_IN_PRIOR_VOTES = 0x2222; // 8738 +/** InvalidMerkleProof: Invalid merkle proof */ +export const JITO_TIP_ROUTER_ERROR__INVALID_MERKLE_PROOF = 0x2223; // 8739 +/** OperatorAdminInvalid: Operator admin needs to sign its vote */ +export const JITO_TIP_ROUTER_ERROR__OPERATOR_ADMIN_INVALID = 0x2224; // 8740 export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW @@ -98,24 +112,31 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST + | typeof JITO_TIP_ROUTER_ERROR__EPOCH_SNAPSHOT_NOT_FINALIZED | typeof JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_FEE_ADMIN | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_NCN | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_NCN_ADMIN | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_WEIGHT_TABLE_ADMIN + | typeof JITO_TIP_ROUTER_ERROR__INVALID_MERKLE_PROOF | typeof JITO_TIP_ROUTER_ERROR__INVALID_MINT_FOR_WEIGHT_TABLE | typeof JITO_TIP_ROUTER_ERROR__MODULO_OVERFLOW | typeof JITO_TIP_ROUTER_ERROR__NEW_PRECISE_NUMBER_ERROR | typeof JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__NO_OPERATORS + | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_ADMIN_INVALID | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL + | typeof JITO_TIP_ROUTER_ERROR__TIE_BREAKER_ADMIN_INVALID + | typeof JITO_TIP_ROUTER_ERROR__TIE_BREAKER_NOT_IN_PRIOR_VOTES | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINTS_LOCKED | typeof JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE | typeof JITO_TIP_ROUTER_ERROR__VAULT_OPERATOR_DELEGATION_FINALIZED + | typeof JITO_TIP_ROUTER_ERROR__VOTING_NOT_FINALIZED + | typeof JITO_TIP_ROUTER_ERROR__VOTING_NOT_VALID | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_NOT_FOUND @@ -131,30 +152,37 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR]: `Cast to imprecise number error`, [JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL]: `NCN config vaults are at capacity`, [JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED]: `Config supported mints do not match NCN Vault Count`, - [JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED]: `Consensus already reached`, + [JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED]: `Consensus already reached, cannot change vote`, [JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED]: `Consensus not reached`, [JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO]: `Zero in the denominator`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE]: `Duplicate mints in table`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION]: `Duplicate vault operator delegation`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST]: `Duplicate Vote Cast`, + [JITO_TIP_ROUTER_ERROR__EPOCH_SNAPSHOT_NOT_FINALIZED]: `Epoch snapshot not finalized`, [JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED]: `Fee cap exceeded`, [JITO_TIP_ROUTER_ERROR__INCORRECT_FEE_ADMIN]: `Incorrect fee admin`, [JITO_TIP_ROUTER_ERROR__INCORRECT_NCN]: `Incorrect NCN`, [JITO_TIP_ROUTER_ERROR__INCORRECT_NCN_ADMIN]: `Incorrect NCN Admin`, [JITO_TIP_ROUTER_ERROR__INCORRECT_WEIGHT_TABLE_ADMIN]: `Incorrect weight table admin`, + [JITO_TIP_ROUTER_ERROR__INVALID_MERKLE_PROOF]: `Invalid merkle proof`, [JITO_TIP_ROUTER_ERROR__INVALID_MINT_FOR_WEIGHT_TABLE]: `Invalid mint for weight table`, [JITO_TIP_ROUTER_ERROR__MODULO_OVERFLOW]: `Modulo Overflow`, [JITO_TIP_ROUTER_ERROR__NEW_PRECISE_NUMBER_ERROR]: `New precise number error`, [JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE]: `There are no mints in the table`, [JITO_TIP_ROUTER_ERROR__NO_OPERATORS]: `No operators in ncn`, + [JITO_TIP_ROUTER_ERROR__OPERATOR_ADMIN_INVALID]: `Operator admin needs to sign its vote`, [JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED]: `Operator is already finalized - should not happen`, [JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL]: `Operator votes full`, + [JITO_TIP_ROUTER_ERROR__TIE_BREAKER_ADMIN_INVALID]: `Tie breaker admin invalid`, + [JITO_TIP_ROUTER_ERROR__TIE_BREAKER_NOT_IN_PRIOR_VOTES]: `Tie breaking ballot must be one of the prior votes`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE]: `Too many mints for table`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS]: `Too many vault operator delegations`, [JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL]: `Tracked mints are at capacity`, [JITO_TIP_ROUTER_ERROR__TRACKED_MINTS_LOCKED]: `Tracked mints are locked for the epoch`, [JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE]: `Vault index already in use by a different mint`, [JITO_TIP_ROUTER_ERROR__VAULT_OPERATOR_DELEGATION_FINALIZED]: `Vault operator delegation is already finalized - should not happen`, + [JITO_TIP_ROUTER_ERROR__VOTING_NOT_FINALIZED]: `Voting not finalized`, + [JITO_TIP_ROUTER_ERROR__VOTING_NOT_VALID]: `Voting not valid, too many slots after consensus reached`, [JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH]: `Weight mints do not match - length`, [JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH]: `Weight mints do not match - mint hash`, [JITO_TIP_ROUTER_ERROR__WEIGHT_NOT_FOUND]: `Weight not found`, diff --git a/clients/js/jito_tip_router/instructions/castVote.ts b/clients/js/jito_tip_router/instructions/castVote.ts new file mode 100644 index 00000000..53860e6f --- /dev/null +++ b/clients/js/jito_tip_router/instructions/castVote.ts @@ -0,0 +1,293 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type IAccountMeta, + type IAccountSignerMeta, + type IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const CAST_VOTE_DISCRIMINATOR = 11; + +export function getCastVoteDiscriminatorBytes() { + return getU8Encoder().encode(CAST_VOTE_DISCRIMINATOR); +} + +export type CastVoteInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountNcnConfig extends string | IAccountMeta = string, + TAccountBallotBox extends string | IAccountMeta = string, + TAccountNcn extends string | IAccountMeta = string, + TAccountEpochSnapshot extends string | IAccountMeta = string, + TAccountOperatorSnapshot extends string | IAccountMeta = string, + TAccountOperator extends string | IAccountMeta = string, + TAccountOperatorAdmin extends string | IAccountMeta = string, + TAccountRestakingProgram extends string | IAccountMeta = string, + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountNcnConfig extends string + ? ReadonlyAccount + : TAccountNcnConfig, + TAccountBallotBox extends string + ? WritableAccount + : TAccountBallotBox, + TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountEpochSnapshot extends string + ? ReadonlyAccount + : TAccountEpochSnapshot, + TAccountOperatorSnapshot extends string + ? ReadonlyAccount + : TAccountOperatorSnapshot, + TAccountOperator extends string + ? ReadonlyAccount + : TAccountOperator, + TAccountOperatorAdmin extends string + ? ReadonlySignerAccount & + IAccountSignerMeta + : TAccountOperatorAdmin, + TAccountRestakingProgram extends string + ? ReadonlyAccount + : TAccountRestakingProgram, + ...TRemainingAccounts, + ] + >; + +export type CastVoteInstructionData = { + discriminator: number; + metaMerkleRoot: ReadonlyUint8Array; + epoch: bigint; +}; + +export type CastVoteInstructionDataArgs = { + metaMerkleRoot: ReadonlyUint8Array; + epoch: number | bigint; +}; + +export function getCastVoteInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['metaMerkleRoot', fixEncoderSize(getBytesEncoder(), 32)], + ['epoch', getU64Encoder()], + ]), + (value) => ({ ...value, discriminator: CAST_VOTE_DISCRIMINATOR }) + ); +} + +export function getCastVoteInstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['metaMerkleRoot', fixDecoderSize(getBytesDecoder(), 32)], + ['epoch', getU64Decoder()], + ]); +} + +export function getCastVoteInstructionDataCodec(): Codec< + CastVoteInstructionDataArgs, + CastVoteInstructionData +> { + return combineCodec( + getCastVoteInstructionDataEncoder(), + getCastVoteInstructionDataDecoder() + ); +} + +export type CastVoteInput< + TAccountNcnConfig extends string = string, + TAccountBallotBox extends string = string, + TAccountNcn extends string = string, + TAccountEpochSnapshot extends string = string, + TAccountOperatorSnapshot extends string = string, + TAccountOperator extends string = string, + TAccountOperatorAdmin extends string = string, + TAccountRestakingProgram extends string = string, +> = { + ncnConfig: Address; + ballotBox: Address; + ncn: Address; + epochSnapshot: Address; + operatorSnapshot: Address; + operator: Address; + operatorAdmin: TransactionSigner; + restakingProgram: Address; + metaMerkleRoot: CastVoteInstructionDataArgs['metaMerkleRoot']; + epoch: CastVoteInstructionDataArgs['epoch']; +}; + +export function getCastVoteInstruction< + TAccountNcnConfig extends string, + TAccountBallotBox extends string, + TAccountNcn extends string, + TAccountEpochSnapshot extends string, + TAccountOperatorSnapshot extends string, + TAccountOperator extends string, + TAccountOperatorAdmin extends string, + TAccountRestakingProgram extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: CastVoteInput< + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountEpochSnapshot, + TAccountOperatorSnapshot, + TAccountOperator, + TAccountOperatorAdmin, + TAccountRestakingProgram + >, + config?: { programAddress?: TProgramAddress } +): CastVoteInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountEpochSnapshot, + TAccountOperatorSnapshot, + TAccountOperator, + TAccountOperatorAdmin, + TAccountRestakingProgram +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + ncnConfig: { value: input.ncnConfig ?? null, isWritable: false }, + ballotBox: { value: input.ballotBox ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + epochSnapshot: { value: input.epochSnapshot ?? null, isWritable: false }, + operatorSnapshot: { + value: input.operatorSnapshot ?? null, + isWritable: false, + }, + operator: { value: input.operator ?? null, isWritable: false }, + operatorAdmin: { value: input.operatorAdmin ?? null, isWritable: false }, + restakingProgram: { + value: input.restakingProgram ?? null, + isWritable: false, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.ballotBox), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.epochSnapshot), + getAccountMeta(accounts.operatorSnapshot), + getAccountMeta(accounts.operator), + getAccountMeta(accounts.operatorAdmin), + getAccountMeta(accounts.restakingProgram), + ], + programAddress, + data: getCastVoteInstructionDataEncoder().encode( + args as CastVoteInstructionDataArgs + ), + } as CastVoteInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountEpochSnapshot, + TAccountOperatorSnapshot, + TAccountOperator, + TAccountOperatorAdmin, + TAccountRestakingProgram + >; + + return instruction; +} + +export type ParsedCastVoteInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + ncnConfig: TAccountMetas[0]; + ballotBox: TAccountMetas[1]; + ncn: TAccountMetas[2]; + epochSnapshot: TAccountMetas[3]; + operatorSnapshot: TAccountMetas[4]; + operator: TAccountMetas[5]; + operatorAdmin: TAccountMetas[6]; + restakingProgram: TAccountMetas[7]; + }; + data: CastVoteInstructionData; +}; + +export function parseCastVoteInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedCastVoteInstruction { + if (instruction.accounts.length < 8) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = instruction.accounts![accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + ncnConfig: getNextAccount(), + ballotBox: getNextAccount(), + ncn: getNextAccount(), + epochSnapshot: getNextAccount(), + operatorSnapshot: getNextAccount(), + operator: getNextAccount(), + operatorAdmin: getNextAccount(), + restakingProgram: getNextAccount(), + }, + data: getCastVoteInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/jito_tip_router/instructions/index.ts b/clients/js/jito_tip_router/instructions/index.ts index 5771c795..2f0ef35c 100644 --- a/clients/js/jito_tip_router/instructions/index.ts +++ b/clients/js/jito_tip_router/instructions/index.ts @@ -7,6 +7,8 @@ */ export * from './adminUpdateWeightTable'; +export * from './castVote'; +export * from './initializeBallotBox'; export * from './initializeEpochSnapshot'; export * from './initializeNCNConfig'; export * from './initializeOperatorSnapshot'; @@ -14,5 +16,7 @@ export * from './initializeTrackedMints'; export * from './initializeWeightTable'; export * from './registerMint'; export * from './setConfigFees'; +export * from './setMerkleRoot'; export * from './setNewAdmin'; +export * from './setTieBreaker'; export * from './snapshotVaultOperatorDelegation'; diff --git a/clients/js/jito_tip_router/instructions/initializeBallotBox.ts b/clients/js/jito_tip_router/instructions/initializeBallotBox.ts new file mode 100644 index 00000000..b660ef35 --- /dev/null +++ b/clients/js/jito_tip_router/instructions/initializeBallotBox.ts @@ -0,0 +1,246 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type IAccountMeta, + type IAccountSignerMeta, + type IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const INITIALIZE_BALLOT_BOX_DISCRIMINATOR = 10; + +export function getInitializeBallotBoxDiscriminatorBytes() { + return getU8Encoder().encode(INITIALIZE_BALLOT_BOX_DISCRIMINATOR); +} + +export type InitializeBallotBoxInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountNcnConfig extends string | IAccountMeta = string, + TAccountBallotBox extends string | IAccountMeta = string, + TAccountNcn extends string | IAccountMeta = string, + TAccountPayer extends string | IAccountMeta = string, + TAccountSystemProgram extends + | string + | IAccountMeta = '11111111111111111111111111111111', + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountNcnConfig extends string + ? ReadonlyAccount + : TAccountNcnConfig, + TAccountBallotBox extends string + ? WritableAccount + : TAccountBallotBox, + TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountPayer extends string + ? WritableSignerAccount & + IAccountSignerMeta + : TAccountPayer, + TAccountSystemProgram extends string + ? ReadonlyAccount + : TAccountSystemProgram, + ...TRemainingAccounts, + ] + >; + +export type InitializeBallotBoxInstructionData = { + discriminator: number; + epoch: bigint; +}; + +export type InitializeBallotBoxInstructionDataArgs = { epoch: number | bigint }; + +export function getInitializeBallotBoxInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['epoch', getU64Encoder()], + ]), + (value) => ({ + ...value, + discriminator: INITIALIZE_BALLOT_BOX_DISCRIMINATOR, + }) + ); +} + +export function getInitializeBallotBoxInstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['epoch', getU64Decoder()], + ]); +} + +export function getInitializeBallotBoxInstructionDataCodec(): Codec< + InitializeBallotBoxInstructionDataArgs, + InitializeBallotBoxInstructionData +> { + return combineCodec( + getInitializeBallotBoxInstructionDataEncoder(), + getInitializeBallotBoxInstructionDataDecoder() + ); +} + +export type InitializeBallotBoxInput< + TAccountNcnConfig extends string = string, + TAccountBallotBox extends string = string, + TAccountNcn extends string = string, + TAccountPayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + ncnConfig: Address; + ballotBox: Address; + ncn: Address; + payer: TransactionSigner; + systemProgram?: Address; + epoch: InitializeBallotBoxInstructionDataArgs['epoch']; +}; + +export function getInitializeBallotBoxInstruction< + TAccountNcnConfig extends string, + TAccountBallotBox extends string, + TAccountNcn extends string, + TAccountPayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: InitializeBallotBoxInput< + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountPayer, + TAccountSystemProgram + >, + config?: { programAddress?: TProgramAddress } +): InitializeBallotBoxInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountPayer, + TAccountSystemProgram +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + ncnConfig: { value: input.ncnConfig ?? null, isWritable: false }, + ballotBox: { value: input.ballotBox ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + payer: { value: input.payer ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + // Resolve default values. + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = + '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>; + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.ballotBox), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.payer), + getAccountMeta(accounts.systemProgram), + ], + programAddress, + data: getInitializeBallotBoxInstructionDataEncoder().encode( + args as InitializeBallotBoxInstructionDataArgs + ), + } as InitializeBallotBoxInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountPayer, + TAccountSystemProgram + >; + + return instruction; +} + +export type ParsedInitializeBallotBoxInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + ncnConfig: TAccountMetas[0]; + ballotBox: TAccountMetas[1]; + ncn: TAccountMetas[2]; + payer: TAccountMetas[3]; + systemProgram: TAccountMetas[4]; + }; + data: InitializeBallotBoxInstructionData; +}; + +export function parseInitializeBallotBoxInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedInitializeBallotBoxInstruction { + if (instruction.accounts.length < 5) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = instruction.accounts![accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + ncnConfig: getNextAccount(), + ballotBox: getNextAccount(), + ncn: getNextAccount(), + payer: getNextAccount(), + systemProgram: getNextAccount(), + }, + data: getInitializeBallotBoxInstructionDataDecoder().decode( + instruction.data + ), + }; +} diff --git a/clients/js/jito_tip_router/instructions/setMerkleRoot.ts b/clients/js/jito_tip_router/instructions/setMerkleRoot.ts new file mode 100644 index 00000000..ccfa1959 --- /dev/null +++ b/clients/js/jito_tip_router/instructions/setMerkleRoot.ts @@ -0,0 +1,312 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getArrayDecoder, + getArrayEncoder, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type IAccountMeta, + type IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type ReadonlyUint8Array, + type WritableAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const SET_MERKLE_ROOT_DISCRIMINATOR = 12; + +export function getSetMerkleRootDiscriminatorBytes() { + return getU8Encoder().encode(SET_MERKLE_ROOT_DISCRIMINATOR); +} + +export type SetMerkleRootInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountNcnConfig extends string | IAccountMeta = string, + TAccountNcn extends string | IAccountMeta = string, + TAccountBallotBox extends string | IAccountMeta = string, + TAccountVoteAccount extends string | IAccountMeta = string, + TAccountTipDistributionAccount extends string | IAccountMeta = string, + TAccountTipDistributionConfig extends string | IAccountMeta = string, + TAccountTipDistributionProgram extends string | IAccountMeta = string, + TAccountRestakingProgram extends string | IAccountMeta = string, + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountNcnConfig extends string + ? WritableAccount + : TAccountNcnConfig, + TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountBallotBox extends string + ? ReadonlyAccount + : TAccountBallotBox, + TAccountVoteAccount extends string + ? ReadonlyAccount + : TAccountVoteAccount, + TAccountTipDistributionAccount extends string + ? WritableAccount + : TAccountTipDistributionAccount, + TAccountTipDistributionConfig extends string + ? ReadonlyAccount + : TAccountTipDistributionConfig, + TAccountTipDistributionProgram extends string + ? ReadonlyAccount + : TAccountTipDistributionProgram, + TAccountRestakingProgram extends string + ? ReadonlyAccount + : TAccountRestakingProgram, + ...TRemainingAccounts, + ] + >; + +export type SetMerkleRootInstructionData = { + discriminator: number; + proof: Array; + merkleRoot: ReadonlyUint8Array; + maxTotalClaim: bigint; + maxNumNodes: bigint; + epoch: bigint; +}; + +export type SetMerkleRootInstructionDataArgs = { + proof: Array; + merkleRoot: ReadonlyUint8Array; + maxTotalClaim: number | bigint; + maxNumNodes: number | bigint; + epoch: number | bigint; +}; + +export function getSetMerkleRootInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['proof', getArrayEncoder(fixEncoderSize(getBytesEncoder(), 32))], + ['merkleRoot', fixEncoderSize(getBytesEncoder(), 32)], + ['maxTotalClaim', getU64Encoder()], + ['maxNumNodes', getU64Encoder()], + ['epoch', getU64Encoder()], + ]), + (value) => ({ ...value, discriminator: SET_MERKLE_ROOT_DISCRIMINATOR }) + ); +} + +export function getSetMerkleRootInstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['proof', getArrayDecoder(fixDecoderSize(getBytesDecoder(), 32))], + ['merkleRoot', fixDecoderSize(getBytesDecoder(), 32)], + ['maxTotalClaim', getU64Decoder()], + ['maxNumNodes', getU64Decoder()], + ['epoch', getU64Decoder()], + ]); +} + +export function getSetMerkleRootInstructionDataCodec(): Codec< + SetMerkleRootInstructionDataArgs, + SetMerkleRootInstructionData +> { + return combineCodec( + getSetMerkleRootInstructionDataEncoder(), + getSetMerkleRootInstructionDataDecoder() + ); +} + +export type SetMerkleRootInput< + TAccountNcnConfig extends string = string, + TAccountNcn extends string = string, + TAccountBallotBox extends string = string, + TAccountVoteAccount extends string = string, + TAccountTipDistributionAccount extends string = string, + TAccountTipDistributionConfig extends string = string, + TAccountTipDistributionProgram extends string = string, + TAccountRestakingProgram extends string = string, +> = { + ncnConfig: Address; + ncn: Address; + ballotBox: Address; + voteAccount: Address; + tipDistributionAccount: Address; + tipDistributionConfig: Address; + tipDistributionProgram: Address; + restakingProgram: Address; + proof: SetMerkleRootInstructionDataArgs['proof']; + merkleRoot: SetMerkleRootInstructionDataArgs['merkleRoot']; + maxTotalClaim: SetMerkleRootInstructionDataArgs['maxTotalClaim']; + maxNumNodes: SetMerkleRootInstructionDataArgs['maxNumNodes']; + epoch: SetMerkleRootInstructionDataArgs['epoch']; +}; + +export function getSetMerkleRootInstruction< + TAccountNcnConfig extends string, + TAccountNcn extends string, + TAccountBallotBox extends string, + TAccountVoteAccount extends string, + TAccountTipDistributionAccount extends string, + TAccountTipDistributionConfig extends string, + TAccountTipDistributionProgram extends string, + TAccountRestakingProgram extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: SetMerkleRootInput< + TAccountNcnConfig, + TAccountNcn, + TAccountBallotBox, + TAccountVoteAccount, + TAccountTipDistributionAccount, + TAccountTipDistributionConfig, + TAccountTipDistributionProgram, + TAccountRestakingProgram + >, + config?: { programAddress?: TProgramAddress } +): SetMerkleRootInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountNcn, + TAccountBallotBox, + TAccountVoteAccount, + TAccountTipDistributionAccount, + TAccountTipDistributionConfig, + TAccountTipDistributionProgram, + TAccountRestakingProgram +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + ncnConfig: { value: input.ncnConfig ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + ballotBox: { value: input.ballotBox ?? null, isWritable: false }, + voteAccount: { value: input.voteAccount ?? null, isWritable: false }, + tipDistributionAccount: { + value: input.tipDistributionAccount ?? null, + isWritable: true, + }, + tipDistributionConfig: { + value: input.tipDistributionConfig ?? null, + isWritable: false, + }, + tipDistributionProgram: { + value: input.tipDistributionProgram ?? null, + isWritable: false, + }, + restakingProgram: { + value: input.restakingProgram ?? null, + isWritable: false, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.ballotBox), + getAccountMeta(accounts.voteAccount), + getAccountMeta(accounts.tipDistributionAccount), + getAccountMeta(accounts.tipDistributionConfig), + getAccountMeta(accounts.tipDistributionProgram), + getAccountMeta(accounts.restakingProgram), + ], + programAddress, + data: getSetMerkleRootInstructionDataEncoder().encode( + args as SetMerkleRootInstructionDataArgs + ), + } as SetMerkleRootInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountNcn, + TAccountBallotBox, + TAccountVoteAccount, + TAccountTipDistributionAccount, + TAccountTipDistributionConfig, + TAccountTipDistributionProgram, + TAccountRestakingProgram + >; + + return instruction; +} + +export type ParsedSetMerkleRootInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + ncnConfig: TAccountMetas[0]; + ncn: TAccountMetas[1]; + ballotBox: TAccountMetas[2]; + voteAccount: TAccountMetas[3]; + tipDistributionAccount: TAccountMetas[4]; + tipDistributionConfig: TAccountMetas[5]; + tipDistributionProgram: TAccountMetas[6]; + restakingProgram: TAccountMetas[7]; + }; + data: SetMerkleRootInstructionData; +}; + +export function parseSetMerkleRootInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedSetMerkleRootInstruction { + if (instruction.accounts.length < 8) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = instruction.accounts![accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + ncnConfig: getNextAccount(), + ncn: getNextAccount(), + ballotBox: getNextAccount(), + voteAccount: getNextAccount(), + tipDistributionAccount: getNextAccount(), + tipDistributionConfig: getNextAccount(), + tipDistributionProgram: getNextAccount(), + restakingProgram: getNextAccount(), + }, + data: getSetMerkleRootInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/jito_tip_router/instructions/setTieBreaker.ts b/clients/js/jito_tip_router/instructions/setTieBreaker.ts new file mode 100644 index 00000000..c0f0440d --- /dev/null +++ b/clients/js/jito_tip_router/instructions/setTieBreaker.ts @@ -0,0 +1,251 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type IAccountMeta, + type IAccountSignerMeta, + type IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const SET_TIE_BREAKER_DISCRIMINATOR = 13; + +export function getSetTieBreakerDiscriminatorBytes() { + return getU8Encoder().encode(SET_TIE_BREAKER_DISCRIMINATOR); +} + +export type SetTieBreakerInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountNcnConfig extends string | IAccountMeta = string, + TAccountBallotBox extends string | IAccountMeta = string, + TAccountNcn extends string | IAccountMeta = string, + TAccountTieBreakerAdmin extends string | IAccountMeta = string, + TAccountRestakingProgram extends string | IAccountMeta = string, + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountNcnConfig extends string + ? ReadonlyAccount + : TAccountNcnConfig, + TAccountBallotBox extends string + ? WritableAccount + : TAccountBallotBox, + TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountTieBreakerAdmin extends string + ? ReadonlySignerAccount & + IAccountSignerMeta + : TAccountTieBreakerAdmin, + TAccountRestakingProgram extends string + ? ReadonlyAccount + : TAccountRestakingProgram, + ...TRemainingAccounts, + ] + >; + +export type SetTieBreakerInstructionData = { + discriminator: number; + metaMerkleRoot: ReadonlyUint8Array; + epoch: bigint; +}; + +export type SetTieBreakerInstructionDataArgs = { + metaMerkleRoot: ReadonlyUint8Array; + epoch: number | bigint; +}; + +export function getSetTieBreakerInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['metaMerkleRoot', fixEncoderSize(getBytesEncoder(), 32)], + ['epoch', getU64Encoder()], + ]), + (value) => ({ ...value, discriminator: SET_TIE_BREAKER_DISCRIMINATOR }) + ); +} + +export function getSetTieBreakerInstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['metaMerkleRoot', fixDecoderSize(getBytesDecoder(), 32)], + ['epoch', getU64Decoder()], + ]); +} + +export function getSetTieBreakerInstructionDataCodec(): Codec< + SetTieBreakerInstructionDataArgs, + SetTieBreakerInstructionData +> { + return combineCodec( + getSetTieBreakerInstructionDataEncoder(), + getSetTieBreakerInstructionDataDecoder() + ); +} + +export type SetTieBreakerInput< + TAccountNcnConfig extends string = string, + TAccountBallotBox extends string = string, + TAccountNcn extends string = string, + TAccountTieBreakerAdmin extends string = string, + TAccountRestakingProgram extends string = string, +> = { + ncnConfig: Address; + ballotBox: Address; + ncn: Address; + tieBreakerAdmin: TransactionSigner; + restakingProgram: Address; + metaMerkleRoot: SetTieBreakerInstructionDataArgs['metaMerkleRoot']; + epoch: SetTieBreakerInstructionDataArgs['epoch']; +}; + +export function getSetTieBreakerInstruction< + TAccountNcnConfig extends string, + TAccountBallotBox extends string, + TAccountNcn extends string, + TAccountTieBreakerAdmin extends string, + TAccountRestakingProgram extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: SetTieBreakerInput< + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountTieBreakerAdmin, + TAccountRestakingProgram + >, + config?: { programAddress?: TProgramAddress } +): SetTieBreakerInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountTieBreakerAdmin, + TAccountRestakingProgram +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + ncnConfig: { value: input.ncnConfig ?? null, isWritable: false }, + ballotBox: { value: input.ballotBox ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + tieBreakerAdmin: { + value: input.tieBreakerAdmin ?? null, + isWritable: false, + }, + restakingProgram: { + value: input.restakingProgram ?? null, + isWritable: false, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.ballotBox), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.tieBreakerAdmin), + getAccountMeta(accounts.restakingProgram), + ], + programAddress, + data: getSetTieBreakerInstructionDataEncoder().encode( + args as SetTieBreakerInstructionDataArgs + ), + } as SetTieBreakerInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountBallotBox, + TAccountNcn, + TAccountTieBreakerAdmin, + TAccountRestakingProgram + >; + + return instruction; +} + +export type ParsedSetTieBreakerInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + ncnConfig: TAccountMetas[0]; + ballotBox: TAccountMetas[1]; + ncn: TAccountMetas[2]; + tieBreakerAdmin: TAccountMetas[3]; + restakingProgram: TAccountMetas[4]; + }; + data: SetTieBreakerInstructionData; +}; + +export function parseSetTieBreakerInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedSetTieBreakerInstruction { + if (instruction.accounts.length < 5) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = instruction.accounts![accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + ncnConfig: getNextAccount(), + ballotBox: getNextAccount(), + ncn: getNextAccount(), + tieBreakerAdmin: getNextAccount(), + restakingProgram: getNextAccount(), + }, + data: getSetTieBreakerInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/jito_tip_router/programs/jitoTipRouter.ts b/clients/js/jito_tip_router/programs/jitoTipRouter.ts index 3c570619..3d69202e 100644 --- a/clients/js/jito_tip_router/programs/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/programs/jitoTipRouter.ts @@ -14,6 +14,8 @@ import { } from '@solana/web3.js'; import { type ParsedAdminUpdateWeightTableInstruction, + type ParsedCastVoteInstruction, + type ParsedInitializeBallotBoxInstruction, type ParsedInitializeEpochSnapshotInstruction, type ParsedInitializeNCNConfigInstruction, type ParsedInitializeOperatorSnapshotInstruction, @@ -21,7 +23,9 @@ import { type ParsedInitializeWeightTableInstruction, type ParsedRegisterMintInstruction, type ParsedSetConfigFeesInstruction, + type ParsedSetMerkleRootInstruction, type ParsedSetNewAdminInstruction, + type ParsedSetTieBreakerInstruction, type ParsedSnapshotVaultOperatorDelegationInstruction, } from '../instructions'; @@ -48,6 +52,10 @@ export enum JitoTipRouterInstruction { SnapshotVaultOperatorDelegation, RegisterMint, InitializeTrackedMints, + InitializeBallotBox, + CastVote, + SetMerkleRoot, + SetTieBreaker, } export function identifyJitoTipRouterInstruction( @@ -84,6 +92,18 @@ export function identifyJitoTipRouterInstruction( if (containsBytes(data, getU8Encoder().encode(9), 0)) { return JitoTipRouterInstruction.InitializeTrackedMints; } + if (containsBytes(data, getU8Encoder().encode(10), 0)) { + return JitoTipRouterInstruction.InitializeBallotBox; + } + if (containsBytes(data, getU8Encoder().encode(11), 0)) { + return JitoTipRouterInstruction.CastVote; + } + if (containsBytes(data, getU8Encoder().encode(12), 0)) { + return JitoTipRouterInstruction.SetMerkleRoot; + } + if (containsBytes(data, getU8Encoder().encode(13), 0)) { + return JitoTipRouterInstruction.SetTieBreaker; + } throw new Error( 'The provided instruction could not be identified as a jitoTipRouter instruction.' ); @@ -121,4 +141,16 @@ export type ParsedJitoTipRouterInstruction< } & ParsedRegisterMintInstruction) | ({ instructionType: JitoTipRouterInstruction.InitializeTrackedMints; - } & ParsedInitializeTrackedMintsInstruction); + } & ParsedInitializeTrackedMintsInstruction) + | ({ + instructionType: JitoTipRouterInstruction.InitializeBallotBox; + } & ParsedInitializeBallotBoxInstruction) + | ({ + instructionType: JitoTipRouterInstruction.CastVote; + } & ParsedCastVoteInstruction) + | ({ + instructionType: JitoTipRouterInstruction.SetMerkleRoot; + } & ParsedSetMerkleRootInstruction) + | ({ + instructionType: JitoTipRouterInstruction.SetTieBreaker; + } & ParsedSetTieBreakerInstruction); diff --git a/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs index 01c1b216..a86e2a28 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs @@ -18,7 +18,7 @@ pub struct BallotBox { serde(with = "serde_with::As::") )] pub ncn: Pubkey, - pub ncn_epoch: u64, + pub epoch: u64, pub bump: u8, pub slot_created: u64, pub slot_consensus_reached: u64, diff --git a/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs b/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs index eaca31b0..d5b3c81c 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs @@ -28,6 +28,8 @@ pub struct NcnConfig { serde(with = "serde_with::As::") )] pub fee_admin: Pubkey, + pub valid_slots_after_consensus: u64, + pub epochs_before_stall: u64, pub fees: Fees, pub bump: u8, #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] diff --git a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs index ce806cbf..2d87cb79 100644 --- a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs +++ b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs @@ -108,12 +108,33 @@ pub enum JitoTipRouterError { /// 8731 - Merkle root tally full #[error("Merkle root tally full")] BallotTallyFull = 0x221B, - /// 8732 - Consensus already reached - #[error("Consensus already reached")] + /// 8732 - Consensus already reached, cannot change vote + #[error("Consensus already reached, cannot change vote")] ConsensusAlreadyReached = 0x221C, /// 8733 - Consensus not reached #[error("Consensus not reached")] ConsensusNotReached = 0x221D, + /// 8734 - Epoch snapshot not finalized + #[error("Epoch snapshot not finalized")] + EpochSnapshotNotFinalized = 0x221E, + /// 8735 - Voting not valid, too many slots after consensus reached + #[error("Voting not valid, too many slots after consensus reached")] + VotingNotValid = 0x221F, + /// 8736 - Tie breaker admin invalid + #[error("Tie breaker admin invalid")] + TieBreakerAdminInvalid = 0x2220, + /// 8737 - Voting not finalized + #[error("Voting not finalized")] + VotingNotFinalized = 0x2221, + /// 8738 - Tie breaking ballot must be one of the prior votes + #[error("Tie breaking ballot must be one of the prior votes")] + TieBreakerNotInPriorVotes = 0x2222, + /// 8739 - Invalid merkle proof + #[error("Invalid merkle proof")] + InvalidMerkleProof = 0x2223, + /// 8740 - Operator admin needs to sign its vote + #[error("Operator admin needs to sign its vote")] + OperatorAdminInvalid = 0x2224, } impl solana_program::program_error::PrintProgramError for JitoTipRouterError { diff --git a/clients/rust/jito_tip_router/src/generated/instructions/cast_vote.rs b/clients/rust/jito_tip_router/src/generated/instructions/cast_vote.rs new file mode 100644 index 00000000..21fea64b --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/cast_vote.rs @@ -0,0 +1,622 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct CastVote { + pub ncn_config: solana_program::pubkey::Pubkey, + + pub ballot_box: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub epoch_snapshot: solana_program::pubkey::Pubkey, + + pub operator_snapshot: solana_program::pubkey::Pubkey, + + pub operator: solana_program::pubkey::Pubkey, + + pub operator_admin: solana_program::pubkey::Pubkey, + + pub restaking_program: solana_program::pubkey::Pubkey, +} + +impl CastVote { + pub fn instruction( + &self, + args: CastVoteInstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: CastVoteInstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.ballot_box, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.epoch_snapshot, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.operator_snapshot, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.operator, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.operator_admin, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.restaking_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = CastVoteInstructionData::new().try_to_vec().unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct CastVoteInstructionData { + discriminator: u8, +} + +impl CastVoteInstructionData { + pub fn new() -> Self { + Self { discriminator: 11 } + } +} + +impl Default for CastVoteInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CastVoteInstructionArgs { + pub meta_merkle_root: [u8; 32], + pub epoch: u64, +} + +/// Instruction builder for `CastVote`. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` ballot_box +/// 2. `[]` ncn +/// 3. `[]` epoch_snapshot +/// 4. `[]` operator_snapshot +/// 5. `[]` operator +/// 6. `[signer]` operator_admin +/// 7. `[]` restaking_program +#[derive(Clone, Debug, Default)] +pub struct CastVoteBuilder { + ncn_config: Option, + ballot_box: Option, + ncn: Option, + epoch_snapshot: Option, + operator_snapshot: Option, + operator: Option, + operator_admin: Option, + restaking_program: Option, + meta_merkle_root: Option<[u8; 32]>, + epoch: Option, + __remaining_accounts: Vec, +} + +impl CastVoteBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ballot_box(&mut self, ballot_box: solana_program::pubkey::Pubkey) -> &mut Self { + self.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn epoch_snapshot(&mut self, epoch_snapshot: solana_program::pubkey::Pubkey) -> &mut Self { + self.epoch_snapshot = Some(epoch_snapshot); + self + } + #[inline(always)] + pub fn operator_snapshot( + &mut self, + operator_snapshot: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.operator_snapshot = Some(operator_snapshot); + self + } + #[inline(always)] + pub fn operator(&mut self, operator: solana_program::pubkey::Pubkey) -> &mut Self { + self.operator = Some(operator); + self + } + #[inline(always)] + pub fn operator_admin(&mut self, operator_admin: solana_program::pubkey::Pubkey) -> &mut Self { + self.operator_admin = Some(operator_admin); + self + } + #[inline(always)] + pub fn restaking_program( + &mut self, + restaking_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.restaking_program = Some(restaking_program); + self + } + #[inline(always)] + pub fn meta_merkle_root(&mut self, meta_merkle_root: [u8; 32]) -> &mut Self { + self.meta_merkle_root = Some(meta_merkle_root); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = CastVote { + ncn_config: self.ncn_config.expect("ncn_config is not set"), + ballot_box: self.ballot_box.expect("ballot_box is not set"), + ncn: self.ncn.expect("ncn is not set"), + epoch_snapshot: self.epoch_snapshot.expect("epoch_snapshot is not set"), + operator_snapshot: self + .operator_snapshot + .expect("operator_snapshot is not set"), + operator: self.operator.expect("operator is not set"), + operator_admin: self.operator_admin.expect("operator_admin is not set"), + restaking_program: self + .restaking_program + .expect("restaking_program is not set"), + }; + let args = CastVoteInstructionArgs { + meta_merkle_root: self + .meta_merkle_root + .clone() + .expect("meta_merkle_root is not set"), + epoch: self.epoch.clone().expect("epoch is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `cast_vote` CPI accounts. +pub struct CastVoteCpiAccounts<'a, 'b> { + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub epoch_snapshot: &'b solana_program::account_info::AccountInfo<'a>, + + pub operator_snapshot: &'b solana_program::account_info::AccountInfo<'a>, + + pub operator: &'b solana_program::account_info::AccountInfo<'a>, + + pub operator_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `cast_vote` CPI instruction. +pub struct CastVoteCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub epoch_snapshot: &'b solana_program::account_info::AccountInfo<'a>, + + pub operator_snapshot: &'b solana_program::account_info::AccountInfo<'a>, + + pub operator: &'b solana_program::account_info::AccountInfo<'a>, + + pub operator_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: CastVoteInstructionArgs, +} + +impl<'a, 'b> CastVoteCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: CastVoteCpiAccounts<'a, 'b>, + args: CastVoteInstructionArgs, + ) -> Self { + Self { + __program: program, + ncn_config: accounts.ncn_config, + ballot_box: accounts.ballot_box, + ncn: accounts.ncn, + epoch_snapshot: accounts.epoch_snapshot, + operator_snapshot: accounts.operator_snapshot, + operator: accounts.operator, + operator_admin: accounts.operator_admin, + restaking_program: accounts.restaking_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.ballot_box.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.epoch_snapshot.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.operator_snapshot.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.operator.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.operator_admin.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.restaking_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = CastVoteInstructionData::new().try_to_vec().unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(8 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.ncn_config.clone()); + account_infos.push(self.ballot_box.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.epoch_snapshot.clone()); + account_infos.push(self.operator_snapshot.clone()); + account_infos.push(self.operator.clone()); + account_infos.push(self.operator_admin.clone()); + account_infos.push(self.restaking_program.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `CastVote` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` ballot_box +/// 2. `[]` ncn +/// 3. `[]` epoch_snapshot +/// 4. `[]` operator_snapshot +/// 5. `[]` operator +/// 6. `[signer]` operator_admin +/// 7. `[]` restaking_program +#[derive(Clone, Debug)] +pub struct CastVoteCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> CastVoteCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(CastVoteCpiBuilderInstruction { + __program: program, + ncn_config: None, + ballot_box: None, + ncn: None, + epoch_snapshot: None, + operator_snapshot: None, + operator: None, + operator_admin: None, + restaking_program: None, + meta_merkle_root: None, + epoch: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn ncn_config( + &mut self, + ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ballot_box( + &mut self, + ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn epoch_snapshot( + &mut self, + epoch_snapshot: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.epoch_snapshot = Some(epoch_snapshot); + self + } + #[inline(always)] + pub fn operator_snapshot( + &mut self, + operator_snapshot: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.operator_snapshot = Some(operator_snapshot); + self + } + #[inline(always)] + pub fn operator( + &mut self, + operator: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.operator = Some(operator); + self + } + #[inline(always)] + pub fn operator_admin( + &mut self, + operator_admin: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.operator_admin = Some(operator_admin); + self + } + #[inline(always)] + pub fn restaking_program( + &mut self, + restaking_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.restaking_program = Some(restaking_program); + self + } + #[inline(always)] + pub fn meta_merkle_root(&mut self, meta_merkle_root: [u8; 32]) -> &mut Self { + self.instruction.meta_merkle_root = Some(meta_merkle_root); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.instruction.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = CastVoteInstructionArgs { + meta_merkle_root: self + .instruction + .meta_merkle_root + .clone() + .expect("meta_merkle_root is not set"), + epoch: self.instruction.epoch.clone().expect("epoch is not set"), + }; + let instruction = CastVoteCpi { + __program: self.instruction.__program, + + ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + + ballot_box: self.instruction.ballot_box.expect("ballot_box is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + epoch_snapshot: self + .instruction + .epoch_snapshot + .expect("epoch_snapshot is not set"), + + operator_snapshot: self + .instruction + .operator_snapshot + .expect("operator_snapshot is not set"), + + operator: self.instruction.operator.expect("operator is not set"), + + operator_admin: self + .instruction + .operator_admin + .expect("operator_admin is not set"), + + restaking_program: self + .instruction + .restaking_program + .expect("restaking_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct CastVoteCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + ncn_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ballot_box: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + epoch_snapshot: Option<&'b solana_program::account_info::AccountInfo<'a>>, + operator_snapshot: Option<&'b solana_program::account_info::AccountInfo<'a>>, + operator: Option<&'b solana_program::account_info::AccountInfo<'a>>, + operator_admin: Option<&'b solana_program::account_info::AccountInfo<'a>>, + restaking_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + meta_merkle_root: Option<[u8; 32]>, + epoch: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust/jito_tip_router/src/generated/instructions/initialize_ballot_box.rs b/clients/rust/jito_tip_router/src/generated/instructions/initialize_ballot_box.rs new file mode 100644 index 00000000..1c8123a2 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/initialize_ballot_box.rs @@ -0,0 +1,472 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct InitializeBallotBox { + pub ncn_config: solana_program::pubkey::Pubkey, + + pub ballot_box: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub payer: solana_program::pubkey::Pubkey, + + pub system_program: solana_program::pubkey::Pubkey, +} + +impl InitializeBallotBox { + pub fn instruction( + &self, + args: InitializeBallotBoxInstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: InitializeBallotBoxInstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.ballot_box, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = InitializeBallotBoxInstructionData::new() + .try_to_vec() + .unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct InitializeBallotBoxInstructionData { + discriminator: u8, +} + +impl InitializeBallotBoxInstructionData { + pub fn new() -> Self { + Self { discriminator: 10 } + } +} + +impl Default for InitializeBallotBoxInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct InitializeBallotBoxInstructionArgs { + pub epoch: u64, +} + +/// Instruction builder for `InitializeBallotBox`. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` ballot_box +/// 2. `[]` ncn +/// 3. `[writable, signer]` payer +/// 4. `[optional]` system_program (default to `11111111111111111111111111111111`) +#[derive(Clone, Debug, Default)] +pub struct InitializeBallotBoxBuilder { + ncn_config: Option, + ballot_box: Option, + ncn: Option, + payer: Option, + system_program: Option, + epoch: Option, + __remaining_accounts: Vec, +} + +impl InitializeBallotBoxBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ballot_box(&mut self, ballot_box: solana_program::pubkey::Pubkey) -> &mut Self { + self.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + /// `[optional account, default to '11111111111111111111111111111111']` + #[inline(always)] + pub fn system_program(&mut self, system_program: solana_program::pubkey::Pubkey) -> &mut Self { + self.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = InitializeBallotBox { + ncn_config: self.ncn_config.expect("ncn_config is not set"), + ballot_box: self.ballot_box.expect("ballot_box is not set"), + ncn: self.ncn.expect("ncn is not set"), + payer: self.payer.expect("payer is not set"), + system_program: self + .system_program + .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), + }; + let args = InitializeBallotBoxInstructionArgs { + epoch: self.epoch.clone().expect("epoch is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `initialize_ballot_box` CPI accounts. +pub struct InitializeBallotBoxCpiAccounts<'a, 'b> { + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `initialize_ballot_box` CPI instruction. +pub struct InitializeBallotBoxCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: InitializeBallotBoxInstructionArgs, +} + +impl<'a, 'b> InitializeBallotBoxCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: InitializeBallotBoxCpiAccounts<'a, 'b>, + args: InitializeBallotBoxInstructionArgs, + ) -> Self { + Self { + __program: program, + ncn_config: accounts.ncn_config, + ballot_box: accounts.ballot_box, + ncn: accounts.ncn, + payer: accounts.payer, + system_program: accounts.system_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.ballot_box.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.payer.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.system_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = InitializeBallotBoxInstructionData::new() + .try_to_vec() + .unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(5 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.ncn_config.clone()); + account_infos.push(self.ballot_box.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.system_program.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `InitializeBallotBox` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` ballot_box +/// 2. `[]` ncn +/// 3. `[writable, signer]` payer +/// 4. `[]` system_program +#[derive(Clone, Debug)] +pub struct InitializeBallotBoxCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> InitializeBallotBoxCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(InitializeBallotBoxCpiBuilderInstruction { + __program: program, + ncn_config: None, + ballot_box: None, + ncn: None, + payer: None, + system_program: None, + epoch: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn ncn_config( + &mut self, + ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ballot_box( + &mut self, + ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + #[inline(always)] + pub fn system_program( + &mut self, + system_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.instruction.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = InitializeBallotBoxInstructionArgs { + epoch: self.instruction.epoch.clone().expect("epoch is not set"), + }; + let instruction = InitializeBallotBoxCpi { + __program: self.instruction.__program, + + ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + + ballot_box: self.instruction.ballot_box.expect("ballot_box is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + payer: self.instruction.payer.expect("payer is not set"), + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct InitializeBallotBoxCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + ncn_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ballot_box: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + epoch: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust/jito_tip_router/src/generated/instructions/mod.rs b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs index 06b2b508..4871a7c2 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs @@ -5,6 +5,8 @@ //! pub(crate) mod r#admin_update_weight_table; +pub(crate) mod r#cast_vote; +pub(crate) mod r#initialize_ballot_box; pub(crate) mod r#initialize_epoch_snapshot; pub(crate) mod r#initialize_n_c_n_config; pub(crate) mod r#initialize_operator_snapshot; @@ -12,12 +14,15 @@ pub(crate) mod r#initialize_tracked_mints; pub(crate) mod r#initialize_weight_table; pub(crate) mod r#register_mint; pub(crate) mod r#set_config_fees; +pub(crate) mod r#set_merkle_root; pub(crate) mod r#set_new_admin; +pub(crate) mod r#set_tie_breaker; pub(crate) mod r#snapshot_vault_operator_delegation; pub use self::{ - r#admin_update_weight_table::*, r#initialize_epoch_snapshot::*, r#initialize_n_c_n_config::*, + r#admin_update_weight_table::*, r#cast_vote::*, r#initialize_ballot_box::*, + r#initialize_epoch_snapshot::*, r#initialize_n_c_n_config::*, r#initialize_operator_snapshot::*, r#initialize_tracked_mints::*, r#initialize_weight_table::*, - r#register_mint::*, r#set_config_fees::*, r#set_new_admin::*, - r#snapshot_vault_operator_delegation::*, + r#register_mint::*, r#set_config_fees::*, r#set_merkle_root::*, r#set_new_admin::*, + r#set_tie_breaker::*, r#snapshot_vault_operator_delegation::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/instructions/set_merkle_root.rs b/clients/rust/jito_tip_router/src/generated/instructions/set_merkle_root.rs new file mode 100644 index 00000000..1776832d --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/set_merkle_root.rs @@ -0,0 +1,694 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct SetMerkleRoot { + pub ncn_config: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub ballot_box: solana_program::pubkey::Pubkey, + + pub vote_account: solana_program::pubkey::Pubkey, + + pub tip_distribution_account: solana_program::pubkey::Pubkey, + + pub tip_distribution_config: solana_program::pubkey::Pubkey, + + pub tip_distribution_program: solana_program::pubkey::Pubkey, + + pub restaking_program: solana_program::pubkey::Pubkey, +} + +impl SetMerkleRoot { + pub fn instruction( + &self, + args: SetMerkleRootInstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: SetMerkleRootInstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.ncn_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ballot_box, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.vote_account, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.tip_distribution_account, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.tip_distribution_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.tip_distribution_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.restaking_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = SetMerkleRootInstructionData::new().try_to_vec().unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct SetMerkleRootInstructionData { + discriminator: u8, +} + +impl SetMerkleRootInstructionData { + pub fn new() -> Self { + Self { discriminator: 12 } + } +} + +impl Default for SetMerkleRootInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SetMerkleRootInstructionArgs { + pub proof: Vec<[u8; 32]>, + pub merkle_root: [u8; 32], + pub max_total_claim: u64, + pub max_num_nodes: u64, + pub epoch: u64, +} + +/// Instruction builder for `SetMerkleRoot`. +/// +/// ### Accounts: +/// +/// 0. `[writable]` ncn_config +/// 1. `[]` ncn +/// 2. `[]` ballot_box +/// 3. `[]` vote_account +/// 4. `[writable]` tip_distribution_account +/// 5. `[]` tip_distribution_config +/// 6. `[]` tip_distribution_program +/// 7. `[]` restaking_program +#[derive(Clone, Debug, Default)] +pub struct SetMerkleRootBuilder { + ncn_config: Option, + ncn: Option, + ballot_box: Option, + vote_account: Option, + tip_distribution_account: Option, + tip_distribution_config: Option, + tip_distribution_program: Option, + restaking_program: Option, + proof: Option>, + merkle_root: Option<[u8; 32]>, + max_total_claim: Option, + max_num_nodes: Option, + epoch: Option, + __remaining_accounts: Vec, +} + +impl SetMerkleRootBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn ballot_box(&mut self, ballot_box: solana_program::pubkey::Pubkey) -> &mut Self { + self.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn vote_account(&mut self, vote_account: solana_program::pubkey::Pubkey) -> &mut Self { + self.vote_account = Some(vote_account); + self + } + #[inline(always)] + pub fn tip_distribution_account( + &mut self, + tip_distribution_account: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.tip_distribution_account = Some(tip_distribution_account); + self + } + #[inline(always)] + pub fn tip_distribution_config( + &mut self, + tip_distribution_config: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.tip_distribution_config = Some(tip_distribution_config); + self + } + #[inline(always)] + pub fn tip_distribution_program( + &mut self, + tip_distribution_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.tip_distribution_program = Some(tip_distribution_program); + self + } + #[inline(always)] + pub fn restaking_program( + &mut self, + restaking_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.restaking_program = Some(restaking_program); + self + } + #[inline(always)] + pub fn proof(&mut self, proof: Vec<[u8; 32]>) -> &mut Self { + self.proof = Some(proof); + self + } + #[inline(always)] + pub fn merkle_root(&mut self, merkle_root: [u8; 32]) -> &mut Self { + self.merkle_root = Some(merkle_root); + self + } + #[inline(always)] + pub fn max_total_claim(&mut self, max_total_claim: u64) -> &mut Self { + self.max_total_claim = Some(max_total_claim); + self + } + #[inline(always)] + pub fn max_num_nodes(&mut self, max_num_nodes: u64) -> &mut Self { + self.max_num_nodes = Some(max_num_nodes); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = SetMerkleRoot { + ncn_config: self.ncn_config.expect("ncn_config is not set"), + ncn: self.ncn.expect("ncn is not set"), + ballot_box: self.ballot_box.expect("ballot_box is not set"), + vote_account: self.vote_account.expect("vote_account is not set"), + tip_distribution_account: self + .tip_distribution_account + .expect("tip_distribution_account is not set"), + tip_distribution_config: self + .tip_distribution_config + .expect("tip_distribution_config is not set"), + tip_distribution_program: self + .tip_distribution_program + .expect("tip_distribution_program is not set"), + restaking_program: self + .restaking_program + .expect("restaking_program is not set"), + }; + let args = SetMerkleRootInstructionArgs { + proof: self.proof.clone().expect("proof is not set"), + merkle_root: self.merkle_root.clone().expect("merkle_root is not set"), + max_total_claim: self + .max_total_claim + .clone() + .expect("max_total_claim is not set"), + max_num_nodes: self + .max_num_nodes + .clone() + .expect("max_num_nodes is not set"), + epoch: self.epoch.clone().expect("epoch is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `set_merkle_root` CPI accounts. +pub struct SetMerkleRootCpiAccounts<'a, 'b> { + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub vote_account: &'b solana_program::account_info::AccountInfo<'a>, + + pub tip_distribution_account: &'b solana_program::account_info::AccountInfo<'a>, + + pub tip_distribution_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub tip_distribution_program: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `set_merkle_root` CPI instruction. +pub struct SetMerkleRootCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub vote_account: &'b solana_program::account_info::AccountInfo<'a>, + + pub tip_distribution_account: &'b solana_program::account_info::AccountInfo<'a>, + + pub tip_distribution_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub tip_distribution_program: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: SetMerkleRootInstructionArgs, +} + +impl<'a, 'b> SetMerkleRootCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: SetMerkleRootCpiAccounts<'a, 'b>, + args: SetMerkleRootInstructionArgs, + ) -> Self { + Self { + __program: program, + ncn_config: accounts.ncn_config, + ncn: accounts.ncn, + ballot_box: accounts.ballot_box, + vote_account: accounts.vote_account, + tip_distribution_account: accounts.tip_distribution_account, + tip_distribution_config: accounts.tip_distribution_config, + tip_distribution_program: accounts.tip_distribution_program, + restaking_program: accounts.restaking_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.ncn_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ballot_box.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.vote_account.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.tip_distribution_account.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.tip_distribution_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.tip_distribution_program.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.restaking_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = SetMerkleRootInstructionData::new().try_to_vec().unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(8 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.ncn_config.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.ballot_box.clone()); + account_infos.push(self.vote_account.clone()); + account_infos.push(self.tip_distribution_account.clone()); + account_infos.push(self.tip_distribution_config.clone()); + account_infos.push(self.tip_distribution_program.clone()); + account_infos.push(self.restaking_program.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `SetMerkleRoot` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable]` ncn_config +/// 1. `[]` ncn +/// 2. `[]` ballot_box +/// 3. `[]` vote_account +/// 4. `[writable]` tip_distribution_account +/// 5. `[]` tip_distribution_config +/// 6. `[]` tip_distribution_program +/// 7. `[]` restaking_program +#[derive(Clone, Debug)] +pub struct SetMerkleRootCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> SetMerkleRootCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(SetMerkleRootCpiBuilderInstruction { + __program: program, + ncn_config: None, + ncn: None, + ballot_box: None, + vote_account: None, + tip_distribution_account: None, + tip_distribution_config: None, + tip_distribution_program: None, + restaking_program: None, + proof: None, + merkle_root: None, + max_total_claim: None, + max_num_nodes: None, + epoch: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn ncn_config( + &mut self, + ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn ballot_box( + &mut self, + ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn vote_account( + &mut self, + vote_account: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.vote_account = Some(vote_account); + self + } + #[inline(always)] + pub fn tip_distribution_account( + &mut self, + tip_distribution_account: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tip_distribution_account = Some(tip_distribution_account); + self + } + #[inline(always)] + pub fn tip_distribution_config( + &mut self, + tip_distribution_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tip_distribution_config = Some(tip_distribution_config); + self + } + #[inline(always)] + pub fn tip_distribution_program( + &mut self, + tip_distribution_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tip_distribution_program = Some(tip_distribution_program); + self + } + #[inline(always)] + pub fn restaking_program( + &mut self, + restaking_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.restaking_program = Some(restaking_program); + self + } + #[inline(always)] + pub fn proof(&mut self, proof: Vec<[u8; 32]>) -> &mut Self { + self.instruction.proof = Some(proof); + self + } + #[inline(always)] + pub fn merkle_root(&mut self, merkle_root: [u8; 32]) -> &mut Self { + self.instruction.merkle_root = Some(merkle_root); + self + } + #[inline(always)] + pub fn max_total_claim(&mut self, max_total_claim: u64) -> &mut Self { + self.instruction.max_total_claim = Some(max_total_claim); + self + } + #[inline(always)] + pub fn max_num_nodes(&mut self, max_num_nodes: u64) -> &mut Self { + self.instruction.max_num_nodes = Some(max_num_nodes); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.instruction.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = SetMerkleRootInstructionArgs { + proof: self.instruction.proof.clone().expect("proof is not set"), + merkle_root: self + .instruction + .merkle_root + .clone() + .expect("merkle_root is not set"), + max_total_claim: self + .instruction + .max_total_claim + .clone() + .expect("max_total_claim is not set"), + max_num_nodes: self + .instruction + .max_num_nodes + .clone() + .expect("max_num_nodes is not set"), + epoch: self.instruction.epoch.clone().expect("epoch is not set"), + }; + let instruction = SetMerkleRootCpi { + __program: self.instruction.__program, + + ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + ballot_box: self.instruction.ballot_box.expect("ballot_box is not set"), + + vote_account: self + .instruction + .vote_account + .expect("vote_account is not set"), + + tip_distribution_account: self + .instruction + .tip_distribution_account + .expect("tip_distribution_account is not set"), + + tip_distribution_config: self + .instruction + .tip_distribution_config + .expect("tip_distribution_config is not set"), + + tip_distribution_program: self + .instruction + .tip_distribution_program + .expect("tip_distribution_program is not set"), + + restaking_program: self + .instruction + .restaking_program + .expect("restaking_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct SetMerkleRootCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + ncn_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ballot_box: Option<&'b solana_program::account_info::AccountInfo<'a>>, + vote_account: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tip_distribution_account: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tip_distribution_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tip_distribution_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + restaking_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + proof: Option>, + merkle_root: Option<[u8; 32]>, + max_total_claim: Option, + max_num_nodes: Option, + epoch: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust/jito_tip_router/src/generated/instructions/set_tie_breaker.rs b/clients/rust/jito_tip_router/src/generated/instructions/set_tie_breaker.rs new file mode 100644 index 00000000..a863ef80 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/set_tie_breaker.rs @@ -0,0 +1,505 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct SetTieBreaker { + pub ncn_config: solana_program::pubkey::Pubkey, + + pub ballot_box: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub tie_breaker_admin: solana_program::pubkey::Pubkey, + + pub restaking_program: solana_program::pubkey::Pubkey, +} + +impl SetTieBreaker { + pub fn instruction( + &self, + args: SetTieBreakerInstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: SetTieBreakerInstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.ballot_box, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.tie_breaker_admin, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.restaking_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = SetTieBreakerInstructionData::new().try_to_vec().unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct SetTieBreakerInstructionData { + discriminator: u8, +} + +impl SetTieBreakerInstructionData { + pub fn new() -> Self { + Self { discriminator: 13 } + } +} + +impl Default for SetTieBreakerInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SetTieBreakerInstructionArgs { + pub meta_merkle_root: [u8; 32], + pub epoch: u64, +} + +/// Instruction builder for `SetTieBreaker`. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` ballot_box +/// 2. `[]` ncn +/// 3. `[signer]` tie_breaker_admin +/// 4. `[]` restaking_program +#[derive(Clone, Debug, Default)] +pub struct SetTieBreakerBuilder { + ncn_config: Option, + ballot_box: Option, + ncn: Option, + tie_breaker_admin: Option, + restaking_program: Option, + meta_merkle_root: Option<[u8; 32]>, + epoch: Option, + __remaining_accounts: Vec, +} + +impl SetTieBreakerBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ballot_box(&mut self, ballot_box: solana_program::pubkey::Pubkey) -> &mut Self { + self.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn tie_breaker_admin( + &mut self, + tie_breaker_admin: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.tie_breaker_admin = Some(tie_breaker_admin); + self + } + #[inline(always)] + pub fn restaking_program( + &mut self, + restaking_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.restaking_program = Some(restaking_program); + self + } + #[inline(always)] + pub fn meta_merkle_root(&mut self, meta_merkle_root: [u8; 32]) -> &mut Self { + self.meta_merkle_root = Some(meta_merkle_root); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = SetTieBreaker { + ncn_config: self.ncn_config.expect("ncn_config is not set"), + ballot_box: self.ballot_box.expect("ballot_box is not set"), + ncn: self.ncn.expect("ncn is not set"), + tie_breaker_admin: self + .tie_breaker_admin + .expect("tie_breaker_admin is not set"), + restaking_program: self + .restaking_program + .expect("restaking_program is not set"), + }; + let args = SetTieBreakerInstructionArgs { + meta_merkle_root: self + .meta_merkle_root + .clone() + .expect("meta_merkle_root is not set"), + epoch: self.epoch.clone().expect("epoch is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `set_tie_breaker` CPI accounts. +pub struct SetTieBreakerCpiAccounts<'a, 'b> { + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub tie_breaker_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `set_tie_breaker` CPI instruction. +pub struct SetTieBreakerCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub tie_breaker_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: SetTieBreakerInstructionArgs, +} + +impl<'a, 'b> SetTieBreakerCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: SetTieBreakerCpiAccounts<'a, 'b>, + args: SetTieBreakerInstructionArgs, + ) -> Self { + Self { + __program: program, + ncn_config: accounts.ncn_config, + ballot_box: accounts.ballot_box, + ncn: accounts.ncn, + tie_breaker_admin: accounts.tie_breaker_admin, + restaking_program: accounts.restaking_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.ballot_box.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.tie_breaker_admin.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.restaking_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = SetTieBreakerInstructionData::new().try_to_vec().unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(5 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.ncn_config.clone()); + account_infos.push(self.ballot_box.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.tie_breaker_admin.clone()); + account_infos.push(self.restaking_program.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `SetTieBreaker` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` ballot_box +/// 2. `[]` ncn +/// 3. `[signer]` tie_breaker_admin +/// 4. `[]` restaking_program +#[derive(Clone, Debug)] +pub struct SetTieBreakerCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> SetTieBreakerCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(SetTieBreakerCpiBuilderInstruction { + __program: program, + ncn_config: None, + ballot_box: None, + ncn: None, + tie_breaker_admin: None, + restaking_program: None, + meta_merkle_root: None, + epoch: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn ncn_config( + &mut self, + ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_config = Some(ncn_config); + self + } + #[inline(always)] + pub fn ballot_box( + &mut self, + ballot_box: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ballot_box = Some(ballot_box); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn tie_breaker_admin( + &mut self, + tie_breaker_admin: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tie_breaker_admin = Some(tie_breaker_admin); + self + } + #[inline(always)] + pub fn restaking_program( + &mut self, + restaking_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.restaking_program = Some(restaking_program); + self + } + #[inline(always)] + pub fn meta_merkle_root(&mut self, meta_merkle_root: [u8; 32]) -> &mut Self { + self.instruction.meta_merkle_root = Some(meta_merkle_root); + self + } + #[inline(always)] + pub fn epoch(&mut self, epoch: u64) -> &mut Self { + self.instruction.epoch = Some(epoch); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = SetTieBreakerInstructionArgs { + meta_merkle_root: self + .instruction + .meta_merkle_root + .clone() + .expect("meta_merkle_root is not set"), + epoch: self.instruction.epoch.clone().expect("epoch is not set"), + }; + let instruction = SetTieBreakerCpi { + __program: self.instruction.__program, + + ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + + ballot_box: self.instruction.ballot_box.expect("ballot_box is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + tie_breaker_admin: self + .instruction + .tie_breaker_admin + .expect("tie_breaker_admin is not set"), + + restaking_program: self + .instruction + .restaking_program + .expect("restaking_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct SetTieBreakerCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + ncn_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ballot_box: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tie_breaker_admin: Option<&'b solana_program::account_info::AccountInfo<'a>>, + restaking_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + meta_merkle_root: Option<[u8; 32]>, + epoch: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 1aa25c24..b55f125d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,6 +18,7 @@ jito-restaking-core = { workspace = true } jito-restaking-sdk = { workspace = true } jito-vault-core = { workspace = true } jito-vault-sdk = { workspace = true } +meta-merkle-tree = { workspace = true } shank = { workspace = true } solana-program = { workspace = true } spl-associated-token-account = { workspace = true } diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs index 0895acc4..aa67afe0 100644 --- a/core/src/ballot_box.rs +++ b/core/src/ballot_box.rs @@ -3,11 +3,18 @@ use jito_bytemuck::{ types::{PodU128, PodU16, PodU64}, AccountDeserialize, Discriminator, }; +use meta_merkle_tree::{meta_merkle_tree::LEAF_PREFIX, tree_node::TreeNode}; use shank::{ShankAccount, ShankType}; -use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{ + account_info::AccountInfo, hash::hashv, msg, program_error::ProgramError, pubkey::Pubkey, +}; use spl_math::precise_number::PreciseNumber; -use crate::{constants::PRECISE_CONSENSUS, discriminators::Discriminators, error::TipRouterError}; +use crate::{ + constants::{precise_consensus, DEFAULT_CONSENSUS_REACHED_SLOT}, + discriminators::Discriminators, + error::TipRouterError, +}; #[derive(Debug, Clone, PartialEq, Eq, Copy, Zeroable, ShankType, Pod, ShankType)] #[repr(C)] @@ -25,6 +32,12 @@ impl Default for Ballot { } } +impl std::fmt::Display for Ballot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.merkle_root) + } +} + impl Ballot { pub const fn new(root: [u8; 32]) -> Self { Self { @@ -169,7 +182,7 @@ impl OperatorVote { pub struct BallotBox { ncn: Pubkey, - ncn_epoch: PodU64, + epoch: PodU64, bump: u8, @@ -193,13 +206,13 @@ impl Discriminator for BallotBox { } impl BallotBox { - pub fn new(ncn: Pubkey, ncn_epoch: u64, bump: u8, current_slot: u64) -> Self { + pub fn new(ncn: Pubkey, epoch: u64, bump: u8, current_slot: u64) -> Self { Self { ncn, - ncn_epoch: PodU64::from(ncn_epoch), + epoch: PodU64::from(epoch), bump, slot_created: PodU64::from(current_slot), - slot_consensus_reached: PodU64::from(0), + slot_consensus_reached: PodU64::from(DEFAULT_CONSENSUS_REACHED_SLOT), operators_voted: PodU64::from(0), unique_ballots: PodU64::from(0), winning_ballot: Ballot::default(), @@ -210,12 +223,16 @@ impl BallotBox { } } - pub fn seeds(ncn: &Pubkey, ncn_epoch: u64) -> Vec> { + pub fn initialize(&mut self, ncn: Pubkey, epoch: u64, bump: u8, current_slot: u64) { + *self = Self::new(ncn, epoch, bump, current_slot); + } + + pub fn seeds(ncn: &Pubkey, epoch: u64) -> Vec> { Vec::from_iter( [ b"ballot_box".to_vec(), ncn.to_bytes().to_vec(), - ncn_epoch.to_le_bytes().to_vec(), + epoch.to_le_bytes().to_vec(), ] .iter() .cloned(), @@ -225,9 +242,9 @@ impl BallotBox { pub fn find_program_address( program_id: &Pubkey, ncn: &Pubkey, - ncn_epoch: u64, + epoch: u64, ) -> (Pubkey, u8, Vec>) { - let seeds = Self::seeds(ncn, ncn_epoch); + let seeds = Self::seeds(ncn, epoch); let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect(); let (pda, bump) = Pubkey::find_program_address(&seeds_iter, program_id); (pda, bump, seeds) @@ -236,29 +253,29 @@ impl BallotBox { pub fn load( program_id: &Pubkey, ncn: &Pubkey, - ncn_epoch: u64, - epoch_snapshot: &AccountInfo, + epoch: u64, + ballot_box_account: &AccountInfo, expect_writable: bool, ) -> Result<(), ProgramError> { - if epoch_snapshot.owner.ne(program_id) { + if ballot_box_account.owner.ne(program_id) { msg!("Ballot box account has an invalid owner"); return Err(ProgramError::InvalidAccountOwner); } - if epoch_snapshot.data_is_empty() { + if ballot_box_account.data_is_empty() { msg!("Ballot box account data is empty"); return Err(ProgramError::InvalidAccountData); } - if expect_writable && !epoch_snapshot.is_writable { + if expect_writable && !ballot_box_account.is_writable { msg!("Ballot box account is not writable"); return Err(ProgramError::InvalidAccountData); } - if epoch_snapshot.data.borrow()[0].ne(&Self::DISCRIMINATOR) { + if ballot_box_account.data.borrow()[0].ne(&Self::DISCRIMINATOR) { msg!("Ballot box account discriminator is invalid"); return Err(ProgramError::InvalidAccountData); } - if epoch_snapshot + if ballot_box_account .key - .ne(&Self::find_program_address(program_id, ncn, ncn_epoch).0) + .ne(&Self::find_program_address(program_id, ncn, epoch).0) { msg!("Ballot box account is not at the correct PDA"); return Err(ProgramError::InvalidAccountData); @@ -266,6 +283,10 @@ impl BallotBox { Ok(()) } + pub fn epoch(&self) -> u64 { + self.epoch.into() + } + pub fn slot_consensus_reached(&self) -> u64 { self.slot_consensus_reached.into() } @@ -279,7 +300,13 @@ impl BallotBox { } pub fn is_consensus_reached(&self) -> bool { - self.slot_consensus_reached() > 0 + self.slot_consensus_reached() != DEFAULT_CONSENSUS_REACHED_SLOT + || self.winning_ballot.is_valid() + } + + pub fn tie_breaker_set(&self) -> bool { + self.slot_consensus_reached() == DEFAULT_CONSENSUS_REACHED_SLOT + && self.winning_ballot.is_valid() } pub fn get_winning_ballot(&self) -> Result { @@ -290,6 +317,10 @@ impl BallotBox { } } + pub fn set_winning_ballot(&mut self, ballot: Ballot) { + self.winning_ballot = ballot; + } + fn increment_or_create_ballot_tally( &mut self, ballot: &Ballot, @@ -328,12 +359,26 @@ impl BallotBox { ballot: Ballot, stake_weight: u128, current_slot: u64, + valid_slots_after_consensus: u64, ) -> Result<(), TipRouterError> { + if !self.is_voting_valid(current_slot, valid_slots_after_consensus)? { + return Err(TipRouterError::VotingNotValid); + } + let ballot_index = self.increment_or_create_ballot_tally(&ballot, stake_weight)?; + let consensus_reached = self.is_consensus_reached(); + for vote in self.operator_votes.iter_mut() { if vote.operator().eq(&operator) { - return Err(TipRouterError::DuplicateVoteCast); + if consensus_reached { + return Err(TipRouterError::ConsensusAlreadyReached); + } + + let operator_vote = + OperatorVote::new(ballot_index, operator, current_slot, stake_weight); + *vote = operator_vote; + return Ok(()); } if vote.is_empty() { @@ -360,7 +405,7 @@ impl BallotBox { total_stake_weight: u128, current_slot: u64, ) -> Result<(), TipRouterError> { - if self.slot_consensus_reached() != 0 { + if self.slot_consensus_reached() != DEFAULT_CONSENSUS_REACHED_SLOT { return Ok(()); } @@ -380,18 +425,403 @@ impl BallotBox { .checked_div(&precise_total_stake_weight) .ok_or(TipRouterError::DenominatorIsZero)?; - let target_precise_percentage = - PreciseNumber::new(PRECISE_CONSENSUS).ok_or(TipRouterError::NewPreciseNumberError)?; + let target_precise_percentage = precise_consensus()?; let consensus_reached = ballot_percentage_of_total.greater_than_or_equal(&target_precise_percentage); - if consensus_reached { + if consensus_reached && !self.winning_ballot.is_valid() { self.slot_consensus_reached = PodU64::from(current_slot); - self.winning_ballot = max_tally.ballot(); + self.set_winning_ballot(max_tally.ballot()); + } + + Ok(()) + } + + pub fn set_tie_breaker_ballot( + &mut self, + meta_merkle_root: [u8; 32], + current_epoch: u64, + epochs_before_stall: u64, + ) -> Result<(), TipRouterError> { + // Check that consensus has not been reached + if self.is_consensus_reached() { + msg!("Consensus already reached"); + return Err(TipRouterError::ConsensusAlreadyReached); + } + + // Check if voting is stalled and setting the tie breaker is eligible + if current_epoch + < self + .epoch() + .checked_add(epochs_before_stall) + .ok_or(TipRouterError::ArithmeticOverflow)? + { + return Err(TipRouterError::VotingNotFinalized); + } + + let finalized_ballot = Ballot::new(meta_merkle_root); + + // Check that the merkle root is one of the existing options + if !self.has_ballot(&finalized_ballot) { + return Err(TipRouterError::TieBreakerNotInPriorVotes); + } + + self.set_winning_ballot(finalized_ballot); + Ok(()) + } + + pub fn has_ballot(&self, ballot: &Ballot) -> bool { + self.ballot_tallies.iter().any(|t| t.ballot.eq(ballot)) + } + + /// Determines if an operator can still cast their vote. + /// Returns true when: + /// Consensus is not reached OR the voting window is still valid, assuming set_tie_breaker was not invoked + pub fn is_voting_valid( + &self, + current_slot: u64, + valid_slots_after_consensus: u64, + ) -> Result { + if self.tie_breaker_set() { + return Ok(false); + } + + if self.is_consensus_reached() { + let vote_window_valid = current_slot + <= self + .slot_consensus_reached() + .checked_add(valid_slots_after_consensus) + .ok_or(TipRouterError::ArithmeticOverflow)?; + + return Ok(vote_window_valid); + } + + Ok(true) + } + + pub fn verify_merkle_root( + &self, + tip_distribution_account: Pubkey, + proof: Vec<[u8; 32]>, + merkle_root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + ) -> Result<(), TipRouterError> { + let tree_node = TreeNode::new( + tip_distribution_account, + merkle_root, + max_total_claim, + max_num_nodes, + ); + + let node_hash = hashv(&[LEAF_PREFIX, &tree_node.hash().to_bytes()]); + + if !meta_merkle_tree::verify::verify( + proof, + self.winning_ballot.root(), + node_hash.to_bytes(), + ) { + return Err(TipRouterError::InvalidMerkleProof); } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] // TODO? + fn test_verify_merkle_root() { + // Create merkle tree of merkle trees + + // Intialize ballot box + let ballot_box = BallotBox::new(Pubkey::default(), 0, 0, 0); + + // Set winning merkle root, don't care about anything else + ballot_box + .verify_merkle_root(Pubkey::default(), vec![], [0u8; 32], 0, 0) + .unwrap(); + } + + #[test] + fn test_cast_vote() { + let ncn = Pubkey::new_unique(); + let operator = Pubkey::new_unique(); + let current_slot = 100; + let epoch = 1; + let stake_weight: u128 = 1000; + let valid_slots_after_consensus = 10; + let mut ballot_box = BallotBox::new(ncn, epoch, 0, current_slot); + let ballot = Ballot::new([1; 32]); + + // Test initial cast vote + ballot_box + .cast_vote( + operator, + ballot, + stake_weight, + current_slot, + valid_slots_after_consensus, + ) + .unwrap(); + + // Verify vote was recorded correctly + let operator_vote = ballot_box + .operator_votes + .iter() + .find(|v| v.operator() == operator) + .unwrap(); + assert_eq!(operator_vote.stake_weight(), stake_weight); + assert_eq!(operator_vote.slot_voted(), current_slot); + + // Verify ballot tally + let tally = ballot_box + .ballot_tallies + .iter() + .find(|t| t.ballot() == ballot) + .unwrap(); + assert_eq!(tally.stake_weight(), stake_weight); + + // Test re-vote with different ballot + let new_ballot = Ballot::new([2u8; 32]); + let new_slot = current_slot + 1; + ballot_box + .cast_vote( + operator, + new_ballot, + stake_weight, + new_slot, + valid_slots_after_consensus, + ) + .unwrap(); + + // Verify new ballot tally increased + let new_tally = ballot_box + .ballot_tallies + .iter() + .find(|t| t.ballot() == new_ballot) + .unwrap(); + assert_eq!(new_tally.stake_weight(), stake_weight); + + // Test error on changing vote after consensus + ballot_box.set_winning_ballot(new_ballot); + ballot_box.slot_consensus_reached = PodU64::from(new_slot); + let result = ballot_box.cast_vote( + operator, + ballot, + stake_weight, + new_slot + 1, + valid_slots_after_consensus, + ); + assert!(matches!( + result, + Err(TipRouterError::ConsensusAlreadyReached) + )); + + // Test voting window expired after consensus + let result = ballot_box.cast_vote( + operator, + ballot, + stake_weight, + new_slot + valid_slots_after_consensus + 1, + valid_slots_after_consensus, + ); + assert!(matches!(result, Err(TipRouterError::VotingNotValid))); + } + + #[test] + fn test_increment_or_create_ballot_tally() { + let mut ballot_box = BallotBox::new(Pubkey::new_unique(), 1, 1, 1); + let ballot = Ballot::new([1u8; 32]); + let stake_weight = 100; + + // Test creating new ballot tally + let tally_index = ballot_box + .increment_or_create_ballot_tally(&ballot, stake_weight) + .unwrap(); + assert_eq!(tally_index, 0); + assert_eq!(ballot_box.unique_ballots(), 1); + assert_eq!(ballot_box.ballot_tallies[0].stake_weight(), stake_weight); + assert_eq!(ballot_box.ballot_tallies[0].ballot(), ballot); + + // Test incrementing existing ballot tally + let tally_index = ballot_box + .increment_or_create_ballot_tally(&ballot, stake_weight) + .unwrap(); + assert_eq!(tally_index, 0); + assert_eq!(ballot_box.unique_ballots(), 1); + assert_eq!( + ballot_box.ballot_tallies[0].stake_weight(), + stake_weight * 2 + ); + assert_eq!(ballot_box.ballot_tallies[0].ballot(), ballot); + + // Test creating second ballot tally + let ballot2 = Ballot::new([2u8; 32]); + let tally_index = ballot_box + .increment_or_create_ballot_tally(&ballot2, stake_weight) + .unwrap(); + assert_eq!(tally_index, 1); + assert_eq!(ballot_box.unique_ballots(), 2); + assert_eq!(ballot_box.ballot_tallies[1].stake_weight(), stake_weight); + assert_eq!(ballot_box.ballot_tallies[1].ballot(), ballot2); + + // Test error when ballot tallies are full + for i in 3..=32 { + let ballot = Ballot::new([i as u8; 32]); + ballot_box + .increment_or_create_ballot_tally(&ballot, stake_weight) + .unwrap(); + } + let ballot_full = Ballot::new([33u8; 32]); + let result = ballot_box.increment_or_create_ballot_tally(&ballot_full, stake_weight); + assert!(matches!(result, Err(TipRouterError::BallotTallyFull))); + } + + #[test] + fn test_tally_votes() { + let ncn = Pubkey::new_unique(); + let current_slot = 100; + let epoch = 1; + let stake_weight: u128 = 1000; + let total_stake_weight: u128 = 1000; + let mut ballot_box = BallotBox::new(ncn, epoch, 0, current_slot); + let ballot = Ballot::new([1; 32]); + + // Test no consensus when below threshold + ballot_box + .increment_or_create_ballot_tally(&ballot, stake_weight / 2) + .unwrap(); + ballot_box + .tally_votes(total_stake_weight, current_slot) + .unwrap(); + assert!(!ballot_box.is_consensus_reached()); + assert_eq!( + ballot_box.slot_consensus_reached(), + DEFAULT_CONSENSUS_REACHED_SLOT + ); + assert!(matches!( + ballot_box.get_winning_ballot(), + Err(TipRouterError::ConsensusNotReached) + )); + + // Test consensus reached when above threshold + ballot_box + .increment_or_create_ballot_tally(&ballot, stake_weight / 2) + .unwrap(); + ballot_box + .tally_votes(total_stake_weight, current_slot) + .unwrap(); + assert!(ballot_box.is_consensus_reached()); + assert_eq!(ballot_box.slot_consensus_reached(), current_slot); + assert_eq!(ballot_box.get_winning_ballot().unwrap(), ballot); + + // Consensus remains after additional votes + let ballot2 = Ballot::new([2; 32]); + ballot_box + .increment_or_create_ballot_tally(&ballot2, stake_weight) + .unwrap(); + ballot_box + .tally_votes(total_stake_weight, current_slot + 1) + .unwrap(); + assert!(ballot_box.is_consensus_reached()); + assert_eq!(ballot_box.slot_consensus_reached(), current_slot); + assert_eq!(ballot_box.get_winning_ballot().unwrap(), ballot); + + // Test with multiple competing ballots + let mut ballot_box = BallotBox::new(ncn, epoch, 0, current_slot); + let ballot1 = Ballot::new([1; 32]); + let ballot2 = Ballot::new([2; 32]); + let ballot3 = Ballot::new([3; 32]); + + ballot_box + .increment_or_create_ballot_tally(&ballot1, stake_weight / 4) + .unwrap(); + ballot_box + .increment_or_create_ballot_tally(&ballot2, stake_weight / 4) + .unwrap(); + ballot_box + .increment_or_create_ballot_tally(&ballot3, stake_weight / 2) + .unwrap(); + + ballot_box + .tally_votes(total_stake_weight, current_slot) + .unwrap(); + assert!(!ballot_box.is_consensus_reached()); + + // Add more votes to reach consensus + ballot_box + .increment_or_create_ballot_tally(&ballot3, stake_weight / 2) + .unwrap(); + ballot_box + .tally_votes(total_stake_weight, current_slot) + .unwrap(); + assert!(ballot_box.is_consensus_reached()); + assert_eq!(ballot_box.get_winning_ballot().unwrap(), ballot3); + } + + #[test] + fn test_set_tie_breaker_ballot() { + let ncn = Pubkey::new_unique(); + let epoch = 0; + let current_slot = 1000; + let mut ballot_box = BallotBox::new(ncn, epoch, 0, current_slot); + + // Create some initial ballots + let ballot1 = Ballot::new([1; 32]); + let ballot2 = Ballot::new([2; 32]); + let stake_weight = 100; + + ballot_box + .increment_or_create_ballot_tally(&ballot1, stake_weight) + .unwrap(); + ballot_box + .increment_or_create_ballot_tally(&ballot2, stake_weight) + .unwrap(); + + // Test setting tie breaker before voting is stalled + let current_epoch = epoch + 1; + let epochs_before_stall = 3; + assert_eq!( + ballot_box.set_tie_breaker_ballot(ballot1.root(), current_epoch, epochs_before_stall), + Err(TipRouterError::VotingNotFinalized) + ); + + // Test setting tie breaker after voting is stalled (current_epoch >= epoch + epochs_before_stall) + let current_epoch = epoch + epochs_before_stall; + ballot_box + .set_tie_breaker_ballot(ballot1.root(), current_epoch, epochs_before_stall) + .unwrap(); + assert!(ballot_box.is_consensus_reached()); + assert_eq!(ballot_box.get_winning_ballot().unwrap(), ballot1); + + // Test setting tie breaker with invalid merkle root + let mut ballot_box = BallotBox::new(ncn, epoch, 0, current_slot); + ballot_box + .increment_or_create_ballot_tally(&ballot1, stake_weight) + .unwrap(); + assert_eq!( + ballot_box.set_tie_breaker_ballot([99; 32], current_epoch, epochs_before_stall), + Err(TipRouterError::TieBreakerNotInPriorVotes) + ); + + // Test setting tie breaker when consensus already reached + let mut ballot_box = BallotBox::new(ncn, epoch, 0, current_slot); + ballot_box + .increment_or_create_ballot_tally(&ballot1, stake_weight * 2) + .unwrap(); + ballot_box + .tally_votes(stake_weight * 2, current_slot) + .unwrap(); + assert!(ballot_box.is_consensus_reached()); + assert_eq!( + ballot_box.set_tie_breaker_ballot(ballot1.root(), current_epoch, epochs_before_stall), + Err(TipRouterError::ConsensusAlreadyReached) + ); + } +} diff --git a/core/src/constants.rs b/core/src/constants.rs index df8fbe01..5c597995 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -1,4 +1,20 @@ +use spl_math::precise_number::PreciseNumber; + +use crate::error::TipRouterError; + pub const MAX_FEE_BPS: u64 = 10_000; pub const MAX_OPERATORS: usize = 256; pub const MAX_VAULT_OPERATOR_DELEGATIONS: usize = 64; -pub const PRECISE_CONSENSUS: u128 = 666_666_666_666; +const PRECISE_CONSENSUS_NUMERATOR: u128 = 2; +const PRECISE_CONSENSUS_DENOMINATOR: u128 = 3; +pub fn precise_consensus() -> Result { + PreciseNumber::new(PRECISE_CONSENSUS_NUMERATOR) + .ok_or(TipRouterError::NewPreciseNumberError)? + .checked_div( + &PreciseNumber::new(PRECISE_CONSENSUS_DENOMINATOR) + .ok_or(TipRouterError::NewPreciseNumberError)?, + ) + .ok_or(TipRouterError::DenominatorIsZero) +} + +pub const DEFAULT_CONSENSUS_REACHED_SLOT: u64 = u64::MAX; diff --git a/core/src/epoch_snapshot.rs b/core/src/epoch_snapshot.rs index 64278019..6dc29527 100644 --- a/core/src/epoch_snapshot.rs +++ b/core/src/epoch_snapshot.rs @@ -106,7 +106,6 @@ impl EpochSnapshot { expect_writable: bool, ) -> Result<(), ProgramError> { if epoch_snapshot.owner.ne(program_id) { - msg!("Epoch Snapshot account has an invalid owner"); return Err(ProgramError::InvalidAccountOwner); } if epoch_snapshot.data_is_empty() { @@ -125,7 +124,6 @@ impl EpochSnapshot { .key .ne(&Self::find_program_address(program_id, ncn, ncn_epoch).0) { - msg!("Epoch Snapshot account is not at the correct PDA"); return Err(ProgramError::InvalidAccountData); } Ok(()) diff --git a/core/src/error.rs b/core/src/error.rs index 4c5f7da6..bf791b57 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -70,10 +70,25 @@ pub enum TipRouterError { OperatorVotesFull, #[error("Merkle root tally full")] BallotTallyFull, - #[error("Consensus already reached")] + #[error("Consensus already reached, cannot change vote")] ConsensusAlreadyReached, #[error("Consensus not reached")] ConsensusNotReached, + + #[error("Epoch snapshot not finalized")] + EpochSnapshotNotFinalized, + #[error("Voting not valid, too many slots after consensus reached")] + VotingNotValid, + #[error("Tie breaker admin invalid")] + TieBreakerAdminInvalid, + #[error("Voting not finalized")] + VotingNotFinalized, + #[error("Tie breaking ballot must be one of the prior votes")] + TieBreakerNotInPriorVotes, + #[error("Invalid merkle proof")] + InvalidMerkleProof, + #[error("Operator admin needs to sign its vote")] + OperatorAdminInvalid, } impl DecodeError for TipRouterError { diff --git a/core/src/instruction.rs b/core/src/instruction.rs index bc42ba61..1143ff8b 100644 --- a/core/src/instruction.rs +++ b/core/src/instruction.rs @@ -139,4 +139,56 @@ pub enum TipRouterInstruction { #[account(3, writable, signer, name = "payer")] #[account(4, name = "system_program")] InitializeTrackedMints, + + /// Initializes the ballot box for an NCN + #[account(0, name = "ncn_config")] + #[account(1, writable, name = "ballot_box")] + #[account(2, name = "ncn")] + #[account(3, writable, signer, name = "payer")] + #[account(4, name = "system_program")] + InitializeBallotBox { + epoch: u64, + }, + + /// Cast a vote for a merkle root + #[account(0, name = "ncn_config")] + #[account(1, writable, name = "ballot_box")] + #[account(2, name = "ncn")] + #[account(3, name = "epoch_snapshot")] + #[account(4, name = "operator_snapshot")] + #[account(5, name = "operator")] + #[account(6, signer, name = "operator_admin")] + #[account(7, name = "restaking_program")] + CastVote { + meta_merkle_root: [u8; 32], + epoch: u64, + }, + + /// Set the merkle root after consensus is reached + #[account(0, writable, name = "ncn_config")] + #[account(1, name = "ncn")] + #[account(2, name = "ballot_box")] + #[account(3, name = "vote_account")] + #[account(4, writable, name = "tip_distribution_account")] + #[account(5, name = "tip_distribution_config")] + #[account(6, name = "tip_distribution_program")] + #[account(7, name = "restaking_program")] + SetMerkleRoot { + proof: Vec<[u8; 32]>, + merkle_root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + epoch: u64, + }, + + /// Set tie breaker in case of stalled voting + #[account(0, name = "ncn_config")] + #[account(1, writable, name = "ballot_box")] + #[account(2, name = "ncn")] + #[account(3, signer, name = "tie_breaker_admin")] + #[account(4, name = "restaking_program")] + SetTieBreaker { + meta_merkle_root: [u8; 32], + epoch: u64, + } } diff --git a/core/src/ncn_config.rs b/core/src/ncn_config.rs index 2ca48083..35cc8697 100644 --- a/core/src/ncn_config.rs +++ b/core/src/ncn_config.rs @@ -1,5 +1,5 @@ use bytemuck::{Pod, Zeroable}; -use jito_bytemuck::{AccountDeserialize, Discriminator}; +use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator}; use shank::{ShankAccount, ShankType}; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; @@ -15,6 +15,12 @@ pub struct NcnConfig { pub fee_admin: Pubkey, + /// Number of slots after consensus reached where voting is still valid + pub valid_slots_after_consensus: PodU64, + + /// Number of epochs before voting is considered stalled + pub epochs_before_stall: PodU64, + pub fees: Fees, /// Bump seed for the PDA @@ -28,16 +34,13 @@ impl Discriminator for NcnConfig { } impl NcnConfig { - pub const fn new( - ncn: Pubkey, - tie_breaker_admin: Pubkey, - fee_admin: Pubkey, - fees: Fees, - ) -> Self { + pub fn new(ncn: Pubkey, tie_breaker_admin: Pubkey, fee_admin: Pubkey, fees: Fees) -> Self { Self { ncn, tie_breaker_admin, fee_admin, + valid_slots_after_consensus: PodU64::from(0), // TODO set this + epochs_before_stall: PodU64::from(0), // TODO set this fees, bump: 0, reserved: [0; 127], @@ -98,4 +101,12 @@ impl NcnConfig { } Ok(()) } + + pub fn valid_slots_after_consensus(&self) -> u64 { + self.valid_slots_after_consensus.into() + } + + pub fn epochs_before_stall(&self) -> u64 { + self.epochs_before_stall.into() + } } diff --git a/format.sh b/format.sh index 6aa27271..61093a29 100755 --- a/format.sh +++ b/format.sh @@ -1,12 +1,20 @@ #! /bin/zsh - -# cargo b && ./target/debug/jito-restaking-cli --markdown-help > ./docs/_tools/00_cli.md && ./target/debug/jito-shank-cli && yarn generate-clients && cargo b +echo "Executing: cargo sort --workspace" cargo sort --workspace + +echo "Executing: cargo fmt --all" cargo fmt --all -cargo nextest run --all-features -cargo clippy --all-features -- -D warnings -D clippy::all -D clippy::nursery -D clippy::integer_division -D clippy::arithmetic_side_effects -D clippy::style -D clippy::perf +echo "Executing: cargo nextest run --all-features -E 'not test(bpf)'" +cargo nextest run --all-features -E 'not test(bpf)' +echo "Executing: cargo clippy --all-features -- -D warnings -D clippy::all -D clippy::nursery -D clippy::integer_division -D clippy::arithmetic_side_effects -D clippy::style -D clippy::perf" +cargo clippy --all-features -- -D warnings -D clippy::all -D clippy::nursery -D clippy::integer_division -D clippy::arithmetic_side_effects -D clippy::style -D clippy::perf + +echo "Executing: cargo b && ./target/debug/jito-tip-router-shank-cli && yarn install && yarn generate-clients && cargo b" cargo b && ./target/debug/jito-tip-router-shank-cli && yarn install && yarn generate-clients && cargo b + +echo "Executing: cargo-build-sbf" cargo-build-sbf + diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index dbac2f45..6f136e00 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -563,6 +563,242 @@ "type": "u8", "value": 9 } + }, + { + "name": "InitializeBallotBox", + "accounts": [ + { + "name": "ncnConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "ballotBox", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "epoch", + "type": "u64" + } + ], + "discriminant": { + "type": "u8", + "value": 10 + } + }, + { + "name": "CastVote", + "accounts": [ + { + "name": "ncnConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "ballotBox", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "epochSnapshot", + "isMut": false, + "isSigner": false + }, + { + "name": "operatorSnapshot", + "isMut": false, + "isSigner": false + }, + { + "name": "operator", + "isMut": false, + "isSigner": false + }, + { + "name": "operatorAdmin", + "isMut": false, + "isSigner": true + }, + { + "name": "restakingProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "metaMerkleRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "epoch", + "type": "u64" + } + ], + "discriminant": { + "type": "u8", + "value": 11 + } + }, + { + "name": "SetMerkleRoot", + "accounts": [ + { + "name": "ncnConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "ballotBox", + "isMut": false, + "isSigner": false + }, + { + "name": "voteAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tipDistributionAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tipDistributionConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "tipDistributionProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "restakingProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "proof", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + }, + { + "name": "merkleRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "maxTotalClaim", + "type": "u64" + }, + { + "name": "maxNumNodes", + "type": "u64" + }, + { + "name": "epoch", + "type": "u64" + } + ], + "discriminant": { + "type": "u8", + "value": 12 + } + }, + { + "name": "SetTieBreaker", + "accounts": [ + { + "name": "ncnConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "ballotBox", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "tieBreakerAdmin", + "isMut": false, + "isSigner": true + }, + { + "name": "restakingProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "metaMerkleRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "epoch", + "type": "u64" + } + ], + "discriminant": { + "type": "u8", + "value": 13 + } } ], "accounts": [ @@ -576,7 +812,7 @@ "type": "publicKey" }, { - "name": "ncnEpoch", + "name": "epoch", "type": { "defined": "PodU64" } @@ -851,6 +1087,18 @@ "name": "feeAdmin", "type": "publicKey" }, + { + "name": "validSlotsAfterConsensus", + "type": { + "defined": "PodU64" + } + }, + { + "name": "epochsBeforeStall", + "type": { + "defined": "PodU64" + } + }, { "name": "fees", "type": { @@ -1398,12 +1646,47 @@ { "code": 8732, "name": "ConsensusAlreadyReached", - "msg": "Consensus already reached" + "msg": "Consensus already reached, cannot change vote" }, { "code": 8733, "name": "ConsensusNotReached", "msg": "Consensus not reached" + }, + { + "code": 8734, + "name": "EpochSnapshotNotFinalized", + "msg": "Epoch snapshot not finalized" + }, + { + "code": 8735, + "name": "VotingNotValid", + "msg": "Voting not valid, too many slots after consensus reached" + }, + { + "code": 8736, + "name": "TieBreakerAdminInvalid", + "msg": "Tie breaker admin invalid" + }, + { + "code": 8737, + "name": "VotingNotFinalized", + "msg": "Voting not finalized" + }, + { + "code": 8738, + "name": "TieBreakerNotInPriorVotes", + "msg": "Tie breaking ballot must be one of the prior votes" + }, + { + "code": 8739, + "name": "InvalidMerkleProof", + "msg": "Invalid merkle proof" + }, + { + "code": 8740, + "name": "OperatorAdminInvalid", + "msg": "Operator admin needs to sign its vote" } ], "metadata": { diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 4417d23c..02be0e21 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -10,17 +10,21 @@ edition = { workspace = true } readme = { workspace = true } [dev-dependencies] +anchor-lang = { workspace = true } borsh = { workspace = true } +bytemuck = { workspace = true } jito-bytemuck = { workspace = true } jito-jsm-core = { workspace = true } jito-restaking-core = { workspace = true } jito-restaking-program = { workspace = true } jito-restaking-sdk = { workspace = true } +jito-tip-distribution-sdk = { workspace = true } jito-tip-router-core = { workspace = true } jito-tip-router-program = { workspace = true } jito-vault-core = { workspace = true } jito-vault-program = { workspace = true } jito-vault-sdk = { workspace = true } +meta-merkle-tree = { workspace = true } shank = { workspace = true } solana-program = { workspace = true } solana-program-test = { workspace = true } diff --git a/integration_tests/tests/fixtures/jito_tip_distribution.so b/integration_tests/tests/fixtures/jito_tip_distribution.so new file mode 100644 index 00000000..e37ff301 Binary files /dev/null and b/integration_tests/tests/fixtures/jito_tip_distribution.so differ diff --git a/integration_tests/tests/fixtures/jito_tip_router_program.so b/integration_tests/tests/fixtures/jito_tip_router_program.so new file mode 100755 index 00000000..c1c1d1cd Binary files /dev/null and b/integration_tests/tests/fixtures/jito_tip_router_program.so differ diff --git a/integration_tests/tests/fixtures/mod.rs b/integration_tests/tests/fixtures/mod.rs index cfc67174..7103bbdd 100644 --- a/integration_tests/tests/fixtures/mod.rs +++ b/integration_tests/tests/fixtures/mod.rs @@ -1,3 +1,4 @@ +use meta_merkle_tree::{error::MerkleTreeError, generated_merkle_tree::MerkleRootGeneratorError}; use solana_program::{instruction::InstructionError, program_error::ProgramError}; use solana_program_test::BanksClientError; use solana_sdk::transaction::TransactionError; @@ -5,6 +6,7 @@ use thiserror::Error; pub mod restaking_client; pub mod test_builder; +pub mod tip_distribution_client; pub mod tip_router_client; pub mod vault_client; @@ -16,6 +18,14 @@ pub enum TestError { BanksClientError(#[from] BanksClientError), #[error(transparent)] ProgramError(#[from] ProgramError), + #[error(transparent)] + MerkleTreeError(#[from] MerkleTreeError), + #[error(transparent)] + MerkleRootGeneratorError(#[from] MerkleRootGeneratorError), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + AnchorError(#[from] anchor_lang::error::Error), } impl TestError { @@ -27,6 +37,7 @@ impl TestError { _ => None, }, TestError::ProgramError(_) => None, + _ => None, } } } diff --git a/integration_tests/tests/fixtures/test_builder.rs b/integration_tests/tests/fixtures/test_builder.rs index e1d181e6..a2713052 100644 --- a/integration_tests/tests/fixtures/test_builder.rs +++ b/integration_tests/tests/fixtures/test_builder.rs @@ -1,14 +1,24 @@ -use std::fmt::{Debug, Formatter}; +use std::{ + borrow::BorrowMut, + fmt::{Debug, Formatter}, +}; use jito_restaking_core::{config::Config, ncn_vault_ticket::NcnVaultTicket}; +use jito_tip_distribution_sdk::jito_tip_distribution; use jito_vault_core::vault_ncn_ticket::VaultNcnTicket; use solana_program::{ clock::Clock, native_token::sol_to_lamports, pubkey::Pubkey, system_instruction::transfer, }; use solana_program_test::{processor, BanksClientError, ProgramTest, ProgramTestContext}; -use solana_sdk::{commitment_config::CommitmentLevel, signature::Signer, transaction::Transaction}; +use solana_sdk::{ + account::Account, commitment_config::CommitmentLevel, epoch_schedule::EpochSchedule, + signature::Signer, transaction::Transaction, +}; -use super::{restaking_client::NcnRoot, tip_router_client::TipRouterClient}; +use super::{ + restaking_client::NcnRoot, tip_distribution_client::TipDistributionClient, + tip_router_client::TipRouterClient, +}; use crate::fixtures::{ restaking_client::{OperatorRoot, RestakingProgramClient}, vault_client::{VaultProgramClient, VaultRoot}, @@ -46,22 +56,41 @@ impl Debug for TestBuilder { impl TestBuilder { pub async fn new() -> Self { - let mut program_test = ProgramTest::new( - "jito_tip_router_program", - jito_tip_router_program::id(), - processor!(jito_tip_router_program::process_instruction), - ); - program_test.add_program( - "jito_vault_program", - jito_vault_program::id(), - processor!(jito_vault_program::process_instruction), - ); - program_test.add_program( - "jito_restaking_program", - jito_restaking_program::id(), - processor!(jito_restaking_program::process_instruction), - ); - program_test.prefer_bpf(true); + let run_as_bpf = std::env::vars().any(|(key, _)| key.eq("SBF_OUT_DIR")); + + let program_test = if run_as_bpf { + let mut program_test = ProgramTest::new( + "jito_tip_router_program", + jito_tip_router_program::id(), + None, + ); + program_test.add_program("jito_vault_program", jito_vault_program::id(), None); + program_test.add_program("jito_restaking_program", jito_restaking_program::id(), None); + + // Tests that invoke this program should be in the "bpf" module so we can run them separately with the bpf vm. + // Anchor programs do not expose a compatible entrypoint for solana_program_test::processor! + program_test.add_program("jito_tip_distribution", jito_tip_distribution::ID, None); + + program_test + } else { + let mut program_test = ProgramTest::new( + "jito_tip_router_program", + jito_tip_router_program::id(), + processor!(jito_tip_router_program::process_instruction), + ); + program_test.add_program( + "jito_vault_program", + jito_vault_program::id(), + processor!(jito_vault_program::process_instruction), + ); + program_test.add_program( + "jito_restaking_program", + jito_restaking_program::id(), + processor!(jito_restaking_program::process_instruction), + ); + + program_test + }; Self { context: program_test.start_with_context().await, @@ -79,10 +108,20 @@ impl TestBuilder { Ok(()) } + pub async fn set_account(&mut self, address: Pubkey, account: Account) { + self.context + .borrow_mut() + .set_account(&address, &account.into()) + } + pub async fn clock(&mut self) -> Clock { self.context.banks_client.get_sysvar().await.unwrap() } + pub async fn epoch_schedule(&mut self) -> EpochSchedule { + self.context.banks_client.get_sysvar().await.unwrap() + } + pub fn tip_router_client(&self) -> TipRouterClient { TipRouterClient::new( self.context.banks_client.clone(), @@ -111,18 +150,22 @@ impl TestBuilder { ) } + pub fn tip_distribution_client(&self) -> TipDistributionClient { + TipDistributionClient::new( + self.context.banks_client.clone(), + self.context.payer.insecure_clone(), + ) + } + #[allow(dead_code)] pub async fn transfer(&mut self, to: &Pubkey, sol: f64) -> Result<(), BanksClientError> { let blockhash = self.context.banks_client.get_latest_blockhash().await?; + let lamports = sol_to_lamports(sol); self.context .banks_client .process_transaction_with_preflight_and_commitment( Transaction::new_signed_with_payer( - &[transfer( - &self.context.payer.pubkey(), - to, - sol_to_lamports(sol), - )], + &[transfer(&self.context.payer.pubkey(), to, lamports)], Some(&self.context.payer.pubkey()), &[&self.context.payer], blockhash, diff --git a/integration_tests/tests/fixtures/tip_distribution_client.rs b/integration_tests/tests/fixtures/tip_distribution_client.rs new file mode 100644 index 00000000..eae7c8a2 --- /dev/null +++ b/integration_tests/tests/fixtures/tip_distribution_client.rs @@ -0,0 +1,307 @@ +use anchor_lang::AccountDeserialize; +use jito_tip_distribution_sdk::{jito_tip_distribution, TipDistributionAccount}; +use solana_program::{pubkey::Pubkey, system_instruction::transfer}; +use solana_program_test::{BanksClient, ProgramTestBanksClientExt}; +use solana_sdk::{ + commitment_config::CommitmentLevel, + native_token::{sol_to_lamports, LAMPORTS_PER_SOL}, + signature::{Keypair, Signer}, + transaction::Transaction, + vote::{ + instruction::CreateVoteAccountConfig, + state::{VoteInit, VoteStateVersions}, + }, +}; + +use crate::fixtures::TestResult; + +pub struct TipDistributionClient { + banks_client: BanksClient, + payer: Keypair, +} + +impl TipDistributionClient { + pub const fn new(banks_client: BanksClient, payer: Keypair) -> Self { + Self { + banks_client, + payer, + } + } + + pub async fn process_transaction(&mut self, tx: &Transaction) -> TestResult<()> { + self.banks_client + .process_transaction_with_preflight_and_commitment( + tx.clone(), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn airdrop(&mut self, to: &Pubkey, sol: f64) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + let new_blockhash = self + .banks_client + .get_new_latest_blockhash(&blockhash) + .await + .unwrap(); + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &[transfer(&self.payer.pubkey(), to, sol_to_lamports(sol))], + Some(&self.payer.pubkey()), + &[&self.payer], + new_blockhash, + ), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn get_tip_distribution_account( + &mut self, + vote_account: Pubkey, + epoch: u64, + ) -> TestResult { + let (tip_distribution_address, _) = + jito_tip_distribution_sdk::derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &vote_account, + epoch, + ); + let tip_distribution_account = self + .banks_client + .get_account(tip_distribution_address) + .await? + .unwrap(); + let mut tip_distribution_data = tip_distribution_account.data.as_slice(); + let tip_distribution = TipDistributionAccount::try_deserialize(&mut tip_distribution_data)?; + + Ok(tip_distribution) + } + + // Sets up a vote account where the node_pubkey is the payer and the address is a new pubkey + pub async fn setup_vote_account(&mut self) -> TestResult { + let vote_keypair = Keypair::new(); + + let vote_init = VoteInit { + node_pubkey: self.payer.pubkey(), + authorized_voter: self.payer.pubkey(), + authorized_withdrawer: self.payer.pubkey(), + commission: 0, + }; + + let ixs = solana_program::vote::instruction::create_account_with_config( + &self.payer.pubkey(), + &vote_keypair.pubkey(), + &vote_init, + 1 * LAMPORTS_PER_SOL, + CreateVoteAccountConfig { + space: VoteStateVersions::vote_state_size_of(true) as u64, + with_seed: None, + }, + ); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &ixs, + Some(&self.payer.pubkey()), + &[&self.payer, &vote_keypair], + blockhash, + )) + .await?; + + Ok(vote_keypair) + } + + pub async fn do_initialize(&mut self, authority: Pubkey) -> TestResult<()> { + let (config, bump) = + jito_tip_distribution_sdk::derive_config_account_address(&jito_tip_distribution::ID); + let system_program = solana_program::system_program::id(); + let initializer = self.payer.pubkey(); + let expired_funds_account = authority; + let num_epochs_valid = 10; + let max_validator_commission_bps = 10000; + + self.initialize( + authority, + expired_funds_account, + num_epochs_valid, + max_validator_commission_bps, + config, + system_program, + initializer, + bump, + ) + .await + } + + pub async fn initialize( + &mut self, + authority: Pubkey, + expired_funds_account: Pubkey, + num_epochs_valid: u64, + max_validator_commission_bps: u16, + config: Pubkey, + system_program: Pubkey, + initializer: Pubkey, + bump: u8, + ) -> TestResult<()> { + let ix = jito_tip_distribution_sdk::instruction::initialize_ix( + config, + system_program, + initializer, + authority, + expired_funds_account, + num_epochs_valid, + max_validator_commission_bps, + bump, + ); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + pub async fn do_initialize_tip_distribution_account( + &mut self, + merkle_root_upload_authority: Pubkey, + vote_keypair: Keypair, + epoch: u64, + validator_commission_bps: u16, + ) -> TestResult<()> { + let (config, _) = + jito_tip_distribution_sdk::derive_config_account_address(&jito_tip_distribution::ID); + let system_program = solana_program::system_program::id(); + let validator_vote_account = vote_keypair.pubkey(); + self.airdrop(&validator_vote_account, 1.0).await?; + let (tip_distribution_account, account_bump) = + jito_tip_distribution_sdk::derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &validator_vote_account, + epoch, + ); + + self.initialize_tip_distribution_account( + merkle_root_upload_authority, + validator_commission_bps, + config, + tip_distribution_account, + system_program, + validator_vote_account, + account_bump, + ) + .await + } + + pub async fn initialize_tip_distribution_account( + &mut self, + merkle_root_upload_authority: Pubkey, + validator_commission_bps: u16, + config: Pubkey, + tip_distribution_account: Pubkey, + system_program: Pubkey, + validator_vote_account: Pubkey, + bump: u8, + ) -> TestResult<()> { + let ix = jito_tip_distribution_sdk::instruction::initialize_tip_distribution_account_ix( + config, + tip_distribution_account, + system_program, + validator_vote_account, + self.payer.pubkey(), + merkle_root_upload_authority, + validator_commission_bps, + bump, + ); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + #[allow(dead_code)] + pub async fn do_claim( + &mut self, + proof: Vec<[u8; 32]>, + amount: u64, + claimant: Pubkey, + epoch: u64, + ) -> TestResult<()> { + let (config, _) = + jito_tip_distribution_sdk::derive_config_account_address(&jito_tip_distribution::ID); + let system_program = solana_program::system_program::id(); + let (tip_distribution_account, _) = + jito_tip_distribution_sdk::derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &claimant, + epoch, + ); + let (claim_status, claim_status_bump) = + jito_tip_distribution_sdk::derive_claim_status_account_address( + &jito_tip_distribution::ID, + &claimant, + &tip_distribution_account, + ); + let payer = self.payer.pubkey(); + + self.claim( + proof, + amount, + config, + tip_distribution_account, + claim_status, + claimant, + payer, + system_program, + claim_status_bump, + ) + .await + } + + #[allow(dead_code)] + pub async fn claim( + &mut self, + proof: Vec<[u8; 32]>, + amount: u64, + config: Pubkey, + tip_distribution_account: Pubkey, + claim_status: Pubkey, + claimant: Pubkey, + payer: Pubkey, + system_program: Pubkey, + bump: u8, + ) -> TestResult<()> { + let ix = jito_tip_distribution_sdk::instruction::claim_ix( + config, + tip_distribution_account, + claim_status, + claimant, + payer, + system_program, + proof, + amount, + bump, + ); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } +} diff --git a/integration_tests/tests/fixtures/tip_router_client.rs b/integration_tests/tests/fixtures/tip_router_client.rs index c4dea87a..95cf1d05 100644 --- a/integration_tests/tests/fixtures/tip_router_client.rs +++ b/integration_tests/tests/fixtures/tip_router_client.rs @@ -2,16 +2,20 @@ use jito_bytemuck::AccountDeserialize; use jito_restaking_core::{ config::Config, ncn_operator_state::NcnOperatorState, ncn_vault_ticket::NcnVaultTicket, }; +use jito_tip_distribution_sdk::{derive_tip_distribution_account_address, jito_tip_distribution}; use jito_tip_router_client::{ instructions::{ - AdminUpdateWeightTableBuilder, InitializeEpochSnapshotBuilder, InitializeNCNConfigBuilder, + AdminUpdateWeightTableBuilder, CastVoteBuilder, InitializeBallotBoxBuilder, + InitializeEpochSnapshotBuilder, InitializeNCNConfigBuilder, InitializeOperatorSnapshotBuilder, InitializeTrackedMintsBuilder, InitializeWeightTableBuilder, RegisterMintBuilder, SetConfigFeesBuilder, - SetNewAdminBuilder, SnapshotVaultOperatorDelegationBuilder, + SetMerkleRootBuilder, SetNewAdminBuilder, SetTieBreakerBuilder, + SnapshotVaultOperatorDelegationBuilder, }, types::ConfigAdminRole, }; use jito_tip_router_core::{ + ballot_box::BallotBox, epoch_snapshot::{EpochSnapshot, OperatorSnapshot}, error::TipRouterError, ncn_config::NcnConfig, @@ -171,6 +175,13 @@ impl TipRouterClient { Ok(*account) } + pub async fn get_ballot_box(&mut self, ncn: Pubkey, epoch: u64) -> TestResult { + let address = + BallotBox::find_program_address(&jito_tip_router_program::id(), &ncn, epoch).0; + let raw_account = self.banks_client.get_account(address).await?.unwrap(); + Ok(*BallotBox::try_from_slice_unchecked(raw_account.data.as_slice()).unwrap()) + } + pub async fn do_initialize_config( &mut self, ncn: Pubkey, @@ -676,6 +687,290 @@ impl TipRouterClient { )) .await } + + pub async fn do_initialize_ballot_box( + &mut self, + ncn: Pubkey, + ncn_epoch: u64, + ) -> Result<(), TestError> { + let ncn_config = jito_tip_router_core::ncn_config::NcnConfig::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ) + .0; + + let ballot_box = jito_tip_router_core::ballot_box::BallotBox::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ncn_epoch, + ) + .0; + + self.initialize_ballot_box(ncn_config, ballot_box, ncn, ncn_epoch) + .await + } + + pub async fn initialize_ballot_box( + &mut self, + ncn_config: Pubkey, + ballot_box: Pubkey, + ncn: Pubkey, + epoch: u64, + ) -> Result<(), TestError> { + let ix = InitializeBallotBoxBuilder::new() + .ncn_config(ncn_config) + .ballot_box(ballot_box) + .ncn(ncn) + .epoch(epoch) + .payer(self.payer.pubkey()) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + pub async fn do_cast_vote( + &mut self, + ncn: Pubkey, + operator: Pubkey, + operator_admin: &Keypair, + meta_merkle_root: [u8; 32], + ncn_epoch: u64, + ) -> Result<(), TestError> { + let ncn_config = jito_tip_router_core::ncn_config::NcnConfig::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ) + .0; + + let ballot_box = jito_tip_router_core::ballot_box::BallotBox::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ncn_epoch, + ) + .0; + + let epoch_snapshot = + jito_tip_router_core::epoch_snapshot::EpochSnapshot::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ncn_epoch, + ) + .0; + + let operator_snapshot = + jito_tip_router_core::epoch_snapshot::OperatorSnapshot::find_program_address( + &jito_tip_router_program::id(), + &operator, + &ncn, + ncn_epoch, + ) + .0; + + self.cast_vote( + ncn_config, + ballot_box, + ncn, + epoch_snapshot, + operator_snapshot, + operator, + operator_admin, + meta_merkle_root, + ncn_epoch, + ) + .await + } + + pub async fn cast_vote( + &mut self, + ncn_config: Pubkey, + ballot_box: Pubkey, + ncn: Pubkey, + epoch_snapshot: Pubkey, + operator_snapshot: Pubkey, + operator: Pubkey, + operator_admin: &Keypair, + meta_merkle_root: [u8; 32], + epoch: u64, + ) -> Result<(), TestError> { + let ix = CastVoteBuilder::new() + .ncn_config(ncn_config) + .ballot_box(ballot_box) + .ncn(ncn) + .epoch_snapshot(epoch_snapshot) + .operator_snapshot(operator_snapshot) + .operator(operator) + .operator_admin(operator_admin.pubkey()) + .restaking_program(jito_restaking_program::id()) + .meta_merkle_root(meta_merkle_root) + .epoch(epoch) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer, operator_admin], + blockhash, + )) + .await + } + + pub async fn do_set_merkle_root( + &mut self, + ncn: Pubkey, + vote_account: Pubkey, + proof: Vec<[u8; 32]>, + merkle_root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + epoch: u64, + ) -> Result<(), TestError> { + let ncn_config = jito_tip_router_core::ncn_config::NcnConfig::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ) + .0; + let ballot_box = + BallotBox::find_program_address(&jito_tip_router_program::id(), &ncn, epoch).0; + + let tip_distribution_program_id = jito_tip_distribution::ID; + let tip_distribution_account = derive_tip_distribution_account_address( + &tip_distribution_program_id, + &vote_account, + epoch, + ) + .0; + + let tip_distribution_config = + jito_tip_distribution_sdk::derive_config_account_address(&tip_distribution_program_id) + .0; + let restaking_program_id = jito_restaking_program::id(); + + self.set_merkle_root( + ncn_config, + ncn, + ballot_box, + vote_account, + tip_distribution_account, + tip_distribution_config, + tip_distribution_program_id, + restaking_program_id, + proof, + merkle_root, + max_total_claim, + max_num_nodes, + epoch, + ) + .await + } + + pub async fn set_merkle_root( + &mut self, + ncn_config: Pubkey, + ncn: Pubkey, + ballot_box: Pubkey, + vote_account: Pubkey, + tip_distribution_account: Pubkey, + tip_distribution_config: Pubkey, + tip_distribution_program_id: Pubkey, + restaking_program_id: Pubkey, + proof: Vec<[u8; 32]>, + merkle_root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + epoch: u64, + ) -> Result<(), TestError> { + let ix = SetMerkleRootBuilder::new() + .ncn_config(ncn_config) + .ncn(ncn) + .ballot_box(ballot_box) + .vote_account(vote_account) + .tip_distribution_account(tip_distribution_account) + .tip_distribution_config(tip_distribution_config) + .tip_distribution_program(tip_distribution_program_id) + .restaking_program(restaking_program_id) + .proof(proof) + .merkle_root(merkle_root) + .max_total_claim(max_total_claim) + .max_num_nodes(max_num_nodes) + .epoch(epoch) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + pub async fn do_set_tie_breaker( + &mut self, + ncn: Pubkey, + meta_merkle_root: [u8; 32], + epoch: u64, + ) -> Result<(), TestError> { + let ncn_config = jito_tip_router_core::ncn_config::NcnConfig::find_program_address( + &jito_tip_router_program::id(), + &ncn, + ) + .0; + let ballot_box = + BallotBox::find_program_address(&jito_tip_router_program::id(), &ncn, epoch).0; + + let tie_breaker_admin = self.payer.pubkey(); + let restaking_program_id = jito_restaking_program::id(); + + self.set_tie_breaker( + ncn_config, + ballot_box, + ncn, + tie_breaker_admin, + meta_merkle_root, + epoch, + restaking_program_id, + ) + .await + } + + pub async fn set_tie_breaker( + &mut self, + ncn_config: Pubkey, + ballot_box: Pubkey, + ncn: Pubkey, + tie_breaker_admin: Pubkey, + meta_merkle_root: [u8; 32], + epoch: u64, + restaking_program_id: Pubkey, + ) -> Result<(), TestError> { + let ix = SetTieBreakerBuilder::new() + .ncn_config(ncn_config) + .ballot_box(ballot_box) + .ncn(ncn) + .tie_breaker_admin(tie_breaker_admin) + .meta_merkle_root(meta_merkle_root) + .epoch(epoch) + .restaking_program(restaking_program_id) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } } #[inline(always)] diff --git a/integration_tests/tests/helpers/ballot_box.rs b/integration_tests/tests/helpers/ballot_box.rs new file mode 100644 index 00000000..38b84742 --- /dev/null +++ b/integration_tests/tests/helpers/ballot_box.rs @@ -0,0 +1,20 @@ +use jito_bytemuck::Discriminator; +use jito_tip_router_core::ballot_box::BallotBox; +use solana_sdk::{account::Account, native_token::LAMPORTS_PER_SOL}; + +pub fn serialized_ballot_box_account(ballot_box: &BallotBox) -> Account { + // TODO add AccountSerialize to jito_restaking::bytemuck? + let mut data = vec![BallotBox::DISCRIMINATOR; 1]; + data.extend_from_slice(&[0; 7]); + data.extend_from_slice(bytemuck::bytes_of(ballot_box)); + + let account = Account { + lamports: LAMPORTS_PER_SOL * 5, + data, + owner: jito_tip_router_program::id(), + executable: false, + rent_epoch: 0, + }; + + account +} diff --git a/integration_tests/tests/helpers/mod.rs b/integration_tests/tests/helpers/mod.rs new file mode 100644 index 00000000..bc023a6f --- /dev/null +++ b/integration_tests/tests/helpers/mod.rs @@ -0,0 +1 @@ +pub mod ballot_box; diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index e761317b..ffd4b7d5 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1,2 +1,3 @@ mod fixtures; +mod helpers; mod tip_router; diff --git a/integration_tests/tests/tip_router/bpf/mod.rs b/integration_tests/tests/tip_router/bpf/mod.rs new file mode 100644 index 00000000..22d4ca31 --- /dev/null +++ b/integration_tests/tests/tip_router/bpf/mod.rs @@ -0,0 +1 @@ +mod set_merkle_root; diff --git a/integration_tests/tests/tip_router/bpf/set_merkle_root.rs b/integration_tests/tests/tip_router/bpf/set_merkle_root.rs new file mode 100644 index 00000000..6bc9f30e --- /dev/null +++ b/integration_tests/tests/tip_router/bpf/set_merkle_root.rs @@ -0,0 +1,454 @@ +mod set_merkle_root { + use jito_tip_distribution_sdk::{ + derive_claim_status_account_address, derive_tip_distribution_account_address, + jito_tip_distribution, + }; + use jito_tip_router_core::{ + ballot_box::{Ballot, BallotBox}, + error::TipRouterError, + ncn_config::NcnConfig, + }; + use meta_merkle_tree::{ + generated_merkle_tree::{ + self, Delegation, GeneratedMerkleTree, GeneratedMerkleTreeCollection, StakeMeta, + StakeMetaCollection, TipDistributionMeta, + }, + meta_merkle_tree::MetaMerkleTree, + }; + use solana_sdk::{epoch_schedule::EpochSchedule, pubkey::Pubkey, signer::Signer}; + + use crate::{ + fixtures::{ + test_builder::TestBuilder, tip_router_client::assert_tip_router_error, TestError, + TestResult, + }, + helpers::ballot_box::serialized_ballot_box_account, + }; + + struct GeneratedMerkleTreeCollectionFixture { + _test_generated_merkle_tree: GeneratedMerkleTree, + collection: GeneratedMerkleTreeCollection, + } + + fn _create_tree_node( + claimant_staker_withdrawer: Pubkey, + amount: u64, + epoch: u64, + ) -> generated_merkle_tree::TreeNode { + let (claim_status_pubkey, claim_status_bump) = derive_claim_status_account_address( + &jito_tip_distribution::ID, + &claimant_staker_withdrawer, + &derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &claimant_staker_withdrawer, + epoch, + ) + .0, + ); + + generated_merkle_tree::TreeNode { + claimant: claimant_staker_withdrawer, + claim_status_pubkey, + claim_status_bump, + staker_pubkey: claimant_staker_withdrawer, + withdrawer_pubkey: claimant_staker_withdrawer, + amount, + proof: None, + } + } + + fn create_generated_merkle_tree_collection( + vote_account: Pubkey, + merkle_root_upload_authority: Pubkey, + epoch: u64, + ) -> TestResult { + let claimant_staker_withdrawer = Pubkey::new_unique(); + + let test_delegation = Delegation { + stake_account_pubkey: claimant_staker_withdrawer, + staker_pubkey: claimant_staker_withdrawer, + withdrawer_pubkey: claimant_staker_withdrawer, + lamports_delegated: 50, + }; + + let vote_account_stake_meta = StakeMeta { + validator_vote_account: vote_account, + validator_node_pubkey: Pubkey::new_unique(), + maybe_tip_distribution_meta: Some(TipDistributionMeta { + merkle_root_upload_authority, + tip_distribution_pubkey: derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &vote_account, + epoch, + ) + .0, + total_tips: 50, + validator_fee_bps: 0, + }), + delegations: vec![test_delegation.clone()], + total_delegated: 50, + commission: 0, + }; + + let other_validator = Pubkey::new_unique(); + let other_stake_meta = StakeMeta { + validator_vote_account: other_validator, + validator_node_pubkey: Pubkey::new_unique(), + maybe_tip_distribution_meta: Some(TipDistributionMeta { + merkle_root_upload_authority: other_validator, + tip_distribution_pubkey: derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &other_validator, + epoch, + ) + .0, + total_tips: 50, + validator_fee_bps: 0, + }), + delegations: vec![test_delegation], + total_delegated: 50, + commission: 0, + }; + + let stake_meta_collection = StakeMetaCollection { + stake_metas: vec![vote_account_stake_meta, other_stake_meta], + tip_distribution_program_id: Pubkey::new_unique(), + bank_hash: String::default(), + epoch, + slot: 0, + }; + + let collection = + GeneratedMerkleTreeCollection::new_from_stake_meta_collection(stake_meta_collection) + .map_err(TestError::from)?; + + let test_tip_distribution_account = derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &vote_account, + epoch, + ) + .0; + let test_generated_merkle_tree = collection + .generated_merkle_trees + .iter() + .find(|tree| tree.tip_distribution_account == test_tip_distribution_account) + .unwrap(); + + Ok(GeneratedMerkleTreeCollectionFixture { + _test_generated_merkle_tree: test_generated_merkle_tree.clone(), + collection, + }) + } + + struct MetaMerkleTreeFixture { + // Contains the individual validator's merkle trees, with the TreeNode idata needed to invoke the set_merkle_root instruction (root, max_num_nodes, max_total_claim) + _generated_merkle_tree_fixture: GeneratedMerkleTreeCollectionFixture, + // Contains meta merkle tree with the root that all validators vote on, and proofs needed to verify the input data + pub meta_merkle_tree: MetaMerkleTree, + } + + fn create_meta_merkle_tree( + vote_account: Pubkey, + merkle_root_upload_authority: Pubkey, + epoch: u64, + ) -> TestResult { + let generated_merkle_tree_fixture = create_generated_merkle_tree_collection( + vote_account, + merkle_root_upload_authority, + epoch, + ) + .map_err(TestError::from)?; + + let meta_merkle_tree = MetaMerkleTree::new_from_generated_merkle_tree_collection( + generated_merkle_tree_fixture.collection.clone(), + )?; + + Ok(MetaMerkleTreeFixture { + _generated_merkle_tree_fixture: generated_merkle_tree_fixture, + meta_merkle_tree, + }) + } + + #[tokio::test] + async fn test_set_merkle_root_ok() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let mut tip_distribution_client = fixture.tip_distribution_client(); + + let test_ncn = fixture.create_test_ncn().await?; + let ncn_address = test_ncn.ncn_root.ncn_pubkey; + let ncn_config_address = + NcnConfig::find_program_address(&jito_tip_router_program::id(), &ncn_address).0; + + let epoch = 0; + tip_distribution_client + .do_initialize(ncn_config_address) + .await?; + let vote_keypair = tip_distribution_client.setup_vote_account().await?; + let vote_account = vote_keypair.pubkey(); + + tip_distribution_client + .do_initialize_tip_distribution_account(ncn_config_address, vote_keypair, epoch, 100) + .await?; + + let meta_merkle_tree_fixture = + create_meta_merkle_tree(vote_account, ncn_config_address, epoch)?; + let winning_root = meta_merkle_tree_fixture.meta_merkle_tree.merkle_root; + + let (ballot_box_address, bump, _) = + BallotBox::find_program_address(&jito_tip_router_program::id(), &ncn_address, epoch); + + let ballot_box_fixture = { + let mut ballot_box = BallotBox::new(ncn_address, epoch, bump, 0); + let winning_ballot = Ballot::new(winning_root); + ballot_box.set_winning_ballot(winning_ballot); + ballot_box + }; + + let epoch_schedule: EpochSchedule = fixture.epoch_schedule().await; + + // Must warp before .set_account + fixture + .warp_slot_incremental(epoch_schedule.get_slots_in_epoch(epoch)) + .await?; + + fixture + .set_account( + ballot_box_address, + serialized_ballot_box_account(&ballot_box_fixture), + ) + .await; + + let tip_distribution_address = derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &vote_account, + epoch, + ) + .0; + + // Get proof for vote_account + let node = meta_merkle_tree_fixture + .meta_merkle_tree + .get_node(&tip_distribution_address); + let proof = node.proof.clone().unwrap(); + + ballot_box_fixture + .verify_merkle_root( + tip_distribution_address, + node.proof.unwrap(), + node.validator_merkle_root, + node.max_total_claim, + node.max_num_nodes, + ) + .unwrap(); + + // Test wrong proof + let res = tip_router_client + .do_set_merkle_root( + ncn_address, + vote_account, + vec![[1; 32]], + node.validator_merkle_root, + node.max_total_claim, + node.max_num_nodes, + epoch, + ) + .await; + assert_tip_router_error(res, TipRouterError::InvalidMerkleProof); + + // Invoke set_merkle_root + tip_router_client + .do_set_merkle_root( + ncn_address, + vote_account, + proof, + node.validator_merkle_root, + node.max_total_claim, + node.max_num_nodes, + epoch, + ) + .await?; + + // Fetch the tip distribution account and check root + let tip_distribution_account = tip_distribution_client + .get_tip_distribution_account(vote_account, epoch) + .await?; + + let merkle_root = tip_distribution_account.merkle_root.unwrap(); + + assert_eq!(merkle_root.root, node.validator_merkle_root); + assert_eq!(merkle_root.max_num_nodes, node.max_num_nodes); + assert_eq!(merkle_root.max_total_claim, node.max_total_claim); + + Ok(()) + } + + // TODO update to use this test once snapshot instructions work with BPF + #[ignore] + #[tokio::test] + async fn _test_set_merkle_root_no_fixture() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let mut tip_distribution_client = fixture.tip_distribution_client(); + + let test_ncn = fixture.create_initial_test_ncn(1, 1).await?; + + ///// TipRouter Setup ///// + fixture.warp_slot_incremental(1000).await?; + fixture.snapshot_test_ncn(&test_ncn).await?; + + let clock = fixture.clock().await; + let slot = clock.slot; + let restaking_config_account = tip_router_client.get_restaking_config().await?; + let ncn_epoch = slot / restaking_config_account.epoch_length(); + + let ncn = test_ncn.ncn_root.ncn_pubkey; + let ncn_config_address = + NcnConfig::find_program_address(&jito_tip_router_program::id(), &ncn).0; + + let epoch: u64 = 0; + tip_distribution_client + .do_initialize(ncn_config_address) + .await?; + let vote_keypair = tip_distribution_client.setup_vote_account().await?; + let vote_account = vote_keypair.pubkey(); + + tip_distribution_client + .do_initialize_tip_distribution_account( + ncn_config_address, + vote_keypair, + ncn_epoch, + 100, + ) + .await?; + + let meta_merkle_tree_fixture = + create_meta_merkle_tree(vote_account, ncn_config_address, epoch)?; + let winning_root = meta_merkle_tree_fixture.meta_merkle_tree.merkle_root; + + let operator = test_ncn.operators[0].operator_pubkey; + let operator_admin = &test_ncn.operators[0].operator_admin; + + tip_router_client + .do_cast_vote(ncn, operator, operator_admin, winning_root, ncn_epoch) + .await?; + let tip_distribution_address = derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &vote_account, + epoch, + ) + .0; + + // Get proof for vote_account + let node = meta_merkle_tree_fixture + .meta_merkle_tree + .get_node(&tip_distribution_address); + let proof = node.proof.clone().unwrap(); + + let ballot_box = tip_router_client.get_ballot_box(ncn, epoch).await?; + + ballot_box + .verify_merkle_root( + tip_distribution_address, + node.proof.unwrap(), + node.validator_merkle_root, + node.max_total_claim, + node.max_num_nodes, + ) + .unwrap(); + + // Invoke set_merkle_root + tip_router_client + .do_set_merkle_root( + ncn, + vote_account, + proof, + node.validator_merkle_root, + node.max_total_claim, + node.max_num_nodes, + epoch, + ) + .await?; + + // Fetch the tip distribution account and check root + let tip_distribution_account = tip_distribution_client + .get_tip_distribution_account(vote_account, epoch) + .await?; + + let merkle_root = tip_distribution_account.merkle_root.unwrap(); + + assert_eq!(merkle_root.root, node.validator_merkle_root); + assert_eq!(merkle_root.max_num_nodes, node.max_num_nodes); + assert_eq!(merkle_root.max_total_claim, node.max_total_claim); + + Ok(()) + } + + #[tokio::test] + async fn test_set_merkle_root_before_consensus() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let mut tip_distribution_client = fixture.tip_distribution_client(); + + let test_ncn = fixture.create_test_ncn().await?; + let ncn = test_ncn.ncn_root.ncn_pubkey; + let ncn_config_address = + NcnConfig::find_program_address(&jito_tip_router_program::id(), &ncn).0; + + let clock = fixture.clock().await; + let slot = clock.slot; + let restaking_config_account = tip_router_client.get_restaking_config().await?; + let ncn_epoch = slot / restaking_config_account.epoch_length(); + + tip_distribution_client + .do_initialize(ncn_config_address) + .await?; + let vote_keypair = tip_distribution_client.setup_vote_account().await?; + let vote_account = vote_keypair.pubkey(); + + tip_distribution_client + .do_initialize_tip_distribution_account( + ncn_config_address, + vote_keypair, + ncn_epoch, + 100, + ) + .await?; + + let meta_merkle_tree_fixture = + create_meta_merkle_tree(vote_account, ncn_config_address, ncn_epoch)?; + + let tip_distribution_address = derive_tip_distribution_account_address( + &jito_tip_distribution::ID, + &vote_account, + ncn_epoch, + ) + .0; + let node = meta_merkle_tree_fixture + .meta_merkle_tree + .get_node(&tip_distribution_address); + let proof = node.proof.clone().unwrap(); + + // Initialize ballot box + tip_router_client + .do_initialize_ballot_box(ncn, ncn_epoch) + .await?; + + // Try setting merkle root before consensus + let res = tip_router_client + .do_set_merkle_root( + ncn, + vote_account, + proof, + node.validator_merkle_root, + node.max_total_claim, + node.max_num_nodes, + ncn_epoch, + ) + .await; + + assert_tip_router_error(res, TipRouterError::ConsensusNotReached); + + Ok(()) + } +} diff --git a/integration_tests/tests/tip_router/cast_vote.rs b/integration_tests/tests/tip_router/cast_vote.rs new file mode 100644 index 00000000..3e03f9d6 --- /dev/null +++ b/integration_tests/tests/tip_router/cast_vote.rs @@ -0,0 +1,47 @@ +#[cfg(test)] +mod tests { + use jito_tip_router_core::ballot_box::Ballot; + + use crate::fixtures::{test_builder::TestBuilder, TestResult}; + + #[tokio::test] + async fn test_cast_vote() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + + let test_ncn = fixture.create_initial_test_ncn(1, 1).await?; + + ///// TipRouter Setup ///// + fixture.warp_slot_incremental(1000).await?; + + fixture.snapshot_test_ncn(&test_ncn).await?; + ////// + + let clock = fixture.clock().await; + let slot = clock.slot; + let ncn = test_ncn.ncn_root.ncn_pubkey; + let operator = test_ncn.operators[0].operator_pubkey; + let restaking_config_account = tip_router_client.get_restaking_config().await?; + let ncn_epoch = slot / restaking_config_account.epoch_length(); + + tip_router_client + .do_initialize_ballot_box(ncn, ncn_epoch) + .await?; + + let meta_merkle_root = [1u8; 32]; + + let operator_admin = &test_ncn.operators[0].operator_admin; + + tip_router_client + .do_cast_vote(ncn, operator, operator_admin, meta_merkle_root, ncn_epoch) + .await?; + + let ballot_box = tip_router_client.get_ballot_box(ncn, ncn_epoch).await?; + + assert!(ballot_box.has_ballot(&Ballot::new(meta_merkle_root))); + assert_eq!(ballot_box.slot_consensus_reached(), slot); + assert!(ballot_box.is_consensus_reached()); + + Ok(()) + } +} diff --git a/integration_tests/tests/tip_router/initialize_ballot_box.rs b/integration_tests/tests/tip_router/initialize_ballot_box.rs new file mode 100644 index 00000000..35fafee1 --- /dev/null +++ b/integration_tests/tests/tip_router/initialize_ballot_box.rs @@ -0,0 +1,39 @@ +#[cfg(test)] +mod tests { + + use jito_tip_router_core::constants::DEFAULT_CONSENSUS_REACHED_SLOT; + + use crate::fixtures::{test_builder::TestBuilder, TestResult}; + + #[tokio::test] + async fn test_initialize_ballot_box() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + + let test_ncn = fixture.create_initial_test_ncn(1, 1).await?; + + fixture.warp_slot_incremental(1000).await?; + + let epoch = fixture.clock().await.epoch; + + let ncn = test_ncn.ncn_root.ncn_pubkey; + + tip_router_client + .do_initialize_ballot_box(ncn, epoch) + .await?; + + let ballot_box = tip_router_client.get_ballot_box(ncn, epoch).await?; + + assert_eq!(ballot_box.epoch(), epoch); + assert_eq!(ballot_box.unique_ballots(), 0); + assert_eq!(ballot_box.operators_voted(), 0); + assert!(!ballot_box.is_consensus_reached()); + assert_eq!( + ballot_box.slot_consensus_reached(), + DEFAULT_CONSENSUS_REACHED_SLOT + ); + assert!(ballot_box.get_winning_ballot().is_err(),); + + Ok(()) + } +} diff --git a/integration_tests/tests/tip_router/mod.rs b/integration_tests/tests/tip_router/mod.rs index 43d86a43..f9f24c8d 100644 --- a/integration_tests/tests/tip_router/mod.rs +++ b/integration_tests/tests/tip_router/mod.rs @@ -1,4 +1,7 @@ mod admin_update_weight_table; +mod bpf; +mod cast_vote; +mod initialize_ballot_box; mod initialize_epoch_snapshot; mod initialize_ncn_config; mod initialize_operator_snapshot; @@ -8,4 +11,5 @@ mod meta_tests; mod register_mint; mod set_config_fees; mod set_new_admin; +mod set_tie_breaker; mod snapshot_vault_operator_delegation; diff --git a/integration_tests/tests/tip_router/set_tie_breaker.rs b/integration_tests/tests/tip_router/set_tie_breaker.rs new file mode 100644 index 00000000..ff09bd66 --- /dev/null +++ b/integration_tests/tests/tip_router/set_tie_breaker.rs @@ -0,0 +1,70 @@ +#[cfg(test)] +mod tests { + + use jito_tip_router_core::{ballot_box::Ballot, constants::DEFAULT_CONSENSUS_REACHED_SLOT}; + + use crate::fixtures::{test_builder::TestBuilder, TestResult}; + + #[tokio::test] + async fn test_set_tie_breaker() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + + // Each operator gets 50% voting share + let test_ncn = fixture.create_initial_test_ncn(2, 1).await?; + + ///// TipRouter Setup ///// + fixture.snapshot_test_ncn(&test_ncn).await?; + ////// + + let clock = fixture.clock().await; + let slot = clock.slot; + let restaking_config_account = tip_router_client.get_restaking_config().await?; + let ncn_epoch = slot / restaking_config_account.epoch_length(); + let ncn = test_ncn.ncn_root.ncn_pubkey; + + tip_router_client + .do_initialize_ballot_box(ncn, ncn_epoch) + .await?; + + let meta_merkle_root = [1; 32]; + + let operator = test_ncn.operators[0].operator_pubkey; + let operator_admin = &test_ncn.operators[0].operator_admin; + + // Cast a vote so that this vote is one of the valid options + // Gets to 50% consensus weight + tip_router_client + .do_cast_vote(ncn, operator, operator_admin, meta_merkle_root, ncn_epoch) + .await?; + + let ballot_box = tip_router_client.get_ballot_box(ncn, ncn_epoch).await?; + assert!(ballot_box.has_ballot(&Ballot::new(meta_merkle_root))); + assert_eq!( + ballot_box.slot_consensus_reached(), + DEFAULT_CONSENSUS_REACHED_SLOT + ); + assert!(!ballot_box.is_consensus_reached()); + + // Wait a bunch of epochs for voting window to expire (TODO use the exact length) + fixture.warp_slot_incremental(1000000).await?; + + tip_router_client + .do_set_tie_breaker(ncn, meta_merkle_root, ncn_epoch) + .await?; + + let ballot_box = tip_router_client.get_ballot_box(ncn, ncn_epoch).await?; + + let ballot = Ballot::new(meta_merkle_root); + assert!(ballot_box.has_ballot(&ballot)); + assert_eq!(ballot_box.get_winning_ballot().unwrap(), ballot); + // No official consensus reached so no slot set + assert_eq!( + ballot_box.slot_consensus_reached(), + DEFAULT_CONSENSUS_REACHED_SLOT + ); + assert!(ballot_box.is_consensus_reached()); + + Ok(()) + } +} diff --git a/meta_merkle_tree/Cargo.toml b/meta_merkle_tree/Cargo.toml new file mode 100644 index 00000000..560236da --- /dev/null +++ b/meta_merkle_tree/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "meta-merkle-tree" +description = "Meta Merkle Tree" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +readme = { workspace = true } + +[dependencies] +borsh = { workspace = true } +bytemuck = { workspace = true } +fast-math = { workspace = true } +hex = { workspace = true } +jito-bytemuck = { workspace = true } +jito-jsm-core = { workspace = true } +jito-restaking-core = { workspace = true } +jito-restaking-sdk = { workspace = true } +jito-tip-distribution-sdk = { workspace = true } +jito-vault-core = { workspace = true } +jito-vault-sdk = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +shank = { workspace = true } +solana-program = { workspace = true } +spl-associated-token-account = { workspace = true } +spl-math = { workspace = true } +spl-token = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +solana-sdk = { workspace = true } diff --git a/meta_merkle_tree/src/error.rs b/meta_merkle_tree/src/error.rs new file mode 100644 index 00000000..ca72c249 --- /dev/null +++ b/meta_merkle_tree/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum MerkleTreeError { + #[error("Merkle Tree Validation Error: {0}")] + MerkleValidationError(String), + #[error("Merkle Root Error")] + MerkleRootError, + #[error("io Error: {0}")] + IoError(#[from] std::io::Error), + #[error("Serde Error: {0}")] + SerdeError(#[from] serde_json::Error), + #[error("Arithmetic Overflow/Underflow")] + ArithmeticOverflow, +} diff --git a/meta_merkle_tree/src/generated_merkle_tree.rs b/meta_merkle_tree/src/generated_merkle_tree.rs new file mode 100644 index 00000000..c08f3ab4 --- /dev/null +++ b/meta_merkle_tree/src/generated_merkle_tree.rs @@ -0,0 +1,342 @@ +// Mostly copied from modules in jito-solana/tip-distributor/src +// To be replaced by tip distributor code in this repo +use std::{fs::File, io::BufReader, path::PathBuf}; + +use jito_tip_distribution_sdk::{jito_tip_distribution, CLAIM_STATUS_SEED}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use solana_program::{ + clock::{Epoch, Slot}, + hash::{Hash, Hasher}, + pubkey::Pubkey, +}; +use thiserror::Error; + +use crate::{merkle_tree::MerkleTree, utils::get_proof}; + +#[derive(Error, Debug)] +pub enum MerkleRootGeneratorError { + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + SerdeJsonError(#[from] serde_json::Error), +} + +#[derive(Clone, Deserialize, Serialize, Debug)] +pub struct GeneratedMerkleTreeCollection { + pub generated_merkle_trees: Vec, + pub bank_hash: String, + pub epoch: Epoch, + pub slot: Slot, +} + +#[derive(Clone, Eq, Debug, Hash, PartialEq, Deserialize, Serialize)] +pub struct GeneratedMerkleTree { + #[serde(with = "pubkey_string_conversion")] + pub tip_distribution_account: Pubkey, + #[serde(with = "pubkey_string_conversion")] + pub merkle_root_upload_authority: Pubkey, + pub merkle_root: Hash, + pub tree_nodes: Vec, + pub max_total_claim: u64, + pub max_num_nodes: u64, +} + +impl GeneratedMerkleTreeCollection { + pub fn new_from_stake_meta_collection( + stake_meta_coll: StakeMetaCollection, + ) -> Result { + let generated_merkle_trees = stake_meta_coll + .stake_metas + .into_iter() + .filter(|stake_meta| stake_meta.maybe_tip_distribution_meta.is_some()) + .filter_map(|stake_meta| { + let mut tree_nodes = match TreeNode::vec_from_stake_meta(&stake_meta) { + Err(e) => return Some(Err(e)), + Ok(maybe_tree_nodes) => maybe_tree_nodes, + }?; + + // if let Some(rpc_client) = &maybe_rpc_client { + // if let Some(tda) = stake_meta.maybe_tip_distribution_meta.as_ref() { + // emit_inconsistent_tree_node_amount_dp( + // &tree_nodes[..], + // &tda.tip_distribution_pubkey, + // rpc_client, + // ); + // } + // } + + let hashed_nodes: Vec<[u8; 32]> = + tree_nodes.iter().map(|n| n.hash().to_bytes()).collect(); + + let tip_distribution_meta = stake_meta.maybe_tip_distribution_meta.unwrap(); + + let merkle_tree = MerkleTree::new(&hashed_nodes[..], true); + let max_num_nodes = tree_nodes.len() as u64; + + for (i, tree_node) in tree_nodes.iter_mut().enumerate() { + tree_node.proof = Some(get_proof(&merkle_tree, i)); + } + + Some(Ok(GeneratedMerkleTree { + max_num_nodes, + tip_distribution_account: tip_distribution_meta.tip_distribution_pubkey, + merkle_root_upload_authority: tip_distribution_meta + .merkle_root_upload_authority, + merkle_root: *merkle_tree.get_root().unwrap(), + tree_nodes, + max_total_claim: tip_distribution_meta.total_tips, + })) + }) + .collect::, MerkleRootGeneratorError>>()?; + + Ok(Self { + generated_merkle_trees, + bank_hash: stake_meta_coll.bank_hash, + epoch: stake_meta_coll.epoch, + slot: stake_meta_coll.slot, + }) + } +} + +#[derive(Clone, Eq, Debug, Hash, PartialEq, Deserialize, Serialize)] +pub struct TreeNode { + /// The stake account entitled to redeem. + #[serde(with = "pubkey_string_conversion")] + pub claimant: Pubkey, + + /// Pubkey of the ClaimStatus PDA account, this account should be closed to reclaim rent. + #[serde(with = "pubkey_string_conversion")] + pub claim_status_pubkey: Pubkey, + + /// Bump of the ClaimStatus PDA account + pub claim_status_bump: u8, + + #[serde(with = "pubkey_string_conversion")] + pub staker_pubkey: Pubkey, + + #[serde(with = "pubkey_string_conversion")] + pub withdrawer_pubkey: Pubkey, + + /// The amount this account is entitled to. + pub amount: u64, + + /// The proof associated with this TreeNode + pub proof: Option>, +} +impl TreeNode { + fn vec_from_stake_meta( + stake_meta: &StakeMeta, + ) -> Result>, MerkleRootGeneratorError> { + if let Some(tip_distribution_meta) = stake_meta.maybe_tip_distribution_meta.as_ref() { + let validator_amount = (tip_distribution_meta.total_tips as u128) + .checked_mul(tip_distribution_meta.validator_fee_bps as u128) + .unwrap() + .checked_div(10_000) + .unwrap() as u64; + let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address( + &[ + CLAIM_STATUS_SEED, + &stake_meta.validator_vote_account.to_bytes(), + &tip_distribution_meta.tip_distribution_pubkey.to_bytes(), + ], + &jito_tip_distribution::ID, + ); + let mut tree_nodes = vec![Self { + claimant: stake_meta.validator_vote_account, + claim_status_pubkey, + claim_status_bump, + staker_pubkey: Pubkey::default(), + withdrawer_pubkey: Pubkey::default(), + amount: validator_amount, + proof: None, + }]; + + let remaining_total_rewards = tip_distribution_meta + .total_tips + .checked_sub(validator_amount) + .unwrap() as u128; + + let total_delegated = stake_meta.total_delegated as u128; + tree_nodes.extend( + stake_meta + .delegations + .iter() + .map(|delegation| { + let amount_delegated = delegation.lamports_delegated as u128; + let reward_amount = (amount_delegated.checked_mul(remaining_total_rewards)) + .unwrap() + .checked_div(total_delegated) + .unwrap(); + let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address( + &[ + CLAIM_STATUS_SEED, + &delegation.stake_account_pubkey.to_bytes(), + &tip_distribution_meta.tip_distribution_pubkey.to_bytes(), + ], + &jito_tip_distribution::ID, + ); + Ok(Self { + claimant: delegation.stake_account_pubkey, + claim_status_pubkey, + claim_status_bump, + staker_pubkey: delegation.staker_pubkey, + withdrawer_pubkey: delegation.withdrawer_pubkey, + amount: reward_amount as u64, + proof: None, + }) + }) + .collect::, MerkleRootGeneratorError>>()?, + ); + + Ok(Some(tree_nodes)) + } else { + Ok(None) + } + } + + fn hash(&self) -> Hash { + let mut hasher = Hasher::default(); + hasher.hash(self.claimant.as_ref()); + hasher.hash(self.amount.to_le_bytes().as_ref()); + hasher.result() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StakeMetaCollection { + /// List of [StakeMeta]. + pub stake_metas: Vec, + + /// base58 encoded tip-distribution program id. + #[serde(with = "pubkey_string_conversion")] + pub tip_distribution_program_id: Pubkey, + + /// Base58 encoded bank hash this object was generated at. + pub bank_hash: String, + + /// Epoch for which this object was generated for. + pub epoch: Epoch, + + /// Slot at which this object was generated. + pub slot: Slot, +} + +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct StakeMeta { + #[serde(with = "pubkey_string_conversion")] + pub validator_vote_account: Pubkey, + + #[serde(with = "pubkey_string_conversion")] + pub validator_node_pubkey: Pubkey, + + /// The validator's tip-distribution meta if it exists. + pub maybe_tip_distribution_meta: Option, + + /// Delegations to this validator. + pub delegations: Vec, + + /// The total amount of delegations to the validator. + pub total_delegated: u64, + + /// The validator's delegation commission rate as a percentage between 0-100. + pub commission: u8, +} + +impl Ord for StakeMeta { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.validator_vote_account + .cmp(&other.validator_vote_account) + } +} + +impl PartialOrd for StakeMeta { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct TipDistributionMeta { + #[serde(with = "pubkey_string_conversion")] + pub merkle_root_upload_authority: Pubkey, + + #[serde(with = "pubkey_string_conversion")] + pub tip_distribution_pubkey: Pubkey, + + /// The validator's total tips in the [TipDistributionAccount]. + pub total_tips: u64, + + /// The validator's cut of tips from [TipDistributionAccount], calculated from the on-chain + /// commission fee bps. + pub validator_fee_bps: u16, +} + +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct Delegation { + #[serde(with = "pubkey_string_conversion")] + pub stake_account_pubkey: Pubkey, + + #[serde(with = "pubkey_string_conversion")] + pub staker_pubkey: Pubkey, + + #[serde(with = "pubkey_string_conversion")] + pub withdrawer_pubkey: Pubkey, + + /// Lamports delegated by the stake account + pub lamports_delegated: u64, +} + +impl Ord for Delegation { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + ( + self.stake_account_pubkey, + self.withdrawer_pubkey, + self.staker_pubkey, + self.lamports_delegated, + ) + .cmp(&( + other.stake_account_pubkey, + other.withdrawer_pubkey, + other.staker_pubkey, + other.lamports_delegated, + )) + } +} + +impl PartialOrd for Delegation { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +mod pubkey_string_conversion { + use std::str::FromStr; + + use serde::{self, Deserialize, Deserializer, Serializer}; + use solana_program::pubkey::Pubkey; + + pub fn serialize(pubkey: &Pubkey, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&pubkey.to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Pubkey::from_str(&s).map_err(serde::de::Error::custom) + } +} + +pub fn read_json_from_file(path: &PathBuf) -> serde_json::Result +where + T: DeserializeOwned, +{ + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + serde_json::from_reader(reader) +} diff --git a/meta_merkle_tree/src/lib.rs b/meta_merkle_tree/src/lib.rs new file mode 100644 index 00000000..e22dc3ed --- /dev/null +++ b/meta_merkle_tree/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod generated_merkle_tree; +pub mod merkle_tree; +pub mod meta_merkle_tree; +pub mod tree_node; +pub mod utils; +pub mod verify; diff --git a/meta_merkle_tree/src/merkle_tree.rs b/meta_merkle_tree/src/merkle_tree.rs new file mode 100644 index 00000000..c9924751 --- /dev/null +++ b/meta_merkle_tree/src/merkle_tree.rs @@ -0,0 +1,320 @@ +#![allow(clippy::arithmetic_side_effects)] +// https://github.com/jito-foundation/jito-solana/blob/v1.16.19-jito/merkle-tree/src/merkle_tree.rs +use solana_program::hash::{hashv, Hash}; + +// We need to discern between leaf and intermediate nodes to prevent trivial second +// pre-image attacks. +// https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack +const LEAF_PREFIX: &[u8] = &[0]; +const INTERMEDIATE_PREFIX: &[u8] = &[1]; + +macro_rules! hash_leaf { + {$d:ident} => { + hashv(&[LEAF_PREFIX, $d]) + } +} + +macro_rules! hash_intermediate { + {$l:ident, $r:ident} => { + hashv(&[INTERMEDIATE_PREFIX, $l.as_ref(), $r.as_ref()]) + } +} + +#[derive(Default, Debug, Eq, Hash, PartialEq)] +pub struct MerkleTree { + leaf_count: usize, + nodes: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ProofEntry<'a>(&'a Hash, Option<&'a Hash>, Option<&'a Hash>); + +impl<'a> ProofEntry<'a> { + pub fn new( + target: &'a Hash, + left_sibling: Option<&'a Hash>, + right_sibling: Option<&'a Hash>, + ) -> Self { + assert!(left_sibling.is_none() ^ right_sibling.is_none()); + Self(target, left_sibling, right_sibling) + } + + pub const fn get_left_sibling(&self) -> Option<&'a Hash> { + self.1 + } + + pub const fn get_right_sibling(&self) -> Option<&'a Hash> { + self.2 + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Proof<'a>(Vec>); + +impl<'a> Proof<'a> { + pub fn push(&mut self, entry: ProofEntry<'a>) { + self.0.push(entry) + } + + pub fn verify(&self, candidate: Hash) -> bool { + let result = self.0.iter().try_fold(candidate, |candidate, pe| { + let lsib = pe.1.unwrap_or(&candidate); + let rsib = pe.2.unwrap_or(&candidate); + let hash = hash_intermediate!(lsib, rsib); + + if hash == *pe.0 { + Some(hash) + } else { + None + } + }); + result.is_some() + } + + pub fn get_proof_entries(self) -> Vec> { + self.0 + } +} + +impl MerkleTree { + #[allow(clippy::integer_division)] + const fn next_level_len(level_len: usize) -> usize { + if level_len == 1 { + 0 + } else { + (level_len + 1) / 2 + } + } + + fn calculate_vec_capacity(leaf_count: usize) -> usize { + // the most nodes consuming case is when n-1 is full balanced binary tree + // then n will cause the previous tree add a left only path to the root + // this cause the total nodes number increased by tree height, we use this + // condition as the max nodes consuming case. + // n is current leaf nodes number + // assuming n-1 is a full balanced binary tree, n-1 tree nodes number will be + // 2(n-1) - 1, n tree height is closed to log2(n) + 1 + // so the max nodes number is 2(n-1) - 1 + log2(n) + 1, finally we can use + // 2n + log2(n+1) as a safe capacity value. + // test results: + // 8192 leaf nodes(full balanced): + // computed cap is 16398, actually using is 16383 + // 8193 leaf nodes:(full balanced plus 1 leaf): + // computed cap is 16400, actually using is 16398 + // about performance: current used fast_math log2 code is constant algo time + if leaf_count > 0 { + fast_math::log2_raw(leaf_count as f32) as usize + 2 * leaf_count + 1 + } else { + 0 + } + } + + pub fn new>(items: &[T], sorted_hashes: bool) -> Self { + let cap = Self::calculate_vec_capacity(items.len()); + let mut mt = Self { + leaf_count: items.len(), + nodes: Vec::with_capacity(cap), + }; + + for item in items { + let item = item.as_ref(); + let hash = hash_leaf!(item); + mt.nodes.push(hash); + } + + let mut level_len = Self::next_level_len(items.len()); + let mut level_start = items.len(); + let mut prev_level_len = items.len(); + let mut prev_level_start = 0; + while level_len > 0 { + for i in 0..level_len { + let prev_level_idx = 2 * i; + let lsib = &mt.nodes[prev_level_start + prev_level_idx]; + let rsib = if prev_level_idx + 1 < prev_level_len { + &mt.nodes[prev_level_start + prev_level_idx + 1] + } else { + // Duplicate last entry if the level length is odd + &mt.nodes[prev_level_start + prev_level_idx] + }; + + // tip-distribution verification uses sorted hashing + if sorted_hashes { + if lsib <= rsib { + let hash = hash_intermediate!(lsib, rsib); + mt.nodes.push(hash); + } else { + let hash = hash_intermediate!(rsib, lsib); + mt.nodes.push(hash); + } + } else { + // hashing for solana internals + let hash = hash_intermediate!(lsib, rsib); + mt.nodes.push(hash); + } + } + prev_level_start = level_start; + prev_level_len = level_len; + level_start += level_len; + level_len = Self::next_level_len(level_len); + } + + mt + } + + pub fn get_root(&self) -> Option<&Hash> { + self.nodes.iter().last() + } + + pub fn find_path(&self, index: usize) -> Option { + if index >= self.leaf_count { + return None; + } + + let mut level_len = self.leaf_count; + let mut level_start = 0; + let mut path = Proof::default(); + let mut node_index = index; + let mut lsib = None; + let mut rsib = None; + while level_len > 0 { + let level = &self.nodes[level_start..(level_start + level_len)]; + + let target = &level[node_index]; + if lsib.is_some() || rsib.is_some() { + path.push(ProofEntry::new(target, lsib, rsib)); + } + if node_index % 2 == 0 { + lsib = None; + rsib = if node_index + 1 < level.len() { + Some(&level[node_index + 1]) + } else { + Some(&level[node_index]) + }; + } else { + lsib = Some(&level[node_index - 1]); + rsib = None; + } + node_index /= 2; + + level_start += level_len; + level_len = Self::next_level_len(level_len); + } + Some(path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST: &[&[u8]] = &[ + b"my", b"very", b"eager", b"mother", b"just", b"served", b"us", b"nine", b"pizzas", + b"make", b"prime", + ]; + const BAD: &[&[u8]] = &[b"bad", b"missing", b"false"]; + + #[test] + fn test_tree_from_empty() { + let mt = MerkleTree::new::<[u8; 0]>(&[], false); + assert_eq!(mt.get_root(), None); + } + + #[test] + fn test_tree_from_one() { + let input = b"test"; + let mt = MerkleTree::new(&[input], false); + let expected = hash_leaf!(input); + assert_eq!(mt.get_root(), Some(&expected)); + } + + #[test] + fn test_tree_from_many() { + let mt = MerkleTree::new(TEST, false); + // This golden hash will need to be updated whenever the contents of `TEST` change in any + // way, including addition, removal and reordering or any of the tree calculation algo + // changes + let bytes = hex::decode("b40c847546fdceea166f927fc46c5ca33c3638236a36275c1346d3dffb84e1bc") + .unwrap(); + let expected = Hash::new(&bytes); + assert_eq!(mt.get_root(), Some(&expected)); + } + + #[test] + fn test_path_creation() { + let mt = MerkleTree::new(TEST, false); + for (i, _s) in TEST.iter().enumerate() { + let _path = mt.find_path(i).unwrap(); + } + } + + #[test] + fn test_path_creation_bad_index() { + let mt = MerkleTree::new(TEST, false); + assert_eq!(mt.find_path(TEST.len()), None); + } + + #[test] + fn test_path_verify_good() { + let mt = MerkleTree::new(TEST, false); + for (i, s) in TEST.iter().enumerate() { + let hash = hash_leaf!(s); + let path = mt.find_path(i).unwrap(); + assert!(path.verify(hash)); + } + } + + #[test] + fn test_path_verify_bad() { + let mt = MerkleTree::new(TEST, false); + for (i, s) in BAD.iter().enumerate() { + let hash = hash_leaf!(s); + let path = mt.find_path(i).unwrap(); + assert!(!path.verify(hash)); + } + } + + #[test] + fn test_proof_entry_instantiation_lsib_set() { + ProofEntry::new(&Hash::default(), Some(&Hash::default()), None); + } + + #[test] + fn test_proof_entry_instantiation_rsib_set() { + ProofEntry::new(&Hash::default(), None, Some(&Hash::default())); + } + + #[test] + fn test_nodes_capacity_compute() { + let iteration_count = |mut leaf_count: usize| -> usize { + let mut capacity = 0; + while leaf_count > 0 { + capacity += leaf_count; + leaf_count = MerkleTree::next_level_len(leaf_count); + } + capacity + }; + + // test max 64k leaf nodes compute + for leaf_count in 0..65536 { + let math_count = MerkleTree::calculate_vec_capacity(leaf_count); + let iter_count = iteration_count(leaf_count); + assert!(math_count >= iter_count); + } + } + + #[test] + #[should_panic] + fn test_proof_entry_instantiation_both_clear() { + ProofEntry::new(&Hash::default(), None, None); + } + + #[test] + #[should_panic] + fn test_proof_entry_instantiation_both_set() { + ProofEntry::new( + &Hash::default(), + Some(&Hash::default()), + Some(&Hash::default()), + ); + } +} diff --git a/meta_merkle_tree/src/meta_merkle_tree.rs b/meta_merkle_tree/src/meta_merkle_tree.rs new file mode 100644 index 00000000..05daf18d --- /dev/null +++ b/meta_merkle_tree/src/meta_merkle_tree.rs @@ -0,0 +1,293 @@ +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::{BufReader, Write}, + path::PathBuf, + result, +}; + +use serde::{Deserialize, Serialize}; +use solana_program::{hash::hashv, pubkey::Pubkey}; + +use crate::{ + error::MerkleTreeError::{self, MerkleValidationError}, + generated_merkle_tree::GeneratedMerkleTreeCollection, + merkle_tree::MerkleTree, + tree_node::TreeNode, + utils::get_proof, + verify::verify, +}; + +// We need to discern between leaf and intermediate nodes to prevent trivial second +// pre-image attacks. +// https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack +pub const LEAF_PREFIX: &[u8] = &[0]; + +/// Merkle Tree which will be used to set the merkle root for each tip distribution account. +/// Contains all the information necessary to verify claims against the Merkle Tree. +/// Wrapper around solana MerkleTree +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetaMerkleTree { + /// The merkle root, which is uploaded on-chain + pub merkle_root: [u8; 32], + pub num_nodes: u64, + pub tree_nodes: Vec, +} + +pub type Result = result::Result; + +impl MetaMerkleTree { + pub fn new(mut tree_nodes: Vec) -> Result { + // Sort by hash to ensure consistent trees + tree_nodes.sort_by_key(|node| node.hash()); + + let hashed_nodes = tree_nodes + .iter() + .map(|claim_info| claim_info.hash().to_bytes()) + .collect::>(); + + let tree = MerkleTree::new(&hashed_nodes[..], true); + + for (i, tree_node) in tree_nodes.iter_mut().enumerate() { + tree_node.proof = Some(get_proof(&tree, i)); + } + + let tree = Self { + merkle_root: tree + .get_root() + .ok_or(MerkleTreeError::MerkleRootError)? + .to_bytes(), + num_nodes: tree_nodes.len() as u64, + tree_nodes, + }; + + println!( + "created merkle tree with {} nodes and max total claim of {}", + tree.num_nodes, tree.num_nodes + ); + tree.validate()?; + Ok(tree) + } + + // TODO replace this with the GeneratedMerkleTreeCollection from the Operator module once that's created + pub fn new_from_generated_merkle_tree_collection( + generated_merkle_tree_collection: GeneratedMerkleTreeCollection, + ) -> Result { + let tree_nodes = generated_merkle_tree_collection + .generated_merkle_trees + .into_iter() + .map(TreeNode::from) + .collect(); + Self::new(tree_nodes) + } + + // TODO if we need to load this from a file (for operator?) + /// Load a merkle tree from a csv path + // pub fn new_from_csv(path: &PathBuf) -> Result { + // let csv_entries = CsvEntry::new_from_file(path)?; + // let tree_nodes: Vec = csv_entries.into_iter().map(TreeNode::from).collect(); + // let tree = Self::new(tree_nodes)?; + // Ok(tree) + // } + + /// Load a serialized merkle tree from file path + pub fn new_from_file(path: &PathBuf) -> Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let tree: Self = serde_json::from_reader(reader)?; + + Ok(tree) + } + + /// Write a merkle tree to a filepath + pub fn write_to_file(&self, path: &PathBuf) { + let serialized = serde_json::to_string_pretty(&self).unwrap(); + let mut file = File::create(path).unwrap(); + file.write_all(serialized.as_bytes()).unwrap(); + } + + pub fn get_node(&self, tip_distribution_account: &Pubkey) -> TreeNode { + for i in self.tree_nodes.iter() { + if i.tip_distribution_account == *tip_distribution_account { + return i.clone(); + } + } + + panic!("Claimant not found in tree"); + } + + fn validate(&self) -> Result<()> { + // The Merkle tree can be at most height 32, implying a max node count of 2^32 - 1 + let max_nodes = 2u64 + .checked_pow(32) + .and_then(|x| x.checked_sub(1)) + .ok_or(MerkleTreeError::ArithmeticOverflow)?; + if self.num_nodes > max_nodes { + return Err(MerkleValidationError(format!( + "Max num nodes {} is greater than 2^32 - 1", + self.num_nodes + ))); + } + + // validate that the length is equal to the max_num_nodes + if self.tree_nodes.len() != self.num_nodes as usize { + return Err(MerkleValidationError(format!( + "Tree nodes length {} does not match max_num_nodes {}", + self.tree_nodes.len(), + self.num_nodes + ))); + } + + // validate that there are no duplicate vote_accounts + let unique_nodes: HashSet<_> = self + .tree_nodes + .iter() + .map(|n| n.tip_distribution_account) + .collect(); + + if unique_nodes.len() != self.tree_nodes.len() { + return Err(MerkleValidationError( + "Duplicate vote_accounts found".to_string(), + )); + } + + if self.verify_proof().is_err() { + return Err(MerkleValidationError( + "Merkle root is invalid given nodes".to_string(), + )); + } + + Ok(()) + } + + /// verify that the leaves of the merkle tree match the nodes + pub fn verify_proof(&self) -> Result<()> { + let root = self.merkle_root; + + // Recreate root given nodes + let hashed_nodes: Vec<[u8; 32]> = self + .tree_nodes + .iter() + .map(|n| n.hash().to_bytes()) + .collect(); + let mk = MerkleTree::new(&hashed_nodes[..], true); + + assert_eq!( + mk.get_root() + .ok_or(MerkleValidationError("invalid merkle proof".to_string()))? + .to_bytes(), + root + ); + + // Verify each node against the root + for (i, _node) in hashed_nodes.iter().enumerate() { + let node = hashv(&[LEAF_PREFIX, &hashed_nodes[i]]); + let proof = get_proof(&mk, i); + + if !verify(proof, root, node.to_bytes()) { + return Err(MerkleValidationError("invalid merkle proof".to_string())); + } + } + + Ok(()) + } + + // Converts Merkle Tree to a map for faster key access + pub fn convert_to_hashmap(&self) -> HashMap { + self.tree_nodes + .iter() + .map(|n| (n.tip_distribution_account, n.clone())) + .collect() + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use solana_program::pubkey::Pubkey; + use solana_sdk::{ + signature::{EncodableKey, Keypair}, + signer::Signer, + }; + + use super::*; + + pub fn new_test_key() -> Pubkey { + let kp = Keypair::new(); + let out_path = format!("./test_keys/{}.json", kp.pubkey()); + + kp.write_to_file(out_path) + .expect("Failed to write to signer"); + + kp.pubkey() + } + + #[test] + fn test_verify_new_merkle_tree() { + let tree_nodes = vec![TreeNode::new(Pubkey::default(), [0; 32], 100, 10)]; + let merkle_tree = MetaMerkleTree::new(tree_nodes).unwrap(); + assert!(merkle_tree.verify_proof().is_ok(), "verify failed"); + } + + #[ignore] + #[test] + fn test_write_merkle_distributor_to_file() { + let tree_nodes = vec![ + TreeNode::new( + new_test_key(), + [0; 32], + 100 * u64::pow(10, 9), + 100 * u64::pow(10, 9), + ), + TreeNode::new( + new_test_key(), + [0; 32], + 100 * u64::pow(10, 9), + 100 * u64::pow(10, 9), + ), + TreeNode::new( + new_test_key(), + [0; 32], + 100 * u64::pow(10, 9), + 100 * u64::pow(10, 9), + ), + ]; + + let merkle_distributor_info = MetaMerkleTree::new(tree_nodes).unwrap(); + let path = PathBuf::from("merkle_tree.json"); + + // serialize merkle distributor to file + merkle_distributor_info.write_to_file(&path); + // now test we can successfully read from file + let merkle_distributor_read: MetaMerkleTree = MetaMerkleTree::new_from_file(&path).unwrap(); + + assert_eq!(merkle_distributor_read.tree_nodes.len(), 3); + } + + // Test creating a merkle tree from Tree Nodes + #[test] + fn test_new_merkle_tree() { + let pubkey1 = Pubkey::new_unique(); + let pubkey2 = Pubkey::new_unique(); + let pubkey3 = Pubkey::new_unique(); + + let mut tree_nodes = vec![ + TreeNode::new(pubkey1, [0; 32], 10, 20), + TreeNode::new(pubkey2, [0; 32], 1, 2), + TreeNode::new(pubkey3, [0; 32], 3, 4), + ]; + + // Sort by hash + tree_nodes.sort_by_key(|node| node.hash()); + + let tree = MetaMerkleTree::new(tree_nodes).unwrap(); + + assert_eq!(tree.tree_nodes.len(), 3); + assert_eq!(tree.tree_nodes[0].max_total_claim, 10); + assert_eq!(tree.tree_nodes[0].max_num_nodes, 20); + assert_eq!(tree.tree_nodes[0].validator_merkle_root, [0; 32]); + assert_eq!(tree.tree_nodes[0].tip_distribution_account, pubkey1); + assert!(tree.tree_nodes[0].proof.is_some()); + } +} diff --git a/meta_merkle_tree/src/tree_node.rs b/meta_merkle_tree/src/tree_node.rs new file mode 100644 index 00000000..209fc5be --- /dev/null +++ b/meta_merkle_tree/src/tree_node.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; +use solana_program::{ + hash::{hashv, Hash}, + pubkey::Pubkey, +}; + +use crate::generated_merkle_tree::GeneratedMerkleTree; + +/// Represents the information for activating a tip distribution account. +#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct TreeNode { + /// Pubkey of the vote account for setting the merkle root + pub tip_distribution_account: Pubkey, + /// Claimant's proof of inclusion in the Merkle Tree + pub proof: Option>, + /// Validator merkle root to be set for the tip distribution account + pub validator_merkle_root: [u8; 32], + /// Maximum total claimable for the tip distribution account + pub max_total_claim: u64, + /// Number of nodes to claim + pub max_num_nodes: u64, +} + +impl TreeNode { + pub const fn new( + tip_distribution_account: Pubkey, + validator_merkle_root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + ) -> Self { + Self { + tip_distribution_account, + proof: None, + validator_merkle_root, + max_total_claim, + max_num_nodes, + } + } + + pub fn hash(&self) -> Hash { + hashv(&[ + &self.tip_distribution_account.to_bytes(), + &self.validator_merkle_root, + &self.max_total_claim.to_le_bytes(), + &self.max_num_nodes.to_le_bytes(), + ]) + } +} + +// TODO replace this with the GeneratedMerkleTree from the Operator module once that's created +impl From for TreeNode { + fn from(generated_merkle_tree: GeneratedMerkleTree) -> Self { + Self { + tip_distribution_account: generated_merkle_tree.tip_distribution_account, + validator_merkle_root: generated_merkle_tree.merkle_root.to_bytes(), + max_total_claim: generated_merkle_tree.max_total_claim, + max_num_nodes: generated_merkle_tree.max_num_nodes, + proof: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize_tree_node() { + let tree_node = TreeNode { + tip_distribution_account: Pubkey::default(), + proof: None, + validator_merkle_root: [0; 32], + max_total_claim: 0, + max_num_nodes: 0, + }; + let serialized = serde_json::to_string(&tree_node).unwrap(); + let deserialized: TreeNode = serde_json::from_str(&serialized).unwrap(); + assert_eq!(tree_node, deserialized); + } +} diff --git a/meta_merkle_tree/src/utils.rs b/meta_merkle_tree/src/utils.rs new file mode 100644 index 00000000..713bae2a --- /dev/null +++ b/meta_merkle_tree/src/utils.rs @@ -0,0 +1,16 @@ +use crate::merkle_tree::MerkleTree; + +pub fn get_proof(merkle_tree: &MerkleTree, index: usize) -> Vec<[u8; 32]> { + let mut proof = Vec::new(); + let path = merkle_tree.find_path(index).expect("path to index"); + for branch in path.get_proof_entries() { + if let Some(hash) = branch.get_left_sibling() { + proof.push(hash.to_bytes()); + } else if let Some(hash) = branch.get_right_sibling() { + proof.push(hash.to_bytes()); + } else { + panic!("expected some hash at each level of the tree"); + } + } + proof +} diff --git a/meta_merkle_tree/src/verify.rs b/meta_merkle_tree/src/verify.rs new file mode 100644 index 00000000..1a86fdbc --- /dev/null +++ b/meta_merkle_tree/src/verify.rs @@ -0,0 +1,23 @@ +use solana_program::hash::hashv; + +/// modified version of https://github.com/saber-hq/merkle-distributor/blob/ac937d1901033ecb7fa3b0db22f7b39569c8e052/programs/merkle-distributor/src/merkle_proof.rs#L8 +/// This function deals with verification of Merkle trees (hash trees). +/// Direct port of https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/cryptography/MerkleProof.sol +/// Returns true if a `leaf` can be proved to be a part of a Merkle tree +/// defined by `root`. For this, a `proof` must be provided, containing +/// sibling hashes on the branch from the leaf to the root of the tree. Each +/// pair of leaves and each pair of pre-images are assumed to be sorted. +pub fn verify(proof: Vec<[u8; 32]>, root: [u8; 32], leaf: [u8; 32]) -> bool { + let mut computed_hash = leaf; + for proof_element in proof.into_iter() { + if computed_hash <= proof_element { + // Hash(current computed hash + current element of the proof) + computed_hash = hashv(&[&[1u8], &computed_hash, &proof_element]).to_bytes(); + } else { + // Hash(current element of the proof + current computed hash) + computed_hash = hashv(&[&[1u8], &proof_element, &computed_hash]).to_bytes(); + } + } + // Check if the computed hash (root) is equal to the provided root + computed_hash == root +} diff --git a/program/Cargo.toml b/program/Cargo.toml index 78f6afb0..295640ab 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -34,6 +34,7 @@ jito-jsm-core = { workspace = true } jito-restaking-core = { workspace = true } jito-restaking-program = { workspace = true } jito-restaking-sdk = { workspace = true } +jito-tip-distribution-sdk = { workspace = true } jito-tip-router-core = { workspace = true } jito-vault-core = { workspace = true } jito-vault-program = { workspace = true } diff --git a/program/src/cast_vote.rs b/program/src/cast_vote.rs new file mode 100644 index 00000000..0eabe979 --- /dev/null +++ b/program/src/cast_vote.rs @@ -0,0 +1,103 @@ +use jito_bytemuck::AccountDeserialize; +use jito_jsm_core::loader::load_signer; +use jito_restaking_core::{ncn::Ncn, operator::Operator}; +use jito_tip_router_core::{ + ballot_box::{Ballot, BallotBox}, + epoch_snapshot::{EpochSnapshot, OperatorSnapshot}, + error::TipRouterError, + ncn_config::NcnConfig, +}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, msg, + program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar, +}; + +pub fn process_cast_vote( + program_id: &Pubkey, + accounts: &[AccountInfo], + meta_merkle_root: [u8; 32], + epoch: u64, +) -> ProgramResult { + let [ncn_config, ballot_box, ncn, epoch_snapshot, operator_snapshot, operator, operator_admin, restaking_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Operator is casting the vote, needs to be signer + load_signer(operator_admin, false)?; + + NcnConfig::load(program_id, ncn.key, ncn_config, false)?; + Ncn::load(restaking_program.key, ncn, false)?; + Operator::load(restaking_program.key, operator, false)?; + + BallotBox::load(program_id, ncn.key, epoch, ballot_box, true)?; + EpochSnapshot::load(program_id, ncn.key, epoch, epoch_snapshot, false)?; + OperatorSnapshot::load( + program_id, + operator.key, + ncn.key, + epoch, + operator_snapshot, + false, + )?; + + let operator_data = operator.data.borrow(); + let operator_account = Operator::try_from_slice_unchecked(&operator_data)?; + + if *operator_admin.key != operator_account.admin { + return Err(TipRouterError::OperatorAdminInvalid.into()); + } + + let valid_slots_after_consensus = { + let ncn_config_data = ncn_config.data.borrow(); + let ncn_config = NcnConfig::try_from_slice_unchecked(&ncn_config_data)?; + ncn_config.valid_slots_after_consensus() + }; + + let mut ballot_box_data = ballot_box.data.borrow_mut(); + let ballot_box = BallotBox::try_from_slice_unchecked_mut(&mut ballot_box_data)?; + + let total_stake_weight = { + let epoch_snapshot_data = epoch_snapshot.data.borrow(); + let epoch_snapshot = EpochSnapshot::try_from_slice_unchecked(&epoch_snapshot_data)?; + + if !epoch_snapshot.finalized() { + return Err(TipRouterError::EpochSnapshotNotFinalized.into()); + } + + epoch_snapshot.stake_weight() + }; + + let operator_stake_weight = { + let operator_snapshot_data = operator_snapshot.data.borrow(); + let operator_snapshot = + OperatorSnapshot::try_from_slice_unchecked(&operator_snapshot_data)?; + + operator_snapshot.stake_weight() + }; + + let slot = Clock::get()?.slot; + + let ballot = Ballot::new(meta_merkle_root); + + ballot_box.cast_vote( + *operator.key, + ballot, + operator_stake_weight, + slot, + valid_slots_after_consensus, + )?; + + ballot_box.tally_votes(total_stake_weight, slot)?; + + if ballot_box.is_consensus_reached() { + msg!( + "Consensus reached for epoch {} with ballot {}", + epoch, + ballot_box.get_winning_ballot()? + ); + } + + Ok(()) +} diff --git a/program/src/initialize_ballot_box.rs b/program/src/initialize_ballot_box.rs new file mode 100644 index 00000000..550fe19c --- /dev/null +++ b/program/src/initialize_ballot_box.rs @@ -0,0 +1,56 @@ +use jito_bytemuck::{AccountDeserialize, Discriminator}; +use jito_jsm_core::{ + create_account, + loader::{load_signer, load_system_account, load_system_program}, +}; +use jito_tip_router_core::{ballot_box::BallotBox, ncn_config::NcnConfig}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, + program_error::ProgramError, pubkey::Pubkey, rent::Rent, sysvar::Sysvar, +}; + +pub fn process_initialize_ballot_box( + program_id: &Pubkey, + accounts: &[AccountInfo], + epoch: u64, +) -> ProgramResult { + let [ncn_config, ballot_box, ncn_account, payer, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Verify accounts + load_system_account(ballot_box, true)?; + load_system_program(system_program)?; + + load_signer(payer, false)?; + + NcnConfig::load(program_id, ncn_account.key, ncn_config, false)?; + + let (ballot_box_pda, ballot_box_bump, mut ballot_box_seeds) = + BallotBox::find_program_address(program_id, ncn_account.key, epoch); + ballot_box_seeds.push(vec![ballot_box_bump]); + + if ballot_box_pda != *ballot_box.key { + return Err(ProgramError::InvalidSeeds); + } + + create_account( + payer, + ballot_box, + system_program, + program_id, + &Rent::get()?, + 8_u64 + .checked_add(std::mem::size_of::() as u64) + .unwrap(), + &ballot_box_seeds, + )?; + + let mut ballot_box_data = ballot_box.try_borrow_mut_data()?; + ballot_box_data[0] = BallotBox::DISCRIMINATOR; + let ballot_box_account = BallotBox::try_from_slice_unchecked_mut(&mut ballot_box_data)?; + + ballot_box_account.initialize(*ncn_account.key, epoch, ballot_box_bump, Clock::get()?.slot); + + Ok(()) +} diff --git a/program/src/lib.rs b/program/src/lib.rs index df0b1446..a775a019 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,4 +1,6 @@ mod admin_update_weight_table; +mod cast_vote; +mod initialize_ballot_box; mod initialize_epoch_snapshot; mod initialize_ncn_config; mod initialize_operator_snapshot; @@ -6,7 +8,9 @@ mod initialize_tracked_mints; mod initialize_weight_table; mod register_mint; mod set_config_fees; +mod set_merkle_root; mod set_new_admin; +mod set_tie_breaker; mod snapshot_vault_operator_delegation; use borsh::BorshDeserialize; @@ -21,13 +25,15 @@ use solana_program::{ use solana_security_txt::security_txt; use crate::{ - admin_update_weight_table::process_admin_update_weight_table, + admin_update_weight_table::process_admin_update_weight_table, cast_vote::process_cast_vote, + initialize_ballot_box::process_initialize_ballot_box, initialize_epoch_snapshot::process_initialize_epoch_snapshot, initialize_ncn_config::process_initialize_ncn_config, initialize_operator_snapshot::process_initialize_operator_snapshot, initialize_tracked_mints::process_initialize_tracked_mints, initialize_weight_table::process_initialize_weight_table, register_mint::process_register_mint, - set_config_fees::process_set_config_fees, + set_config_fees::process_set_config_fees, set_merkle_root::process_set_merkle_root, + set_tie_breaker::process_set_tie_breaker, snapshot_vault_operator_delegation::process_snapshot_vault_operator_delegation, }; @@ -140,5 +146,41 @@ pub fn process_instruction( msg!("Instruction: InitializeTrackedMints"); process_initialize_tracked_mints(program_id, accounts) } + TipRouterInstruction::InitializeBallotBox { epoch } => { + msg!("Instruction: InitializeBallotBox"); + process_initialize_ballot_box(program_id, accounts, epoch) + } + TipRouterInstruction::CastVote { + meta_merkle_root, + epoch, + } => { + msg!("Instruction: CastVote"); + process_cast_vote(program_id, accounts, meta_merkle_root, epoch) + } + TipRouterInstruction::SetMerkleRoot { + proof, + merkle_root, + max_total_claim, + max_num_nodes, + epoch, + } => { + msg!("Instruction: SetMerkleRoot"); + process_set_merkle_root( + program_id, + accounts, + proof, + merkle_root, + max_total_claim, + max_num_nodes, + epoch, + ) + } + TipRouterInstruction::SetTieBreaker { + meta_merkle_root, + epoch, + } => { + msg!("Instruction: SetTieBreaker"); + process_set_tie_breaker(program_id, accounts, meta_merkle_root, epoch) + } } } diff --git a/program/src/set_merkle_root.rs b/program/src/set_merkle_root.rs new file mode 100644 index 00000000..8e5b7175 --- /dev/null +++ b/program/src/set_merkle_root.rs @@ -0,0 +1,83 @@ +use jito_bytemuck::AccountDeserialize; +use jito_restaking_core::ncn::Ncn; +use jito_tip_distribution_sdk::{ + derive_tip_distribution_account_address, instruction::upload_merkle_root_ix, +}; +use jito_tip_router_core::{ballot_box::BallotBox, error::TipRouterError, ncn_config::NcnConfig}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program::invoke_signed, + program_error::ProgramError, pubkey::Pubkey, +}; + +pub fn process_set_merkle_root( + program_id: &Pubkey, + accounts: &[AccountInfo], + proof: Vec<[u8; 32]>, + merkle_root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + epoch: u64, +) -> ProgramResult { + let [ncn_config, ncn, ballot_box, vote_account, tip_distribution_account, tip_distribution_config, tip_distribution_program_id, restaking_program_id] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + NcnConfig::load(program_id, ncn.key, ncn_config, true)?; + Ncn::load(restaking_program_id.key, ncn, false)?; + BallotBox::load(program_id, ncn.key, epoch, ballot_box, false)?; + + let (tip_distribution_address, _) = derive_tip_distribution_account_address( + tip_distribution_program_id.key, + vote_account.key, + epoch, + ); + + if tip_distribution_address.ne(tip_distribution_account.key) { + msg!("Incorrect tip distribution account"); + return Err(ProgramError::InvalidAccountData); + } + + let ballot_box_data = ballot_box.data.borrow(); + let ballot_box = BallotBox::try_from_slice_unchecked(&ballot_box_data)?; + + if !ballot_box.is_consensus_reached() { + msg!("Ballot box not finalized"); + return Err(TipRouterError::ConsensusNotReached.into()); + } + + ballot_box.verify_merkle_root( + tip_distribution_address, + proof, + merkle_root, + max_total_claim, + max_num_nodes, + )?; + + let (_, bump, mut ncn_config_seeds) = NcnConfig::find_program_address(program_id, ncn.key); + ncn_config_seeds.push(vec![bump]); + + invoke_signed( + &upload_merkle_root_ix( + *tip_distribution_config.key, + *ncn_config.key, + *tip_distribution_account.key, + merkle_root, + max_total_claim, + max_num_nodes, + ), + &[ + tip_distribution_config.clone(), + tip_distribution_account.clone(), + ncn_config.clone(), + ], + &[ncn_config_seeds + .iter() + .map(|s| s.as_slice()) + .collect::>() + .as_slice()], + )?; + + Ok(()) +} diff --git a/program/src/set_tie_breaker.rs b/program/src/set_tie_breaker.rs new file mode 100644 index 00000000..5eec5004 --- /dev/null +++ b/program/src/set_tie_breaker.rs @@ -0,0 +1,45 @@ +use jito_bytemuck::AccountDeserialize; +use jito_jsm_core::loader::load_signer; +use jito_restaking_core::ncn::Ncn; +use jito_tip_router_core::{ballot_box::BallotBox, error::TipRouterError, ncn_config::NcnConfig}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, msg, + program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar, +}; + +pub fn process_set_tie_breaker( + program_id: &Pubkey, + accounts: &[AccountInfo], + meta_merkle_root: [u8; 32], + ncn_epoch: u64, +) -> ProgramResult { + let [ncn_config, ballot_box, ncn, tie_breaker_admin, restaking_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + NcnConfig::load(program_id, ncn.key, ncn_config, false)?; + BallotBox::load(program_id, ncn.key, ncn_epoch, ballot_box, false)?; + Ncn::load(restaking_program.key, ncn, false)?; + load_signer(tie_breaker_admin, false)?; + + let ncn_config_data = ncn_config.data.borrow(); + let ncn_config = NcnConfig::try_from_slice_unchecked(&ncn_config_data)?; + + if ncn_config.tie_breaker_admin.ne(tie_breaker_admin.key) { + msg!("Tie breaker admin invalid"); + return Err(TipRouterError::TieBreakerAdminInvalid.into()); + } + + let mut ballot_box_data = ballot_box.data.borrow_mut(); + let ballot_box_account = BallotBox::try_from_slice_unchecked_mut(&mut ballot_box_data)?; + + let current_epoch = Clock::get()?.epoch; + + ballot_box_account.set_tie_breaker_ballot( + meta_merkle_root, + current_epoch, + ncn_config.epochs_before_stall(), + )?; + + Ok(()) +} diff --git a/tip_distribution_sdk/Cargo.toml b/tip_distribution_sdk/Cargo.toml new file mode 100644 index 00000000..0ba5171c --- /dev/null +++ b/tip_distribution_sdk/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jito-tip-distribution-sdk" +description = "SDK for interacting with Jito's Tip Distribution Program via declare_program" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +readme = { workspace = true } + +[dependencies] +anchor-lang = { workspace = true } diff --git a/tip_distribution_sdk/idls/jito_tip_distribution.json b/tip_distribution_sdk/idls/jito_tip_distribution.json new file mode 100644 index 00000000..403bccd3 --- /dev/null +++ b/tip_distribution_sdk/idls/jito_tip_distribution.json @@ -0,0 +1,981 @@ +{ + "address": "4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7", + "metadata": { + "name": "jito_tip_distribution", + "version": "0.1.5", + "spec": "0.1.0", + "description": "Tip distribution program, responsible for distributing funds to entitled parties." + }, + "instructions": [ + { + "name": "claim", + "docs": [ + "Claims tokens from the [TipDistributionAccount]." + ], + "discriminator": [ + 62, + 198, + 214, + 193, + 213, + 159, + 108, + 210 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "tip_distribution_account", + "writable": true + }, + { + "name": "claim_status", + "docs": [ + "Status of the claim. Used to prevent the same party from claiming multiple times." + ], + "writable": true + }, + { + "name": "claimant", + "docs": [ + "Receiver of the funds." + ], + "writable": true + }, + { + "name": "payer", + "docs": [ + "Who is paying for the claim." + ], + "writable": true, + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "proof", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + ] + }, + { + "name": "close_claim_status", + "docs": [ + "Anyone can invoke this only after the [TipDistributionAccount] has expired.", + "This instruction will return any rent back to `claimant` and close the account" + ], + "discriminator": [ + 163, + 214, + 191, + 165, + 245, + 188, + 17, + 185 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "claim_status", + "writable": true + }, + { + "name": "claim_status_payer", + "docs": [ + "Receiver of the funds." + ], + "writable": true + } + ], + "args": [] + }, + { + "name": "close_tip_distribution_account", + "docs": [ + "Anyone can invoke this only after the [TipDistributionAccount] has expired.", + "This instruction will send any unclaimed funds to the designated `expired_funds_account`", + "before closing and returning the rent exempt funds to the validator." + ], + "discriminator": [ + 47, + 136, + 208, + 190, + 125, + 243, + 74, + 227 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "expired_funds_account", + "writable": true + }, + { + "name": "tip_distribution_account", + "writable": true + }, + { + "name": "validator_vote_account", + "writable": true + }, + { + "name": "signer", + "docs": [ + "Anyone can crank this instruction." + ], + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "_epoch", + "type": "u64" + } + ] + }, + { + "name": "initialize", + "docs": [ + "Initialize a singleton instance of the [Config] account." + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "system_program" + }, + { + "name": "initializer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "expired_funds_account", + "type": "pubkey" + }, + { + "name": "num_epochs_valid", + "type": "u64" + }, + { + "name": "max_validator_commission_bps", + "type": "u16" + }, + { + "name": "bump", + "type": "u8" + } + ] + }, + { + "name": "initialize_tip_distribution_account", + "docs": [ + "Initialize a new [TipDistributionAccount] associated with the given validator vote key", + "and current epoch." + ], + "discriminator": [ + 120, + 191, + 25, + 182, + 111, + 49, + 179, + 55 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "tip_distribution_account", + "writable": true + }, + { + "name": "validator_vote_account", + "docs": [ + "The validator's vote account is used to check this transaction's signer is also the authorized withdrawer." + ] + }, + { + "name": "signer", + "docs": [ + "Must be equal to the supplied validator vote account's authorized withdrawer." + ], + "writable": true, + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [ + { + "name": "merkle_root_upload_authority", + "type": "pubkey" + }, + { + "name": "validator_commission_bps", + "type": "u16" + }, + { + "name": "bump", + "type": "u8" + } + ] + }, + { + "name": "update_config", + "docs": [ + "Update config fields. Only the [Config] authority can invoke this." + ], + "discriminator": [ + 29, + 158, + 252, + 191, + 10, + 83, + 219, + 99 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "authority", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "new_config", + "type": { + "defined": { + "name": "Config" + } + } + } + ] + }, + { + "name": "upload_merkle_root", + "docs": [ + "Uploads a merkle root to the provided [TipDistributionAccount]. This instruction may be", + "invoked many times as long as the account is at least one epoch old and not expired; and", + "no funds have already been claimed. Only the `merkle_root_upload_authority` has the", + "authority to invoke." + ], + "discriminator": [ + 70, + 3, + 110, + 29, + 199, + 190, + 205, + 176 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "tip_distribution_account", + "writable": true + }, + { + "name": "merkle_root_upload_authority", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "max_total_claim", + "type": "u64" + }, + { + "name": "max_num_nodes", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "ClaimStatus", + "discriminator": [ + 22, + 183, + 249, + 157, + 247, + 95, + 150, + 96 + ] + }, + { + "name": "Config", + "discriminator": [ + 155, + 12, + 170, + 224, + 30, + 250, + 204, + 130 + ] + }, + { + "name": "TipDistributionAccount", + "discriminator": [ + 85, + 64, + 113, + 198, + 234, + 94, + 120, + 123 + ] + } + ], + "events": [ + { + "name": "ClaimStatusClosedEvent", + "discriminator": [ + 188, + 143, + 237, + 229, + 192, + 182, + 164, + 118 + ] + }, + { + "name": "ClaimedEvent", + "discriminator": [ + 144, + 172, + 209, + 86, + 144, + 87, + 84, + 115 + ] + }, + { + "name": "ConfigUpdatedEvent", + "discriminator": [ + 245, + 158, + 129, + 99, + 60, + 100, + 214, + 220 + ] + }, + { + "name": "MerkleRootUploadAuthorityUpdatedEvent", + "discriminator": [ + 83, + 157, + 58, + 165, + 200, + 171, + 8, + 106 + ] + }, + { + "name": "MerkleRootUploadedEvent", + "discriminator": [ + 94, + 233, + 236, + 49, + 52, + 224, + 181, + 167 + ] + }, + { + "name": "TipDistributionAccountClosedEvent", + "discriminator": [ + 246, + 152, + 49, + 154, + 9, + 79, + 25, + 58 + ] + }, + { + "name": "TipDistributionAccountInitializedEvent", + "discriminator": [ + 39, + 165, + 224, + 61, + 40, + 140, + 139, + 255 + ] + }, + { + "name": "ValidatorCommissionBpsUpdatedEvent", + "discriminator": [ + 4, + 34, + 92, + 25, + 228, + 88, + 51, + 206 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "AccountValidationFailure", + "msg": "Account failed validation." + }, + { + "code": 6001, + "name": "ArithmeticError", + "msg": "Encountered an arithmetic under/overflow error." + }, + { + "code": 6002, + "name": "ExceedsMaxClaim", + "msg": "The maximum number of funds to be claimed has been exceeded." + }, + { + "code": 6003, + "name": "ExceedsMaxNumNodes", + "msg": "The maximum number of claims has been exceeded." + }, + { + "code": 6004, + "name": "ExpiredTipDistributionAccount", + "msg": "The given TipDistributionAccount has expired." + }, + { + "code": 6005, + "name": "FundsAlreadyClaimed", + "msg": "The funds for the given index and TipDistributionAccount have already been claimed." + }, + { + "code": 6006, + "name": "InvalidParameters", + "msg": "Supplied invalid parameters." + }, + { + "code": 6007, + "name": "InvalidProof", + "msg": "The given proof is invalid." + }, + { + "code": 6008, + "name": "InvalidVoteAccountData", + "msg": "Failed to deserialize the supplied vote account data." + }, + { + "code": 6009, + "name": "MaxValidatorCommissionFeeBpsExceeded", + "msg": "Validator's commission basis points must be less than or equal to the Config account's max_validator_commission_bps." + }, + { + "code": 6010, + "name": "PrematureCloseTipDistributionAccount", + "msg": "The given TipDistributionAccount is not ready to be closed." + }, + { + "code": 6011, + "name": "PrematureCloseClaimStatus", + "msg": "The given ClaimStatus account is not ready to be closed." + }, + { + "code": 6012, + "name": "PrematureMerkleRootUpload", + "msg": "Must wait till at least one epoch after the tip distribution account was created to upload the merkle root." + }, + { + "code": 6013, + "name": "RootNotUploaded", + "msg": "No merkle root has been uploaded to the given TipDistributionAccount." + }, + { + "code": 6014, + "name": "Unauthorized", + "msg": "Unauthorized signer." + } + ], + "types": [ + { + "name": "ClaimStatus", + "docs": [ + "Gives us an audit trail of who and what was claimed; also enforces and only-once claim by any party." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "is_claimed", + "docs": [ + "If true, the tokens have been claimed." + ], + "type": "bool" + }, + { + "name": "claimant", + "docs": [ + "Authority that claimed the tokens. Allows for delegated rewards claiming." + ], + "type": "pubkey" + }, + { + "name": "claim_status_payer", + "docs": [ + "The payer who created the claim." + ], + "type": "pubkey" + }, + { + "name": "slot_claimed_at", + "docs": [ + "When the funds were claimed." + ], + "type": "u64" + }, + { + "name": "amount", + "docs": [ + "Amount of funds claimed." + ], + "type": "u64" + }, + { + "name": "expires_at", + "docs": [ + "The epoch (upto and including) that tip funds can be claimed.", + "Copied since TDA can be closed, need to track to avoid making multiple claims" + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "The bump used to generate this account" + ], + "type": "u8" + } + ] + } + }, + { + "name": "ClaimStatusClosedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "claim_status_payer", + "docs": [ + "Account where funds were transferred to." + ], + "type": "pubkey" + }, + { + "name": "claim_status_account", + "docs": [ + "[ClaimStatus] account that was closed." + ], + "type": "pubkey" + } + ] + } + }, + { + "name": "ClaimedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tip_distribution_account", + "docs": [ + "[TipDistributionAccount] claimed from." + ], + "type": "pubkey" + }, + { + "name": "payer", + "docs": [ + "User that paid for the claim, may or may not be the same as claimant." + ], + "type": "pubkey" + }, + { + "name": "claimant", + "docs": [ + "Account that received the funds." + ], + "type": "pubkey" + }, + { + "name": "amount", + "docs": [ + "Amount of funds to distribute." + ], + "type": "u64" + } + ] + } + }, + { + "name": "Config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "Account with authority over this PDA." + ], + "type": "pubkey" + }, + { + "name": "expired_funds_account", + "docs": [ + "We want to expire funds after some time so that validators can be refunded the rent.", + "Expired funds will get transferred to this account." + ], + "type": "pubkey" + }, + { + "name": "num_epochs_valid", + "docs": [ + "Specifies the number of epochs a merkle root is valid for before expiring." + ], + "type": "u64" + }, + { + "name": "max_validator_commission_bps", + "docs": [ + "The maximum commission a validator can set on their distribution account." + ], + "type": "u16" + }, + { + "name": "bump", + "docs": [ + "The bump used to generate this account" + ], + "type": "u8" + } + ] + } + }, + { + "name": "ConfigUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "Who updated it." + ], + "type": "pubkey" + } + ] + } + }, + { + "name": "MerkleRoot", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "max_total_claim", + "docs": [ + "Maximum number of funds that can ever be claimed from this [MerkleRoot]." + ], + "type": "u64" + }, + { + "name": "max_num_nodes", + "docs": [ + "Maximum number of nodes that can ever be claimed from this [MerkleRoot]." + ], + "type": "u64" + }, + { + "name": "total_funds_claimed", + "docs": [ + "Total funds that have been claimed." + ], + "type": "u64" + }, + { + "name": "num_nodes_claimed", + "docs": [ + "Number of nodes that have been claimed." + ], + "type": "u64" + } + ] + } + }, + { + "name": "MerkleRootUploadAuthorityUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "old_authority", + "type": "pubkey" + }, + { + "name": "new_authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "MerkleRootUploadedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "merkle_root_upload_authority", + "docs": [ + "Who uploaded the root." + ], + "type": "pubkey" + }, + { + "name": "tip_distribution_account", + "docs": [ + "Where the root was uploaded to." + ], + "type": "pubkey" + } + ] + } + }, + { + "name": "TipDistributionAccount", + "docs": [ + "The account that validators register as **tip_receiver** with the tip-payment program." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_vote_account", + "docs": [ + "The validator's vote account, also the recipient of remaining lamports after", + "upon closing this account." + ], + "type": "pubkey" + }, + { + "name": "merkle_root_upload_authority", + "docs": [ + "The only account authorized to upload a merkle-root for this account." + ], + "type": "pubkey" + }, + { + "name": "merkle_root", + "docs": [ + "The merkle root used to verify user claims from this account." + ], + "type": { + "option": { + "defined": { + "name": "MerkleRoot" + } + } + } + }, + { + "name": "epoch_created_at", + "docs": [ + "Epoch for which this account was created." + ], + "type": "u64" + }, + { + "name": "validator_commission_bps", + "docs": [ + "The commission basis points this validator charges." + ], + "type": "u16" + }, + { + "name": "expires_at", + "docs": [ + "The epoch (upto and including) that tip funds can be claimed." + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "The bump used to generate this account" + ], + "type": "u8" + } + ] + } + }, + { + "name": "TipDistributionAccountClosedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "expired_funds_account", + "docs": [ + "Account where unclaimed funds were transferred to." + ], + "type": "pubkey" + }, + { + "name": "tip_distribution_account", + "docs": [ + "[TipDistributionAccount] closed." + ], + "type": "pubkey" + }, + { + "name": "expired_amount", + "docs": [ + "Unclaimed amount transferred." + ], + "type": "u64" + } + ] + } + }, + { + "name": "TipDistributionAccountInitializedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tip_distribution_account", + "type": "pubkey" + } + ] + } + }, + { + "name": "ValidatorCommissionBpsUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tip_distribution_account", + "type": "pubkey" + }, + { + "name": "old_commission_bps", + "type": "u16" + }, + { + "name": "new_commission_bps", + "type": "u16" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tip_distribution_sdk/src/instruction.rs b/tip_distribution_sdk/src/instruction.rs new file mode 100644 index 00000000..86df22a6 --- /dev/null +++ b/tip_distribution_sdk/src/instruction.rs @@ -0,0 +1,123 @@ +use anchor_lang::{ + prelude::Pubkey, solana_program::instruction::Instruction, InstructionData, ToAccountMetas, +}; + +use crate::jito_tip_distribution; + +#[allow(clippy::too_many_arguments)] +pub fn initialize_ix( + config: Pubkey, + system_program: Pubkey, + initializer: Pubkey, + authority: Pubkey, + expired_funds_account: Pubkey, + num_epochs_valid: u64, + max_validator_commission_bps: u16, + bump: u8, +) -> Instruction { + Instruction { + program_id: jito_tip_distribution::ID, + accounts: jito_tip_distribution::client::accounts::Initialize { + config, + system_program, + initializer, + } + .to_account_metas(None), + data: jito_tip_distribution::client::args::Initialize { + authority, + expired_funds_account, + num_epochs_valid, + max_validator_commission_bps, + bump, + } + .data(), + } +} + +#[allow(clippy::too_many_arguments)] +pub fn initialize_tip_distribution_account_ix( + config: Pubkey, + tip_distribution_account: Pubkey, + system_program: Pubkey, + validator_vote_account: Pubkey, + signer: Pubkey, + merkle_root_upload_authority: Pubkey, + validator_commission_bps: u16, + bump: u8, +) -> Instruction { + Instruction { + program_id: jito_tip_distribution::ID, + accounts: jito_tip_distribution::client::accounts::InitializeTipDistributionAccount { + config, + tip_distribution_account, + system_program, + validator_vote_account, + signer, + } + .to_account_metas(None), + data: jito_tip_distribution::client::args::InitializeTipDistributionAccount { + merkle_root_upload_authority, + validator_commission_bps, + bump, + } + .data(), + } +} + +#[allow(clippy::too_many_arguments)] +pub fn claim_ix( + config: Pubkey, + tip_distribution_account: Pubkey, + claim_status: Pubkey, + claimant: Pubkey, + payer: Pubkey, + system_program: Pubkey, + proof: Vec<[u8; 32]>, + amount: u64, + bump: u8, +) -> Instruction { + Instruction { + program_id: jito_tip_distribution::ID, + accounts: jito_tip_distribution::client::accounts::Claim { + config, + tip_distribution_account, + claim_status, + claimant, + payer, + system_program, + } + .to_account_metas(None), + data: jito_tip_distribution::client::args::Claim { + proof, + amount, + bump, + } + .data(), + } +} + +#[allow(clippy::too_many_arguments)] +pub fn upload_merkle_root_ix( + config: Pubkey, + merkle_root_upload_authority: Pubkey, + tip_distribution_account: Pubkey, + root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, +) -> Instruction { + Instruction { + program_id: jito_tip_distribution::ID, + accounts: jito_tip_distribution::client::accounts::UploadMerkleRoot { + config, + merkle_root_upload_authority, + tip_distribution_account, + } + .to_account_metas(None), + data: jito_tip_distribution::client::args::UploadMerkleRoot { + root, + max_total_claim, + max_num_nodes, + } + .data(), + } +} diff --git a/tip_distribution_sdk/src/lib.rs b/tip_distribution_sdk/src/lib.rs new file mode 100644 index 00000000..4f00ca80 --- /dev/null +++ b/tip_distribution_sdk/src/lib.rs @@ -0,0 +1,45 @@ +#![allow(clippy::redundant_pub_crate)] +use anchor_lang::{declare_program, prelude::Pubkey, solana_program::clock::Epoch}; + +declare_program!(jito_tip_distribution); +pub use jito_tip_distribution::accounts::TipDistributionAccount; + +pub mod instruction; + +pub const CONFIG_SEED: &[u8] = b"CONFIG_ACCOUNT"; +pub const CLAIM_STATUS_SEED: &[u8] = b"CLAIM_STATUS_ACCOUNT"; +pub const TIP_DISTRIBUTION_SEED: &[u8] = b"TIP_DISTRIBUTION_ACCOUNT"; + +pub fn derive_tip_distribution_account_address( + tip_distribution_program_id: &Pubkey, + vote_pubkey: &Pubkey, + epoch: Epoch, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[ + TIP_DISTRIBUTION_SEED, + vote_pubkey.to_bytes().as_ref(), + epoch.to_le_bytes().as_ref(), + ], + tip_distribution_program_id, + ) +} + +pub fn derive_config_account_address(tip_distribution_program_id: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[CONFIG_SEED], tip_distribution_program_id) +} + +pub fn derive_claim_status_account_address( + tip_distribution_program_id: &Pubkey, + claimant: &Pubkey, + tip_distribution_account: &Pubkey, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[ + CLAIM_STATUS_SEED, + claimant.to_bytes().as_ref(), + tip_distribution_account.to_bytes().as_ref(), + ], + tip_distribution_program_id, + ) +}