Skip to content

Forwarder #1179

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

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open

Forwarder #1179

wants to merge 30 commits into from

Conversation

augustbleeds
Copy link
Contributor

@augustbleeds augustbleeds commented Apr 10, 2025

Description

Design of forwarder based off of #614

EDIT: please see the committed README.md docs for more info

instructions:

  • initialize: creates a new forwarder state, and precomputes the bump for the forwarder authority pda which signs the CPI to the receiver
  • transfer/accept_ownership: self-explanatory
  • init/update/close_oracles_config: oracle configs (which are called oracle sets in EVM) are stored as separate data accounts since a new config version / config_id with the same DON would create a new oracle config so the size is unbounded (as opposed to finding a soft-limit on the new dons that are created)
  • report: processes the report which is of format data = bump | len_signatures (N) | signatures (N*65) | raw_report (M) | report_context (96). A report will create a new execution_state pda data account if doesn't exist via create_account instruction. It executes a CPI to a receiver program where the forwarder authority pda is the signer.

Requires Dependencies

Resolves Dependencies


use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about what steps we need to perform for deployment. Is it

  1. Deploy the program.
  2. Initialize (can we do it as part of the deployment transaction?)
  3. Init oracles config
  4. Transfer ownership to MCMS
  5. Accept ownership on MCMS

#[account]
#[derive(Default)]
#[derive(InitSpace)]
pub struct ExecutionState {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how we can verify if the report was forwarded on the WriteTarget level

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate on the scenario? we can add an event to the report function which you can listen to or you can query the execution status account associated with the transmission

pub struct OraclesConfig {
config_id: u64,
f: u8,
signer_addresses: Vec<EthAddress>, // maybe think about doing ECDSA over ed25519?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I'd use a constant size array plus a length int and make the type zero_copy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let data = &data[2..];
let _signatures = &data[..65*len];
let data = &data[65*len..];
let _report_context = &data[data.len()-96..];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make more sense to have the report context before the report

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept it this way because the existing solana keyring assumes the report context comes after https://github.com/smartcontractkit/chainlink/blob/develop/core/services/keystore/keys/ocr2key/solana_keyring.go#L38 . But open to changing it as well.

We should be using ocr2 signatures, not ocr3 right?

prev_signer = curr_signer;
}

oracles_config.config_id = ((don_id as u64) << 32) | (config_version as u64);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use get_config_id

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

}

fn get_config_id(don_id: u32, config_version: u32) -> u64 {
((don_id as u64) << 32) | (config_version as u64)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config id could technically just be a struct with two u32 fields, it would have the same underlying size

) -> Result<()> {
// ensure MAX_SIGNERS fit in the bits of uniques
let mut uniques: u32 = 0;
assert!(uniques.count_ones() + uniques.count_zeros() >= MAX_ORACLES as u32);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to use require! and a custom error here?


pub fn extract_config_id(raw_report: &[u8]) -> [u8; 8] {
// don_id | don_config_version
raw_report[37..45].try_into().expect("Expected 8 bytes")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where this will panic, would it be better to guard this with require!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good check, this something we have a ticket for @chray-zhang will pick up some of the small improvements


`forwarder_authority` is the PDA which is responsible for signing the CPI to a receiver program via invoke_signed()

`execution_state` is the PDA which stores a report's execution status. For now, it only stores successes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will offchain components (writetarget) know when a transmission is failed / fatal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. This is something that the platform expansion team will need to add in later iterations. I think the body of work is too large for our timeline at the moment. But I have some thoughts here. I think the first option is the most robust, but we have to have a new capabilities for that to happen.

As it is right now, the write targeter will just keep on trying until it reaches the max tries and then stop.

@chray-zhang chray-zhang self-requested a review May 20, 2025 15:27
@cl-sonarqube-production
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants