Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute budget reimbursement + configuration #430

Merged
merged 14 commits into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"dependencies": {
"@chainlink/solana-sdk": "../ts",
"@project-serum/anchor": "^0.25.0",
"@solana/spl-token": "^0.2.0",
"@solana/spl-token": "^0.3.5",
"@solana/web3.js": "^1.50.1",
"@types/chai": "^4.2.22",
"@types/mocha": "^9.0.0",
Expand Down
3 changes: 3 additions & 0 deletions contracts/programs/ocr2/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ pub struct Transmit<'info> {
pub store_program: Program<'info, Store>,
/// CHECK: PDA from the aggregator state, validated by the store program
pub store_authority: AccountInfo<'info>,
// TODO: stop using remaining_accounts once nodes update
// /// CHECK: Validated by sysvar::instructions::load_current_index_checked/load_instruction_at_checked
// pub instructions: AccountInfo<'info>,
}

#[derive(Accounts)]
Expand Down
71 changes: 67 additions & 4 deletions contracts/programs/ocr2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,11 @@ fn transmit_impl<'info>(ctx: Context<Transmit<'info>>, data: &[u8]) -> Result<()
state.config.latest_transmitter = ctx.accounts.transmitter.key();

// calculate and pay reimbursement
let reimbursement_gjuels =
calculate_reimbursement_gjuels(report.juels_per_lamport, signature_count)?; // gjuels
let reimbursement_gjuels = calculate_reimbursement_gjuels(
report.juels_per_lamport,
signature_count,
ctx.remaining_accounts.first(),
)?;
let amount_gjuels = reimbursement_gjuels
.saturating_add(u64::from(state.config.billing.transmission_payment_gjuels));
state.oracles[oracle_idx].payment_gjuels = state.oracles[oracle_idx]
Expand Down Expand Up @@ -800,18 +803,78 @@ impl Report {
}
}

