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

Keystone: forwarder + write target #614

Draft
wants to merge 5 commits into
base: anchor-0.29
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions contracts/Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
anchor_version = "0.29.0"
[toolchain]

[features]
seeds = false
skip-lint = false

[registry]
url = "https://anchor.projectserum.com"

[provider]
cluster = "localnet"
# wallet = "~/.config/solana/id.json"
cluster = "Localnet"
wallet = "id.json"

[scripts]
Expand All @@ -21,6 +24,7 @@ test = "yarn run test"
# TODO: add pubkeys

[programs.localnet]
access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW"
keystone-forwarder = "6v9Lm94wiHXJf4HYoWoRj7JGb5YCDnsvybr9Y3seJ7po"
ocr_2 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" # need to rename the idl to satisfy anchor.js...
store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny"
access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW"
7 changes: 7 additions & 0 deletions contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions contracts/programs/keystone-forwarder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "keystone-forwarder"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "keystone_forwarder"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = "0.29.0"
2 changes: 2 additions & 0 deletions contracts/programs/keystone-forwarder/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
148 changes: 148 additions & 0 deletions contracts/programs/keystone-forwarder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use anchor_lang::prelude::*;

declare_id!("6v9Lm94wiHXJf4HYoWoRj7JGb5YCDnsvybr9Y3seJ7po");

// TODO: ownable

pub const STATE_VERSION: u8 = 1;

#[account]
#[derive(Default)]
pub struct State {
version: u8,
authority_nonce: u8,
owner: Pubkey,
}

#[account]
#[derive(Default)]
pub struct ExecutionState {}

#[error_code]
pub enum ErrorCode {
#[msg("Unauthorized")]
Unauthorized = 0,

#[msg("Invalid input")]
InvalidInput = 1,
}

#[program]
pub mod keystone_forwarder {
use anchor_lang::solana_program::{instruction::Instruction, program::invoke_signed};

use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Precompute the authority PDA bump
let (_authority_pubkey, authority_nonce) = Pubkey::find_program_address(
&[b"forwarder", ctx.accounts.state.key().as_ref()],
&crate::ID,
);

let state = &mut ctx.accounts.state;
state.version = STATE_VERSION;
state.authority_nonce = authority_nonce;
state.owner = ctx.accounts.owner.key();
Ok(())
}

// TODO: use raw &[u8] input to avoid serialization and encoding
pub fn report(ctx: Context<Report>, data: Vec<u8>) -> Result<()> {
const RAW_REPORT_LEN: usize = 1 + OFFSET;
require!(data.len() > RAW_REPORT_LEN, ErrorCode::InvalidInput);
let len = data[0] as usize;
let data = &data[1..];
let (raw_signatures, raw_report) = data.split_at(32 * len);

// TODO: a way to store context inside data without limiting the receiver
const OFFSET: usize = 32 + 32;
// meta = (workflowID, workflowExecutionID)
let (meta, data) = raw_report.split_at(OFFSET);

// verify signature
use anchor_lang::solana_program::{hash, keccak, secp256k1_recover::*};

// 64 byte signature + 1 byte recovery id
const SIGNATURE_LEN: usize = SECP256K1_SIGNATURE_LENGTH + 1;
// raw_signatures is exactly sized
require!(
raw_signatures.len() % SIGNATURE_LEN == 0,
ErrorCode::InvalidInput
);
// let signature_count = raw_signatures.len() / SIGNATURE_LEN;
// require!(
// signature_count == usize::from(config.f) + 1,
// ErrorCode::InvalidInput
// );

let hash = hash::hash(&raw_report).to_bytes();

let raw_signatures = raw_signatures.chunks(SIGNATURE_LEN);
for signature in raw_signatures {
// TODO:
}

// check if PDA exists, if so terminate the call

// invoke_signed with forwarder authority
let program_id = ctx.accounts.receiver_program.key();
let accounts = vec![];
let ix = Instruction::new_with_bytes(program_id, &raw_report, accounts);
let account_infos = &[];
let state_pubkey = ctx.accounts.state.key();
let signers_seeds = &[
b"forwarder",
state_pubkey.as_ref(),
&[ctx.accounts.state.authority_nonce],
];
let _ = invoke_signed(&ix, account_infos, &[signers_seeds]);

// mark tx as signed by initializing PDA via create_account instruction
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
// space: 8 discriminator + u8 authority_nonce + 1 bump
#[account(
init,
payer = owner,
space = 8 + 1 + 1 + 32
)]
pub state: Account<'info, State>,
#[account(mut)]
pub owner: Signer<'info>,

pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Report<'info> {
/// Forwarder state acccount
#[account(mut)]
pub state: Account<'info, State>,

/// Transmitter, signing the current transaction call
pub authority: Signer<'info>,

/// Authority used for signing the receiver invocation
/// CHECK: This is a PDA
#[account(seeds = [b"forwarder", state.key().as_ref()], bump = state.authority_nonce)]
pub forwarder_authority: AccountInfo<'info>,

