solana: add retry/poking mechanism

Closes #6
This commit is contained in:
Hendrik Hofstadt 2020-08-31 21:05:38 +02:00
parent 2747839bd4
commit 4ba7885c62
8 changed files with 182 additions and 27 deletions

View File

@ -16,6 +16,14 @@ Initializes a new Bridge at `bridge`.
| 3 | guardian_set | GuardianSet | | ✅ | ✅ | ✅ | | 3 | guardian_set | GuardianSet | | ✅ | ✅ | ✅ |
| 4 | payer | Account | ✅ | | | | | 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 #### TransferOut
Burns a wrapped asset `token` from `sender` on the Solana chain. Burns a wrapped asset `token` from `sender` on the Solana chain.
@ -29,11 +37,12 @@ Parameters:
| 0 | bridge_p | BridgeProgram | | | | | | 0 | bridge_p | BridgeProgram | | | | |
| 1 | sys | SystemProgram | | | | | | 1 | sys | SystemProgram | | | | |
| 2 | token_program | SplToken | | | | | | 2 | token_program | SplToken | | | | |
| 3 | token_account | TokenAccount | | ✅ | | | | 3 | clock | Sysvar | | | | ✅ |
| 4 | bridge | BridgeConfig | | | | | | 4 | token_account | TokenAccount | | ✅ | | |
| 5 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ | | 5 | bridge | BridgeConfig | | | | |
| 6 | token | WrappedAsset | | ✅ | | ✅ | | 6 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ |
| 7 | payer | Account | ✅ | | | | | 7 | token | WrappedAsset | | ✅ | | ✅ |
| 8 | payer | Account | ✅ | | | |
#### TransferOutNative #### TransferOutNative
@ -47,12 +56,13 @@ The transfer proposal will be tracked at a new account `proposal` where a VAA wi
| 0 | bridge_p | BridgeProgram | | | | | | 0 | bridge_p | BridgeProgram | | | | |
| 1 | sys | SystemProgram | | | | | | 1 | sys | SystemProgram | | | | |
| 2 | token_program | SplToken | | | | | | 2 | token_program | SplToken | | | | |
| 3 | token_account | TokenAccount | | ✅ | | | | 3 | clock | Sysvar | | | | ✅ |
| 4 | bridge | BridgeConfig | | | | | | 4 | token_account | TokenAccount | | ✅ | | |
| 5 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ | | 5 | bridge | BridgeConfig | | | | |
| 6 | token | Mint | | ✅ | | | | 6 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ |
| 7 | payer | Account | ✅ | | | | | 7 | token | Mint | | ✅ | | |
| 8 | custody_account | TokenAccount | | ✅ | opt | ✅ | | 8 | payer | Account | ✅ | | | |
| 9 | custody_account | TokenAccount | | ✅ | opt | ✅ |
#### EvictTransferOut #### EvictTransferOut

View File

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

View File

