Переглянути джерело

solana: add retry/poking mechanism

Closes #6
Hendrik Hofstadt 5 роки тому
батько
коміт
4ba7885c62

+ 21 - 11
docs/solana_program.md

@@ -16,6 +16,14 @@ Initializes a new Bridge at `bridge`.
 | 3     | guardian_set | GuardianSet         |        |   ✅       | ✅    | ✅      |
 | 4     | payer | Account         |    ✅     |          |     |       |
 
+#### PokeProposal
+
+Pokes a `TransferOutProposal` so it is reprocessed by the guardians.
+
+| Index | Name   | Type         | signer | writeable | empty | derived |
+| ----- | ------ | ------------ | ------ | --------- | ----- | ------- |
+| 0     | proposal | TransferOutProposal |        |     ✅      | ️   |  ✅    |
+
 #### TransferOut
 
 Burns a wrapped asset `token` from `sender` on the Solana chain.
@@ -29,11 +37,12 @@ Parameters:
 | 0     | bridge_p | BridgeProgram |        |           | ️   |      |
 | 1     | sys | SystemProgram |        |           | ️   |      |
 | 2     | token_program | SplToken |        |           | ️   |      |
-| 3     | token_account   | TokenAccount        |        | ✅        |       |         |
-| 4     | bridge   | BridgeConfig        |        |           |       |         |
-| 5     | proposal | TransferOutProposal |        | ✅        | ✅    | ✅      |
-| 6     | token    | WrappedAsset        |        | ✅        |       | ✅      |
-| 7     | payer    | Account        |    ✅    |         |       |       |
+| 3     | clock | Sysvar |        |           | ️   | ✅     |
+| 4     | token_account   | TokenAccount        |        | ✅        |       |         |
+| 5     | bridge   | BridgeConfig        |        |           |       |         |
+| 6     | proposal | TransferOutProposal |        | ✅        | ✅    | ✅      |
+| 7     | token    | WrappedAsset        |        | ✅        |       | ✅      |
+| 8     | payer    | Account        |    ✅    |         |       |       |
 
 #### TransferOutNative
 
@@ -47,12 +56,13 @@ The transfer proposal will be tracked at a new account `proposal` where a VAA wi
 | 0     | bridge_p | BridgeProgram |        |           | ️   |      |
 | 1     | sys | SystemProgram |        |           | ️   |      |
 | 2     | token_program | SplToken |        |           | ️   |      |
-| 3     | token_account          | TokenAccount        |        | ✅        |       |         |
-| 4     | bridge          | BridgeConfig        |        |           |       |         |
-| 5     | proposal        | TransferOutProposal |        | ✅        | ✅    | ✅      |
-| 6     | token           | Mint                |        | ✅        |       |         |
-| 7     | payer    | Account        |    ✅    |         |       |       |
-| 8     | custody_account | TokenAccount                |        | ✅        | opt   | ✅      |
+| 3     | clock | Sysvar |        |           | ️   | ✅     |
+| 4     | token_account          | TokenAccount        |        | ✅        |       |         |
+| 5     | bridge          | BridgeConfig        |        |           |       |         |
+| 6     | proposal        | TransferOutProposal |        | ✅        | ✅    | ✅      |
+| 7     | token           | Mint                |        | ✅        |       |         |
+| 8     | payer    | Account        |    ✅    |         |       |       |
+| 9     | custody_account | TokenAccount                |        | ✅        | opt   | ✅      |
 
 #### EvictTransferOut
 

+ 2 - 10
solana/agent/src/main.rs