/// State PDA for the workflow execution represented by this report.
/// TODO: we need to manually verify that it's the correct PDA since we need to unpack meta to get the execution ID
#[account(mut)]
// pub execution_state: Account<'info, ExecutionState>,
/// CHECK: TODO:
pub execution_state: UncheckedAccount<'info>,

#[account(executable)]
/// CHECK: We don't use Program<> here since it can be any program, "executable" is enough
pub receiver_program: UncheckedAccount<'info>,
// TODO: ensure receiver isn't forwarder itself?

// remaining_accounts... get passed to receiver as is
}
145 changes: 145 additions & 0 deletions contracts/tests/forwarder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as anchor from "@coral-xyz/anchor";
import { ProgramError, BN } from "@coral-xyz/anchor";
import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import * as borsh from "borsh";

import { randomBytes, createHash } from "crypto";
import * as secp256k1 from "secp256k1";
import { keccak256 } from "ethereum-cryptography/keccak";

import { assert } from "chai";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";

describe("ocr2", () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.local();
anchor.setProvider(provider);

const forwarderProgram = anchor.workspace.KeystoneForwarder;

// Generate a new wallet keypair and airdrop SOL
const payer = Keypair.generate();

const owner = provider.wallet;

const state = Keypair.generate();
const transmitter = Keypair.generate();

let authorityNonce: number;
let authority: PublicKey;

let oracles = [];
const f = 6;
// NOTE: 17 is the most we can fit into one proposeConfig if we use a different payer
// if the owner == payer then we can fit 19
const n = 19; // min: 3 * f + 1;

let generateOracle = async () => {
let secretKey = randomBytes(32);
let transmitter = Keypair.generate();
return {
signer: {
secretKey,
publicKey: secp256k1.publicKeyCreate(secretKey, false).slice(1), // compressed = false, skip first byte (0x04)
},
transmitter,
};
};

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

await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(transmitter.publicKey, LAMPORTS_PER_SOL * 1000),
"confirmed"
);
});

it("Initializes the forwarder", async() => {
await forwarderProgram.methods
.initialize()
.accounts({
state: state.publicKey,
owner: owner.publicKey,
})
.signers([state])
// .preInstructions([await program.account.state.createInstruction(state)])
.rpc();

let stateAccount = await forwarderProgram.account.state.fetch(state.publicKey);
authorityNonce = stateAccount.authorityNonce;
authority = PublicKey.createProgramAddressSync(
[
Buffer.from(anchor.utils.bytes.utf8.encode("forwarder")),
state.publicKey.toBuffer(),
Buffer.from([authorityNonce])
],
forwarderProgram.programId
);

console.log(`Generating ${n} oracles...`);
let futures = [];
for (let i = 0; i < n; i++) {
futures.push(generateOracle());
}
oracles = await Promise.all(futures);
});

// TODO: deploy mock receiver, forward the report there, assert on program log

it("Successfully receives a new, valid report", async () => {
const rawReport = Buffer.from([
// 32 byte workflow id
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
// 32 byte workflow execution id
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
// report data
0, 0, 0, 1
]);

let hash = createHash("sha256")
.update(rawReport)
.digest();

let rawSignatures = [];
// for (let oracle of oracles.slice(0, f + 1)) {
// // sign with `f` + 1 oracles
// let { signature, recid } = secp256k1.ecdsaSign(
// hash,
// oracle.signer.secretKey
// );
// rawSignatures.push(...signature);
// rawSignatures.push(recid);
// }

let data = Buffer.concat([
Buffer.from([rawSignatures.length]),
Buffer.from(rawSignatures),
rawReport,
]);

const executionState = Keypair.generate();

await forwarderProgram.methods
.report(data)
.accounts({
state: state.publicKey,
authority: transmitter.publicKey,
forwarderAuthority: authority,
executionState: executionState.publicKey, // TODO: derive
receiverProgram: forwarderProgram.programId, // TODO:
})
.signers([transmitter])
.rpc();

// TODO: await until confirmation on all of these

});

it("Doesn't retransmit the same report", async () => {

});
})
2 changes: 1 addition & 1 deletion contracts/tests/ocr2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe("ocr2", () => {
const state = Keypair.generate();
// const stateSize = 8 + ;
const feed = Keypair.generate();
const payer = Keypair.generate();
const payer = Keypair.generate(); // TODO: both payer and fromWallet seem redundant, use payer
// const owner = Keypair.generate();
const owner = provider.wallet;
const mintAuthority = Keypair.generate();
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ require (
github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27
github.com/gagliardetto/treeout v0.1.4
github.com/google/uuid v1.3.1
github.com/hashicorp/go-plugin v1.5.2
github.com/hashicorp/go-plugin v1.6.0
github.com/mitchellh/mapstructure v1.5.0
github.com/pelletier/go-toml/v2 v2.1.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240213113935-001c2f4befd4
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240301141417-6e6e1dd8a6ea
github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1
github.com/stretchr/testify v1.8.4
github.com/test-go/testify v1.1.4
Expand Down Expand Up @@ -48,7 +49,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.15 // indirect
Expand Down