@ -14,7 +14,7 @@ use solana_sdk::{
use crate::error::Error; use crate::error::Error;
use crate::error::Error::VAATooLong; 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::state::{AssetMeta, Bridge, BridgeConfig};
use crate::vaa::{VAABody, VAA}; 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. /// Deletes a `ExecutedVAA` after the `VAA_EXPIRATION_TIME` is over to free up space on chain.
/// This returns the rent to the sender. /// This returns the rent to the sender.
EvictClaimedVAA(), EvictClaimedVAA(),
/// Pokes a proposal with no valid VAAs attached so guardians reprocess it.
PokeProposal(),
} }
impl BridgeInstruction { impl BridgeInstruction {
@ -153,6 +156,7 @@ impl BridgeInstruction {
let payload: VAAData = input[1..].to_vec(); let payload: VAAData = input[1..].to_vec();
PostVAA(payload) PostVAA(payload)
} }
5 => PokeProposal(),
_ => return Err(ProgramError::InvalidInstructionData), _ => return Err(ProgramError::InvalidInstructionData),
}) })
} }
@ -201,6 +205,9 @@ impl BridgeInstruction {
Self::EvictClaimedVAA() => { Self::EvictClaimedVAA() => {
output[0] = 4; output[0] = 4;
} }
Self::PokeProposal() => {
output[0] = 5;
}
} }
Ok(output) Ok(output)
} }
@ -273,6 +280,7 @@ pub fn transfer_out(
AccountMeta::new_readonly(*program_id, false), AccountMeta::new_readonly(*program_id, false),
AccountMeta::new_readonly(solana_sdk::system_program::id(), false), AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
AccountMeta::new_readonly(spl_token::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(*token_account, false),
AccountMeta::new(bridge_key, false), AccountMeta::new(bridge_key, false),
AccountMeta::new(transfer_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. /// Unpacks a reference from a bytes buffer.
pub fn unpack<T>(input: &[u8]) -> Result<&T, ProgramError> { pub fn unpack<T>(input: &[u8]) -> Result<&T, ProgramError> {
if input.len() < size_of::<u8>() + size_of::<T>() { if input.len() < size_of::<u8>() + size_of::<T>() {

View File

@ -59,6 +59,11 @@ impl Bridge {
Self::process_vaa(program_id, accounts, vaa_body, &vaa) Self::process_vaa(program_id, accounts, vaa_body, &vaa)
} }
PokeProposal() => {
info!("Instruction: PokeProposal");
Self::process_poke(program_id, accounts)
}
_ => panic!(""), _ => panic!(""),
} }
} }
@ -133,6 +138,23 @@ impl Bridge {
Ok(()) 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 /// Transfers a wrapped asset out
pub fn process_transfer_out( pub fn process_transfer_out(
program_id: &Pubkey, program_id: &Pubkey,
@ -144,6 +166,7 @@ impl Bridge {
next_account_info(account_info_iter)?; // Bridge program next_account_info(account_info_iter)?; // Bridge program
next_account_info(account_info_iter)?; // System program next_account_info(account_info_iter)?; // System program
next_account_info(account_info_iter)?; // Token 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 sender_account_info = next_account_info(account_info_iter)?;
let bridge_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)?; 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 sender = Bridge::token_account_deserialize(sender_account_info)?;
let bridge = Bridge::bridge_deserialize(bridge_info)?; let bridge = Bridge::bridge_deserialize(bridge_info)?;
let mint = Bridge::mint_deserialize(mint_info)?; let mint = Bridge::mint_deserialize(mint_info)?;
let clock = Clock::from_account_info(clock_info)?;
// Does the token belong to the mint // Does the token belong to the mint
if sender.mint != *mint_info.key { if sender.mint != *mint_info.key {
@ -209,6 +233,7 @@ impl Bridge {
transfer.foreign_address = t.target; transfer.foreign_address = t.target;
transfer.amount = t.amount; transfer.amount = t.amount;
transfer.to_chain_id = t.chain_id; transfer.to_chain_id = t.chain_id;
transfer.lockup_time = clock.unix_timestamp as u32;
// Make sure decimals are correct // Make sure decimals are correct
transfer.asset = AssetMeta { transfer.asset = AssetMeta {
@ -231,6 +256,7 @@ impl Bridge {
next_account_info(account_info_iter)?; // Bridge program next_account_info(account_info_iter)?; // Bridge program
next_account_info(account_info_iter)?; // System program next_account_info(account_info_iter)?; // System program
next_account_info(account_info_iter)?; // Token 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 sender_account_info = next_account_info(account_info_iter)?;
let bridge_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)?; 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 sender = Bridge::token_account_deserialize(sender_account_info)?;
let mint = Bridge::mint_deserialize(mint_info)?; let mint = Bridge::mint_deserialize(mint_info)?;
let bridge = Bridge::bridge_deserialize(bridge_info)?; let bridge = Bridge::bridge_deserialize(bridge_info)?;
let clock = Clock::from_account_info(clock_info)?;
// Does the token belong to the mint // Does the token belong to the mint
if sender.mint != *mint_info.key { if sender.mint != *mint_info.key {
@ -317,6 +344,7 @@ impl Bridge {
transfer.source_address = sender_account_info.key.to_bytes(); transfer.source_address = sender_account_info.key.to_bytes();
transfer.foreign_address = t.target; transfer.foreign_address = t.target;
transfer.nonce = t.nonce; 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 // Don't use the user-given data as we don't check mint = AssetMeta.address
transfer.asset = AssetMeta { transfer.asset = AssetMeta {

View File

@ -69,6 +69,10 @@ pub struct TransferOutProposal {
pub vaa: [u8; MAX_VAA_SIZE + 1], pub vaa: [u8; MAX_VAA_SIZE + 1],
/// time the vaa was submitted /// time the vaa was submitted
pub vaa_time: u32, 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. /// Is `true` if this structure has been initialized.
pub is_initialized: bool, pub is_initialized: bool,

View File

@ -85,6 +85,18 @@ fn command_deploy_bridge(
Ok(Some(transaction)) 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( fn command_lock_tokens(
config: &Config, config: &Config,
bridge: &Pubkey, bridge: &Pubkey,
@ -954,6 +966,34 @@ fn main() {
.help("The vaa to be posted"), .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(
SubCommand::with_name("wrapped-address") SubCommand::with_name("wrapped-address")
.about("Derive wrapped asset address") .about("Derive wrapped asset address")
@ -1114,6 +1154,11 @@ fn main() {
let vaa = hex::decode(vaa_string).unwrap(); let vaa = hex::decode(vaa_string).unwrap();
command_submit_vaa(&config, &bridge, vaa.as_slice()) 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)) => { ("wrapped-address", Some(arg_matches)) => {
let bridge = pubkey_of(arg_matches, "bridge").unwrap(); let bridge = pubkey_of(arg_matches, "bridge").unwrap();
let chain = value_t_or_exit!(arg_matches, "chain", u8); let chain = value_t_or_exit!(arg_matches, "chain", u8);

View File

@ -9,7 +9,9 @@ import {WormholeFactory} from "../contracts/WormholeFactory";
import {BRIDGE_ADDRESS} from "../config"; import {BRIDGE_ADDRESS} from "../config";
import {keccak256} from "ethers/utils"; import {keccak256} from "ethers/utils";
import BN from 'bn.js'; 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 // @ts-ignore
window.ethereum.enable(); window.ethereum.enable();
@ -32,6 +34,8 @@ function TransferProposals() {
let t = useContext(SolanaTokenContext); let t = useContext(SolanaTokenContext);
let tokens = useContext(SolanaTokenContext); let tokens = useContext(SolanaTokenContext);
let b = useContext(BridgeContext); let b = useContext(BridgeContext);
let k = useContext(KeyContext);
let c = useContext(ClientContext);
let [lockups, setLockups] = useState<LockupWithStatus[]>([]) 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}) message.loading({content: "Waiting for transaction to be mined...", key: "eth_tx", duration: 1000})
await tx.wait(1) await tx.wait(1)
message.success({content: "Execution of VAA succeeded", key: "eth_tx"}) 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) => { let statusToPrompt = (v: LockupWithStatus) => {
switch (v.status) { switch (v.status) {
case LockupStatus.AWAITING_VAA: case LockupStatus.AWAITING_VAA:
return ("Awaiting VAA"); return (<>Awaiting VAA (<a onClick={() => {
pokeProposal(v.lockupAddress)
}}>poke</a>)</>);
case LockupStatus.UNCLAIMED_VAA: case LockupStatus.UNCLAIMED_VAA:
return (<Button onClick={() => { return (<Button onClick={() => {
executeVAA(v) executeVAA(v)

View File

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