diff --git a/contracts/programs/ocr2/src/lib.rs b/contracts/programs/ocr2/src/lib.rs index 5c90f10cb..9f995a404 100644 --- a/contracts/programs/ocr2/src/lib.rs +++ b/contracts/programs/ocr2/src/lib.rs @@ -116,6 +116,7 @@ pub mod ocr2 { offchain_config.len() < config.pending_offchain_config.remaining_capacity(), InvalidInput ); + require!(config.pending_offchain_config.version != 0, InvalidInput); config.pending_offchain_config.extend(&offchain_config); Ok(()) } @@ -147,6 +148,24 @@ pub mod ocr2 { Ok(()) } + #[access_control(owner(&ctx.accounts.state, &ctx.accounts.authority))] + pub fn reset_pending_offchain_config(ctx: Context) -> ProgramResult { + let state = &mut *ctx.accounts.state.load_mut()?; + let config = &mut state.config; + + // Require that at least some data was written + require!( + config.pending_offchain_config.version > 0 + || !config.pending_offchain_config.is_empty(), + InvalidInput + ); + + // reset staging area + config.pending_offchain_config.clear(); + config.pending_offchain_config.version = 0; + Ok(()) + } + #[access_control(owner(&ctx.accounts.state, &ctx.accounts.authority))] pub fn set_config( ctx: Context, diff --git a/contracts/tests/ocr2.spec.ts b/contracts/tests/ocr2.spec.ts index 1ea600ed5..99710b0d2 100644 --- a/contracts/tests/ocr2.spec.ts +++ b/contracts/tests/ocr2.spec.ts @@ -436,19 +436,88 @@ describe('ocr2', async () => { authority: owner.publicKey, }, }); - assert.fail("beginOffchainConfig shouldn't have succeeded!") } catch { // beginOffchainConfig should fail + return } + assert.fail("beginOffchainConfig shouldn't have succeeded!") + }); + + it("Can't write offchain config if begin has not been called", async () => { + try { + await program.rpc.writeOffchainConfig( + Buffer.from([4, 5, 6]), + { + accounts: { + state: state.publicKey, + authority: owner.publicKey, + }, + }); + } catch { + // writeOffchainConfig should fail + return + } + assert.fail("writeOffchainConfig shouldn't have succeeded!") + }); + + it("ResetPendingOffchainConfig clears pending state", async () => { + + await program.rpc.beginOffchainConfig( + new BN(2), + { + accounts: { + state: state.publicKey, + authority: owner.publicKey, + }, + }); + await program.rpc.writeOffchainConfig( + Buffer.from([4, 5, 6]), + { + accounts: { + state: state.publicKey, + authority: owner.publicKey, + }, + }); + let account = await program.account.state.fetch(state.publicKey); + assert.ok(account.config.pendingOffchainConfig.version != 0); + assert.ok(account.config.pendingOffchainConfig.len != 0); + + await program.rpc.resetPendingOffchainConfig( + { + accounts: { + state: state.publicKey, + authority: owner.publicKey, + }, + }); + account = await program.account.state.fetch(state.publicKey); + assert.ok(account.config.pendingOffchainConfig.version == 0); + assert.ok(account.config.pendingOffchainConfig.len == 0); + }) + + it("Can't reset pending config if already in new state", async () => { + try { + await program.rpc.resetPendingOffchainConfig( + { + accounts: { + state: state.publicKey, + authority: owner.publicKey, + }, + }); + } catch { + // resetPendingOffchainConfig should fail + return + } + assert.fail("resetPendingOffchainConfig shouldn't have succeeded!") }); it("Can't transmit a round if not the writer", async () => { try { await transmit(1, 1, new BN(1)); - assert.fail("transmit() shouldn't have succeeded!"); } catch { // transmit should fail + return } + assert.fail("transmit() shouldn't have succeeded!"); }); it('Sets the cluster as the feed writer', async () => { diff --git a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json index e51f86962..0e6c4d8ef 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json +++ b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json @@ -211,6 +211,22 @@ ], "args": [] }, + { + "name": "resetPendingOffchainConfig", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "setConfig", "accounts": [ diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/index.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/index.ts index d98b6cbb1..82e4d6ee9 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/index.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/index.ts @@ -4,6 +4,7 @@ import BeginOffchainConfig from './offchainConfig/begin' import CommitOffchainConfig from './offchainConfig/commit' import SetOffchainConfigFlow from './offchainConfig/setOffchainConfig.flow' import WriteOffchainConfig from './offchainConfig/write' +import ResetPendingOffchainConfig from './offchainConfig/resetPending' import PayRemaining from './payRemaining' import ReadState from './read' import SetBillingAccessController from './setBillingAccessController' @@ -28,6 +29,7 @@ export default [ BeginOffchainConfig, WriteOffchainConfig, CommitOffchainConfig, + ResetPendingOffchainConfig, SetBillingAccessController, SetRequesterAccessController, // Inspection diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts new file mode 100644 index 000000000..2ced9e29f --- /dev/null +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts @@ -0,0 +1,53 @@ +import { Result } from '@chainlink/gauntlet-core' +import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' +import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' +import { PublicKey } from '@solana/web3.js' +import { CONTRACT_LIST, getContract } from '../../../../lib/contracts' + +export default class ResetPendingOffchainConfig extends SolanaCommand { + static id = 'ocr2:reset_pending_offchain_config' + static category = CONTRACT_LIST.OCR_2 + + static examples = ['yarn gauntlet ocr2:reset_pending_offchain_config --network=devnet --state=[OCR2_STATE]'] + constructor(flags, args) { + super(flags, args) + + this.require(!!this.flags.state, 'Please provide flags with "state"') + } + + execute = async () => { + const ocr2 = getContract(CONTRACT_LIST.OCR_2, '') + const address = ocr2.programId.toString() + const program = this.loadProgram(ocr2.idl, address) + + const state = new PublicKey(this.flags.state) + const owner = this.wallet.payer + + const info = await program.account.state.fetch(state) + console.log(info.config.pendingOffchainConfig) + this.require( + info.config.pendingOffchainConfig.version != 0 || info.config.pendingOffchainConfig.len != 0, + 'pending offchain config version is already in reset state', + ) + + await prompt(`Reset pending offchain config?`) + + const tx = await program.rpc.resetPendingOffchainConfig({ + accounts: { + state: state, + authority: owner.publicKey, + }, + }) + + logger.success(`Reset pending offchain config on tx ${tx}`) + + return { + responses: [ + { + tx: this.wrapResponse(tx, state.toString(), { state: state.toString() }), + contract: state.toString(), + }, + ], + } as Result + } +}