fn calculate_reimbursement_gjuels(juels_per_lamport: u64, _signature_count: usize) -> Result<u64> {
fn calculate_reimbursement_gjuels(
juels_per_lamport: u64,
signature_count: usize,
instructions_sysvar: Option<&AccountInfo<'_>>,
) -> Result<u64> {
const SIGNERS: u64 = 1;
const MICRO: u64 = 10u64.pow(6);
const GIGA: u128 = 10u128.pow(9);
const LAMPORTS_PER_SIGNATURE: u64 = 5_000; // constant, originally retrieved from deprecated sysvar fees
let lamports = LAMPORTS_PER_SIGNATURE * SIGNERS;
let mut lamports = LAMPORTS_PER_SIGNATURE * SIGNERS;

// if a compute unit price is set then we also need to
if let Some(sysvar) = instructions_sysvar {
if let Some(compute_unit_price_micro_lamports) = compute_unit_price(sysvar)? {
// 25k per signature, plus ~21k for the rest
let exec_units = 25_000 * signature_count as u64 + 21_000;
// TODO: std::cmp::min(compute_unit_price_micro_lamports, config.max_compute_unit_price)

// https://github.com/solana-labs/solana/blob/090e11210aa7222d8295610a6ccac4acda711bb9/program-runtime/src/prioritization_fee.rs#L34-L38
let micro_lamport_fee = exec_units.saturating_mul(compute_unit_price_micro_lamports);
let fee = micro_lamport_fee
.saturating_add(MICRO.saturating_sub(1))
.saturating_div(MICRO);

lamports = lamports.saturating_add(fee);
};
}

let juels = u128::from(lamports) * u128::from(juels_per_lamport);
let gjuels = juels / GIGA; // return value as gjuels

// convert from u128 to u64 with staturating logic to max u64
Ok(gjuels.try_into().unwrap_or(u64::MAX))
}

pub mod compute_budget {
crate::declare_id!("ComputeBudget111111111111111111111111111111");
}

fn compute_unit_price(instruction_sysvar: &AccountInfo<'_>) -> Result<Option<u64>> {
use anchor_lang::solana_program::sysvar;
// look at sysvar instructions
let current_instruction = sysvar::instructions::load_current_index_checked(instruction_sysvar)?;

// if we can't find unit price return None and skip this part of calc
if current_instruction == 0 {
return Ok(None);
}

// find ComputeBudgetInstruction::SetComputeUnitPrice()
let compute_budget_ix = sysvar::instructions::load_instruction_at_checked(
(current_instruction as usize) - 1,
instruction_sysvar,
)?;

require!(
compute_budget_ix.program_id == compute_budget::ID,
InvalidInput
);

require!(compute_budget_ix.data[0] == 3, InvalidInput); // SetComputeUnitPrice index

// u8, u64
require!(compute_budget_ix.data.len() == 9, InvalidInput); // <--
let unit_price_micro_lamports =
u64::from_le_bytes(compute_budget_ix.data[1..1 + 8].try_into().unwrap());

// parse out execution unit price

Ok(Some(unit_price_micro_lamports))
}

fn calculate_owed_payment_gjuels(
config: &Billing,
oracle: &Oracle,
Expand Down
83 changes: 76 additions & 7 deletions contracts/tests/ocr2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Keypair,
Transaction,
TransactionInstruction,
SYSVAR_INSTRUCTIONS_PUBKEY,
ComputeBudgetProgram,
} from "@solana/web3.js";
import {
createMint,
Expand Down Expand Up @@ -130,6 +132,7 @@ describe("ocr2", async () => {
epoch: number,
round: number,
answer: BN,
computeUnitPrice: number = 0,
juels: Buffer = Buffer.from([0, 0, 0, 0, 0, 0, 0, 2]) // juels per lamport (2)
): Promise<string> => {
let account = await program.account.state.fetch(state.publicKey);
Expand Down Expand Up @@ -206,6 +209,9 @@ describe("ocr2", async () => {
const transmitter = oracles[0].transmitter;

const tx = new Transaction();
if (computeUnitPrice > 0) {
tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: computeUnitPrice }))
}
tx.add(
new TransactionInstruction({
programId: anchor.translateAddress(program.programId),
Expand All @@ -223,6 +229,7 @@ describe("ocr2", async () => {
isSigner: false,
},
{ pubkey: storeAuthority, isWritable: false, isSigner: false },
{ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isWritable: false, isSigner: false },
],
data: Buffer.concat([
Buffer.from([storeNonce]),
Expand All @@ -247,13 +254,14 @@ describe("ocr2", async () => {
};

it("Funds the payer", async () => {
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(
fromWallet.publicKey,
LAMPORTS_PER_SOL
);
LAMPORTS_PER_SOL * 1000
), "confirmed");

await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(payer.publicKey, 10000000000),
await provider.connection.requestAirdrop(payer.publicKey, LAMPORTS_PER_SOL * 1000),
"confirmed"
);
});
Expand Down Expand Up @@ -861,6 +869,7 @@ describe("ocr2", async () => {
},
],
]);

it("Can call query", async () => {
let round = await query(
feed.publicKey,
Expand Down Expand Up @@ -981,22 +990,81 @@ describe("ocr2", async () => {
}
});

it("Transmit reimburses for setComputeUnitPrice", async () => {
const transmitter = oracles[0].transmitter.publicKey;

let account = await program.account.state.fetch(state.publicKey);
let currentOracles = account.oracles.xs.slice(0, account.oracles.len);
// get the transmitter
let i = currentOracles.findIndex((o) => o.transmitter.equals(transmitter));
let oracle = currentOracles[i];
console.log(oracle);
let before = oracle.paymentGjuels;

let computePriceMicroLamports = 10;
let juelsPerLamport = Buffer.from([0, 0, 0, 0, 59, 154, 202, 0]); // 1 gjuel (to_be_bytes)
let tx = await transmit(
feed.publicKey,
rounds + 1,
rounds + 1,
new BN(rounds + 2),
computePriceMicroLamports,
juelsPerLamport,
);
await provider.connection.confirmTransaction(tx);
let t = await provider.connection.getTransaction(tx, { commitment: "confirmed" });
console.log(t.meta.logMessages); // 195_785/195_997
account = await program.account.state.fetch(state.publicKey, 'confirmed');
currentOracles = account.oracles.xs.slice(0, account.oracles.len);
// get the transmitter
oracle = currentOracles[i];
let after = oracle.paymentGjuels;

let payment = after.sub(before).toNumber();
let execUnits = (25_000 * (f+1) + 21_000);
let computeUnitCostLamports = execUnits * computePriceMicroLamports / Math.pow(10, 6);
let lamportsPerSignature = 5000;
let signers = 1;
let computeUnitCostGjuels = Math.ceil((computeUnitCostLamports + lamportsPerSignature * signers) * 1); // 1 lamport = 1 gjuel
assert.equal(payment, transmissionPayment + computeUnitCostGjuels);

// payOracles to clean up the payment state for the next test
let payees = currentOracles.map((oracle) => {
return { pubkey: oracle.payee, isWritable: true, isSigner: false };
});
tx = await program.rpc.payOracles({
accounts: {
state: state.publicKey,
authority: owner.publicKey,
accessController: billingAccessController.publicKey,
tokenReceiver: recipientTokenAccount.address,
tokenVault: tokenVault,
vaultAuthority: vaultAuthority,
tokenProgram: TOKEN_PROGRAM_ID,
},
remainingAccounts: payees,
});
await provider.connection.confirmTransaction(tx);
});

it("Transmit does not fail on juelsPerFeecoin edge cases", async () => {
// zero value u64 juelsPerFeecoin
await transmit(
feed.publicKey,
rounds + 1,
rounds + 1,
rounds + 2,
rounds + 2,
new BN(rounds + 1),
0,
Buffer.from([0, 0, 0, 0, 0, 0, 0, 0])
);

// max value u64 juelsPerFeecoin
await transmit(
feed.publicKey,
rounds + 2,
rounds + 2,
rounds + 3,
rounds + 3,
new BN(rounds + 2),
0,
Buffer.from([127, 127, 127, 127, 127, 127, 127, 127])
);
});
Expand Down Expand Up @@ -1216,4 +1284,5 @@ describe("ocr2", async () => {
);
assert.equal(new BN(round.answer, 10, "le").toNumber(), 100);
});

});
Loading