@@ -137,14 +137,6 @@ impl Agent for AgentImpl {
 
                         println!("lockup changed in slot: {}", v.context.slot);
 
-                        let time = match rpc.get_block_time(v.context.slot) {
-                            Ok(v) => v as u64,
-                            Err(e) => {
-                                println!("failed to fetch block time for event: {}", e);
-                                continue;
-                            }
-                        };
-
                         let b = match Bridge::unpack_immutable::<TransferOutProposal>(
                             v.value.account.data.as_slice(),
                         ) {
@@ -163,7 +155,7 @@ impl Agent for AgentImpl {
                             LockupEvent {
                                 slot: v.context.slot,
                                 lockup_address: v.value.pubkey.to_string(),
-                                time,
+                                time: b.lockup_time as u64,
                                 event: Some(Event::New(LockupEventNew {
                                     nonce: b.nonce,
                                     source_chain: CHAIN_ID_SOLANA as u32,
@@ -181,7 +173,7 @@ impl Agent for AgentImpl {
                             LockupEvent {
                                 slot: v.context.slot,
                                 lockup_address: v.value.pubkey.to_string(),
-                                time,
+                                time: b.lockup_time as u64,
                                 event: Some(Event::VaaPosted(LockupEventVaaPosted {
                                     nonce: b.nonce,
                                     source_chain: CHAIN_ID_SOLANA as u32,

+ 26 - 1
solana/bridge/src/instruction.rs

@@ -14,7 +14,7 @@ use solana_sdk::{
 
 use crate::error::Error;
 use crate::error::Error::VAATooLong;
-use crate::instruction::BridgeInstruction::{Initialize, PostVAA, TransferOut};
+use crate::instruction::BridgeInstruction::{Initialize, PokeProposal, PostVAA, TransferOut};
 use crate::state::{AssetMeta, Bridge, BridgeConfig};
 use crate::vaa::{VAABody, VAA};
 
@@ -123,6 +123,9 @@ pub enum BridgeInstruction {
     /// Deletes a `ExecutedVAA` after the `VAA_EXPIRATION_TIME` is over to free up space on chain.
     /// This returns the rent to the sender.
     EvictClaimedVAA(),
+
+    /// Pokes a proposal with no valid VAAs attached so guardians reprocess it.
+    PokeProposal(),
 }
 
 impl BridgeInstruction {
@@ -153,6 +156,7 @@ impl BridgeInstruction {
                 let payload: VAAData = input[1..].to_vec();
                 PostVAA(payload)
             }
+            5 => PokeProposal(),
             _ => return Err(ProgramError::InvalidInstructionData),
         })
     }
@@ -201,6 +205,9 @@ impl BridgeInstruction {
             Self::EvictClaimedVAA() => {
                 output[0] = 4;
             }
+            Self::PokeProposal() => {
+                output[0] = 5;
+            }
         }
         Ok(output)
     }
@@ -273,6 +280,7 @@ pub fn transfer_out(
         AccountMeta::new_readonly(*program_id, false),
         AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
         AccountMeta::new_readonly(spl_token::id(), false),
+        AccountMeta::new_readonly(solana_sdk::sysvar::clock::id(), false),
         AccountMeta::new(*token_account, false),
         AccountMeta::new(bridge_key, false),
         AccountMeta::new(transfer_key, false),
@@ -374,6 +382,23 @@ pub fn post_vaa(
     })
 }
 
+/// Creates an 'PokeProposal' instruction.
+#[cfg(not(target_arch = "bpf"))]
+pub fn poke_proposal(
+    program_id: &Pubkey,
+    transfer_proposal: &Pubkey,
+) -> Result<Instruction, ProgramError> {
+    let data = BridgeInstruction::PokeProposal().serialize()?;
+
+    let mut accounts = vec![AccountMeta::new(*transfer_proposal, false)];
+
+    Ok(Instruction {
+        program_id: *program_id,
+        accounts,
+        data,
+    })
+}
+
 /// Unpacks a reference from a bytes buffer.
 pub fn unpack<T>(input: &[u8]) -> Result<&T, ProgramError> {
     if input.len() < size_of::<u8>() + size_of::<T>() {

+ 28 - 0
solana/bridge/src/processor.rs

@@ -59,6 +59,11 @@ impl Bridge {
 
                 Self::process_vaa(program_id, accounts, vaa_body, &vaa)
             }
+            PokeProposal() => {
+                info!("Instruction: PokeProposal");
+
+                Self::process_poke(program_id, accounts)
+            }
             _ => panic!(""),
         }
     }
@@ -133,6 +138,23 @@ impl Bridge {
         Ok(())
     }
 
+    /// Transfers a wrapped asset out
+    pub fn process_poke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+        let account_info_iter = &mut accounts.iter();
+        let proposal_info = next_account_info(account_info_iter)?;
+
+        let mut transfer_data = proposal_info.data.borrow_mut();
+        let mut proposal: &mut TransferOutProposal = Self::unpack(&mut transfer_data)?;
+        if proposal.vaa_time != 0 {
+            return Err(Error::VAAAlreadySubmitted.into());
+        }
+
+        // Increase poke counter
+        proposal.poke_counter += 1;
+
+        Ok(())
+    }
+
     /// Transfers a wrapped asset out
     pub fn process_transfer_out(
         program_id: &Pubkey,
@@ -144,6 +166,7 @@ impl Bridge {
         next_account_info(account_info_iter)?; // Bridge program
         next_account_info(account_info_iter)?; // System program
         next_account_info(account_info_iter)?; // Token program
+        let clock_info = next_account_info(account_info_iter)?;
         let sender_account_info = next_account_info(account_info_iter)?;
         let bridge_info = next_account_info(account_info_iter)?;
         let transfer_info = next_account_info(account_info_iter)?;
@@ -153,6 +176,7 @@ impl Bridge {
         let sender = Bridge::token_account_deserialize(sender_account_info)?;
         let bridge = Bridge::bridge_deserialize(bridge_info)?;
         let mint = Bridge::mint_deserialize(mint_info)?;
+        let clock = Clock::from_account_info(clock_info)?;
 
         // Does the token belong to the mint
         if sender.mint != *mint_info.key {
@@ -209,6 +233,7 @@ impl Bridge {
         transfer.foreign_address = t.target;
         transfer.amount = t.amount;
         transfer.to_chain_id = t.chain_id;
+        transfer.lockup_time = clock.unix_timestamp as u32;
 
         // Make sure decimals are correct
         transfer.asset = AssetMeta {
@@ -231,6 +256,7 @@ impl Bridge {
         next_account_info(account_info_iter)?; // Bridge program
         next_account_info(account_info_iter)?; // System program
         next_account_info(account_info_iter)?; // Token program
+        let clock_info = next_account_info(account_info_iter)?;
         let sender_account_info = next_account_info(account_info_iter)?;
         let bridge_info = next_account_info(account_info_iter)?;
         let transfer_info = next_account_info(account_info_iter)?;
@@ -241,6 +267,7 @@ impl Bridge {
         let sender = Bridge::token_account_deserialize(sender_account_info)?;
         let mint = Bridge::mint_deserialize(mint_info)?;
         let bridge = Bridge::bridge_deserialize(bridge_info)?;
+        let clock = Clock::from_account_info(clock_info)?;
 
         // Does the token belong to the mint
         if sender.mint != *mint_info.key {
@@ -317,6 +344,7 @@ impl Bridge {
         transfer.source_address = sender_account_info.key.to_bytes();
         transfer.foreign_address = t.target;
         transfer.nonce = t.nonce;
+        transfer.lockup_time = clock.unix_timestamp as u32;
 
         // Don't use the user-given data as we don't check mint = AssetMeta.address
         transfer.asset = AssetMeta {

+ 4 - 0
solana/bridge/src/state.rs

@@ -69,6 +69,10 @@ pub struct TransferOutProposal {
     pub vaa: [u8; MAX_VAA_SIZE + 1],
     /// time the vaa was submitted
     pub vaa_time: u32,
+    /// time the lockup was created
+    pub lockup_time: u32,
+    /// times the proposal has been poked
+    pub poke_counter: u8,
 
     /// Is `true` if this structure has been initialized.
     pub is_initialized: bool,

+ 45 - 0
solana/cli/src/main.rs

@@ -85,6 +85,18 @@ fn command_deploy_bridge(
     Ok(Some(transaction))
 }
 
+fn command_poke_proposal(config: &Config, bridge: &Pubkey, proposal: &Pubkey) -> CommmandResult {
+    println!("Poking lockup");
+
+    let ix = poke_proposal(bridge, proposal)?;
+    let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
+
+    let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
+    check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
+    transaction.sign(&[&config.fee_payer, &config.owner], recent_blockhash);
+    Ok(Some(transaction))
+}
+
 fn command_lock_tokens(
     config: &Config,
     bridge: &Pubkey,
@@ -954,6 +966,34 @@ fn main() {
                         .help("The vaa to be posted"),
                 )
         )
+        .subcommand(
+            SubCommand::with_name("poke")
+                .about("Poke a proposal so it's retried")
+                .arg(
+                    Arg::with_name("bridge")
+                        .long("bridge")
+                        .value_name("BRIDGE_KEY")
+                        .validator(is_pubkey_or_keypair)
+                        .takes_value(true)
+                        .index(1)
+                        .required(true)
+                        .help(
+                            "Specify the bridge program public key"
+                        ),
+                )
+                .arg(
+                    Arg::with_name("proposal")
+                        .long("proposal")
+                        .value_name("PROPOSAL_KEY")
+                        .validator(is_pubkey_or_keypair)
+                        .takes_value(true)
+                        .index(2)
+                        .required(true)
+                        .help(
+                            "Specify the transfer proposal to poke"
+                        ),
+                )
+        )
         .subcommand(
             SubCommand::with_name("wrapped-address")
                 .about("Derive wrapped asset address")
@@ -1114,6 +1154,11 @@ fn main() {
             let vaa = hex::decode(vaa_string).unwrap();
             command_submit_vaa(&config, &bridge, vaa.as_slice())
         }
+        ("poke", Some(arg_matches)) => {
+            let bridge = pubkey_of(arg_matches, "bridge").unwrap();
+            let proposal = pubkey_of(arg_matches, "proposal").unwrap();
+            command_poke_proposal(&config, &bridge, &proposal)
+        }
         ("wrapped-address", Some(arg_matches)) => {
             let bridge = pubkey_of(arg_matches, "bridge").unwrap();
             let chain = value_t_or_exit!(arg_matches, "chain", u8);

+ 24 - 2
web/src/components/TransferProposals.tsx

@@ -9,7 +9,9 @@ import {WormholeFactory} from "../contracts/WormholeFactory";
 import {BRIDGE_ADDRESS} from "../config";
 import {keccak256} from "ethers/utils";
 import BN from 'bn.js';
-import {PublicKey} from "@solana/web3.js";
+import {PublicKey, Transaction} from "@solana/web3.js";
+import KeyContext from "../providers/KeyContext";
+import ClientContext from "../providers/ClientContext";
 
 // @ts-ignore
 window.ethereum.enable();
@@ -32,6 +34,8 @@ function TransferProposals() {
     let t = useContext(SolanaTokenContext);
     let tokens = useContext(SolanaTokenContext);
     let b = useContext(BridgeContext);
+    let k = useContext(KeyContext);
+    let c = useContext(ClientContext);
 
     let [lockups, setLockups] = useState<LockupWithStatus[]>([])
 
@@ -84,13 +88,31 @@ function TransferProposals() {
         message.loading({content: "Waiting for transaction to be mined...", key: "eth_tx", duration: 1000})
         await tx.wait(1)
         message.success({content: "Execution of VAA succeeded", key: "eth_tx"})
+    }
 
+    let pokeProposal = async (proposalAddress: PublicKey) => {
+        message.loading({content: "Poking lockup ...", key: "poke"}, 1000)
+
+        let ix = await b.createPokeProposalInstruction(proposalAddress);
+        let recentHash = await c.getRecentBlockhash();
+        let tx = new Transaction();
+        tx.recentBlockhash = recentHash.blockhash
+        tx.add(ix)
+        tx.sign(k)
+        try {
+            await c.sendTransaction(tx, [k])
+            message.success({content: "Poke succeeded", key: "poke"})
+        } catch (e) {
+            message.error({content: "Poke failed", key: "poke"})
+        }
     }
 
     let statusToPrompt = (v: LockupWithStatus) => {
         switch (v.status) {
             case LockupStatus.AWAITING_VAA:
-                return ("Awaiting VAA");
+                return (<>Awaiting VAA (<a onClick={() => {
+                    pokeProposal(v.lockupAddress)
+                }}>poke</a>)</>);
             case LockupStatus.UNCLAIMED_VAA:
                 return (<Button onClick={() => {
                     executeVAA(v)

+ 32 - 3
web/src/utils/bridge.ts

@@ -14,6 +14,7 @@ export interface AssetMeta {
 }
 
 export interface Lockup {
+    lockupAddress: PublicKey,
     amount: BN,
     toChain: number,
     sourceAddress: PublicKey,
@@ -24,6 +25,7 @@ export interface Lockup {
     nonce: number,
     vaa: Uint8Array,
     vaaTime: number,
+    pokeCounter: number,
     initialized: boolean,
 }
 
@@ -93,6 +95,7 @@ class SolanaBridge {
             {pubkey: this.programID, isSigner: false, isWritable: false},
             {pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false},
             {pubkey: this.tokenProgram, isSigner: false, isWritable: false},
+            {pubkey: solanaWeb3.SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false},
             {pubkey: tokenAccount, isSigner: false, isWritable: true},
             {pubkey: configKey, isSigner: false, isWritable: false},
 
@@ -115,6 +118,30 @@ class SolanaBridge {
         });
     }
 
+    createPokeProposalInstruction(
+        proposalAccount: PublicKey,
+    ): TransactionInstruction {
+        const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'),]);
+
+        const data = Buffer.alloc(dataLayout.span);
+        dataLayout.encode(
+            {
+                instruction: 5, // PokeProposal instruction
+            },
+            data,
+        );
+
+        const keys = [
+            {pubkey: proposalAccount, isSigner: false, isWritable: true},
+        ];
+
+        return new TransactionInstruction({
+            keys,
+            programId: this.programID,
+            data,
+        });
+    }
+
     // fetchAssetMeta fetches the AssetMeta for an SPL token
     async fetchAssetMeta(
         mint: PublicKey,
@@ -183,14 +210,15 @@ class SolanaBridge {
             BufferLayout.blob(1001, 'vaa'),
             BufferLayout.seq(BufferLayout.u8(), 3), // 4 byte alignment because a u32 is following
             BufferLayout.u32('vaaTime'),
+            BufferLayout.u8('pokeCounter'),
             BufferLayout.u8('initialized'),
         ]);
 
         let accounts: Lockup[] = [];
         for (let acc of raw_accounts) {
-            acc = acc.account;
-            let parsedAccount = dataLayout.decode(bs58.decode(acc.data))
+            let parsedAccount = dataLayout.decode(bs58.decode(acc.account.data))
             accounts.push({
+                lockupAddress: acc.pubkey,
                 amount: new BN(parsedAccount.amount, 2, "le"),
                 assetAddress: parsedAccount.assetAddress,
                 assetChain: parsedAccount.assetChain,
@@ -201,7 +229,8 @@ class SolanaBridge {
                 targetAddress: parsedAccount.targetAddress,
                 toChain: parsedAccount.toChain,
                 vaa: parsedAccount.vaa,
-                vaaTime: parsedAccount.vaaTime
+                vaaTime: parsedAccount.vaaTime,
+                pokeCounter: parsedAccount.pokeCounter
             })
         }