Skip to content

Commit

Permalink
Gauntlet validation checks (#54)
Browse files Browse the repository at this point in the history
* Ignore falsy values in proto encoding

* null encoding reference source in code

* config and payees validation check

* validation on offchain config
  • Loading branch information
RodrigoAD authored and krebernisak committed Dec 20, 2021
1 parent e2b62c3 commit 4708ee8
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Result, utils } from '@chainlink/gauntlet-core'
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 { MAX_TRANSACTION_BYTES } from '../../../../lib/constants'
import { MAX_TRANSACTION_BYTES, ORACLES_MAX_LENGTH } from '../../../../lib/constants'
import { CONTRACT_LIST, getContract } from '../../../../lib/contracts'
import { Protobuf } from '../../../../core/proto'
import { descriptor as OCR2Descriptor } from '../../../../core/proto/ocr2Proto'
import { getRDD } from '../../../../lib/rdd'
import { makeSharedSecretEncryptions, SharedSecretEncryptions } from '../../../../core/sharedSecretEncryptions'
import { durationToNanoseconds } from '../../../../core/time'
import { durationToNanoseconds, Millisecond } from '../../../../core/time'
import { divideIntoChunks } from '../../../../core/utils'
import BN from 'bn.js'

export type Input = {
deltaProgressNanoseconds: number
Expand Down Expand Up @@ -124,6 +125,59 @@ export default class WriteOffchainConfig extends SolanaCommand {
return makeSharedSecretEncryptions(gauntletSecret!, operatorsPublicKeys)
}

validateInput = (input: Input): boolean => {
const _isNegative = (v: number): boolean => new BN(v).lt(new BN(0))
const nonNegativeValues = [
'deltaProgressNanoseconds',
'deltaResendNanoseconds',
'deltaRoundNanoseconds',
'deltaGraceNanoseconds',
'deltaStageNanoseconds',
'maxDurationQueryNanoseconds',
'maxDurationObservationNanoseconds',
'maxDurationReportNanoseconds',
'maxDurationShouldAcceptFinalizedReportNanoseconds',
'maxDurationShouldTransmitAcceptedReportNanoseconds',
]
for (let prop in nonNegativeValues) {
if (_isNegative(input[prop])) throw new Error(`${prop} must be non-negative`)
}
const safeIntervalNanoseconds = new BN(200).mul(Millisecond).toNumber()
if (input.deltaProgressNanoseconds < safeIntervalNanoseconds)
throw new Error(
`deltaProgressNanoseconds (${input.deltaProgressNanoseconds} ns) is set below the resource exhaustion safe interval (${safeIntervalNanoseconds} ns)`,
)
if (input.deltaResendNanoseconds < safeIntervalNanoseconds)
throw new Error(
`deltaResendNanoseconds (${input.deltaResendNanoseconds} ns) is set below the resource exhaustion safe interval (${safeIntervalNanoseconds} ns)`,
)

if (input.deltaRoundNanoseconds < input.deltaProgressNanoseconds)
throw new Error(
`deltaRoundNanoseconds (${input.deltaRoundNanoseconds}) must be less than deltaProgressNanoseconds (${input.deltaProgressNanoseconds})`,
)
const sumMaxDurationsReportGeneration = new BN(input.maxDurationQueryNanoseconds)
.add(new BN(input.maxDurationObservationNanoseconds))
.add(new BN(input.maxDurationReportNanoseconds))

if (sumMaxDurationsReportGeneration.gte(new BN(input.deltaProgressNanoseconds)))
throw new Error(
`sum of MaxDurationQuery/Observation/Report (${sumMaxDurationsReportGeneration}) must be less than deltaProgressNanoseconds (${input.deltaProgressNanoseconds})`,
)

if (input.rMax <= 0 || input.rMax >= 255)
throw new Error(`rMax (${input.rMax}) must be greater than zero and less than 255`)

if (input.s.length >= 1000) throw new Error(`Length of S (${input.s.length}) must be less than 1000`)
for (let i = 0; i < input.s.length; i++) {
const s = input.s[i]
if (s < 0 || s > ORACLES_MAX_LENGTH)
throw new Error(`S[${i}] (${s}) must be between 0 and Max Oracles (${ORACLES_MAX_LENGTH})`)
}

return true
}

execute = async () => {
const ocr2 = getContract(CONTRACT_LIST.OCR_2, '')
const address = ocr2.programId.toString()
Expand All @@ -132,9 +186,10 @@ export default class WriteOffchainConfig extends SolanaCommand {
const state = new PublicKey(this.flags.state)
const owner = this.wallet.payer

// Throws on invalid input
const input = this.makeInput(this.flags.input)
// TODO: Add validation https://github.com/smartcontractkit/offchain-reporting/blob/master/lib/offchainreporting2/internal/config/public_config.go#L248
// MORE: https://github.com/smartcontractkit/offchain-reporting/blob/master/lib/offchainreporting2/internal/config/public_config.go#L152
this.validateInput(input)

// Check correct format OCR Keys
const offchainConfig = await this.serializeOffchainConfig(input)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { logger } from '@chainlink/gauntlet-core/dist/utils'
import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana'
import { PublicKey } from '@solana/web3.js'
import BN from 'bn.js'
import { ORACLES_MAX_LENGTH } from '../../../lib/constants'
import { CONTRACT_LIST, getContract } from '../../../lib/contracts'
import { getRDD } from '../../../lib/rdd'

Expand Down Expand Up @@ -57,16 +58,19 @@ export default class SetConfig extends SolanaCommand {

console.log(`Setting config on ${state.toString()}...`)

// TODO: Check valid keys
const oracles = input.oracles.map(({ signer, transmitter }) => ({
signer: Buffer.from(signer, 'hex'),
transmitter: new PublicKey(transmitter),
}))
const f = new BN(input.f)

// Must be = oracles.length > 3 * threshold
// TODO: Check here too
// MAX oracles = 19
const minOracleLength = f.mul(new BN(3)).toNumber()
this.require(oracles.length > minOracleLength, `Number of oracles should be higher than ${minOracleLength}`)
this.require(
oracles.length <= ORACLES_MAX_LENGTH,
`Oracles max length is ${ORACLES_MAX_LENGTH}, currently ${oracles.length}`,
)

const tx = await program.rpc.setConfig(oracles, f, {
accounts: {
state: state,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Result } from '@chainlink/gauntlet-core'
import { logger } from '@chainlink/gauntlet-core/dist/utils'
import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana'
import { Token, TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { PublicKey } from '@solana/web3.js'
import { CONTRACT_LIST, getContract } from '../../../lib/contracts'
import { getRDD } from '../../../lib/rdd'
Expand All @@ -10,6 +11,8 @@ type Input = {
transmitter: string
payee: string
}[]
// Allows to set payees that do not have a token generated address
allowFundRecipient?: boolean
}

export default class SetPayees extends SolanaCommand {
Expand All @@ -26,12 +29,13 @@ export default class SetPayees extends SolanaCommand {
const aggregator = rdd.contracts[this.flags.state]
const aggregatorOperators: string[] = aggregator.oracles.map((o) => o.operator)
const operators = aggregatorOperators.map((operator) => ({
// Check this too RDD
// TODO: Update to latest RDD
transmitter: rdd.operators[operator].nodeAddress[0],
payee: rdd.operators[operator].payeeAddress,
}))
return {
operators,
allowFundRecipient: false,
}
}

Expand All @@ -51,16 +55,39 @@ export default class SetPayees extends SolanaCommand {
const state = new PublicKey(this.flags.state)

const info = await program.account.state.fetch(state)
const token = new Token(
this.provider.connection,
new PublicKey(this.flags.link),
TOKEN_PROGRAM_ID,
this.wallet.payer,
)

const areValidPayees = (
await Promise.all(
input.operators.map(async ({ payee }) => {
try {
const info = await token.getAccountInfo(new PublicKey(payee))
return !!info.address
} catch (e) {
logger.error(`Payee with address ${payee} does not have a valid Token recipient address`)
return false
}
}),
)
).every((isValid) => isValid)

// TODO: check that is a valid payable address: new PublicKey(operator.payee),
this.require(
areValidPayees || !!input.allowFundRecipient,
'Every payee needs to have a valid token recipient address',
)
const payeeByTransmitter = input.operators.reduce(
(agg, operator) => ({
...agg,
[new PublicKey(operator.transmitter).toString()]: new PublicKey(operator.payee),
}),
{},
)
// TODO: Check keys format

// Set the payees in the same order the oracles are saved in the contract. The length of the payees need to be same as the oracles saved
const payees = info.oracles.xs
.slice(0, info.oracles.len)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default class SetupFlow extends FlowCommand<TransactionResponse> {
payee: 'G5LdWMvWoQQ787iPgWbCSTrkPB5Li9e2CWi6jYuAUHUH',
},
],
allowFundRecipient: false,
}

const configInput = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const MAX_TRANSACTION_BYTES = 996
export const ORACLES_MAX_LENGTH = 19

0 comments on commit 4708ee8

Please sign in to comment.