Solana中的跨合约调用 及 Program Derived Addresses

1. 引言

Solana runtime可通过cross-program invocation机制来支持合约间的相互调用。
invoking合约A(Caller) 可触发调用 invoked合约B(Callee) 的instruction。invoking合约A 将halted直到 invoked合约B处理完成该instruction。

借助Program derived address,合约可 issue instructions that contain signed accounts that were not signed in the original transaction。

2. Cross-Program Invocation机制

如客户端可创建a transaction that modifies two accounts, each owned by separate on-chain programs:

let message = Message::new(vec![
    token_instruction::pay(&alice_pubkey),
    acme_instruction::launch_missiles(&bob_pubkey),
]);
client.send_and_confirm_message(&[&alice_keypair, &bob_keypair], &message);

替代方案可为:客户端可允许acme合约来代替其触发token instruction:

let message = Message::new(vec![
    acme_instruction::pay_and_launch_missiles(&alice_pubkey, &bob_pubkey),
]);
client.send_and_confirm_message(&[&alice_keypair, &bob_keypair], &message);

存在2个链上合约:

  • token合约:实现了pay() instruction。
  • acme合约:实现了launch_missiles() instruction。

可借助cross-program invocation,在acme合约中,实现对token合约的调用:

mod acme {
    
    
    use token_instruction;

    fn launch_missiles(accounts: &[AccountInfo]) -> Result<()> {
    
    
        ...
    }

    fn pay_and_launch_missiles(accounts: &[AccountInfo]) -> Result<()> {
    
    
        let alice_pubkey = accounts[1].key;
        let instruction = token_instruction::pay(&alice_pubkey);
        invoke(&instruction, accounts)?; //

        launch_missiles(accounts)?;
    }
}

其中的invoke()函数集成在Solana runtime中,负责routing the given instruction to the token program via the instruction’s program_id field。

注意,invoke时需要传入所有accounts:

  • the executable account (the ones that matches the instruction’s program id)
  • the accounts passed to the instruction processor

在调用pay()之前,runtime需确保acme未修改token所拥有的任何account。
pay()执行完成后,runtime需确保token未修改acme所拥有的任何account。

2.1 instructions that require privileges

runtime使用the privileges granted to the caller program to determine what privileges can be extended to the callee。
在本上下文中,privileges是指:

  • signers
  • writable accounts

即若caller处理的instruction中包含了signer和(或)writable account,则该caller可触发同样包含signer和(或)writable account的instruction。
这种privileges extension依赖于合约是不会更改的,排除了合约升级这种特殊场景。

在本例中,acme合约中,runtime可安全地将交易签名 当成是 token instruction的签名。当runtime看到token instruction中引用了alice_pubkey时,其会在acme instruction中查找该key是否对应a signed account。此时,即可授权token合约修改Alice的account。

2.2 Program signed accounts

借助Program derived address,合约可 issue instructions that contain signed accounts that were not signed in the original transaction。

合约可调用invoke_signed()来sign an account with program derived addresses:

        invoke_signed(
            &instruction,
            accounts,
            &[&["First addresses seed"],
              &["Second addresses first seed", "Second addresses second seed"]],
        )?;

2.3 Call Depth

通过Cross-program invocations,支持合约A 直接调用 合约B,但是depth当前限制为4。

2.4 Reentrancy

Reentrancy当前限制为direct self recursion capped at a fixed depth。该限制可防止a program might invoke another from an intermediary state without the knowledge that it might later be called back into。而direct self recursion可使合约能完全控制其state when it gets called back。

3. Program Derived Addresses(PDAs)

当跨合约调用时,Program derived addresses支持在合约内部生成签名。

Using a program derived address, a program may be given the authority over an account and later transfer that authority to another. This is possible because the program can act as the signer in the transaction that gives authority.

program derived addresses具有如下优点:

  • 1)支持合约控制program addresses,使得外部用户无法为这些address生成有效的签名。
  • 2)支持合约通过代码来为 跨合约调用的instructions内的program addresses 进行签名。

基于以上2个优点,用户可安全地 transfer or assign the authority of on-chain assets to program addresses,然后该合约可assign the authority elsewhere at its discretion。

从而可将program derived address用于以下场景:

  • Decentralized Exchanges that transfer assets between matching bid and ask orders.
  • Auctions that transfer assets to the winner.
  • Games or prediction markets that collect and redistribute prizes to the winners.

3.1 program address 没有私钥

program address不在ed25519 curve上,因此,没有有效的私钥与其关联,从而使得无法为其生成签名。
由于其没有私钥合约可issue an instruction,将该program address作为a signer。

3.2 基于hash生成的program address

使用a 256-bit pre-image resistant hash function,根据a collection of seeds 以及 a program id,来确定性地生成program address。

program address必须不在ed25519 curve上,以确保没有关联的私钥。若发现生成的address在ed25519 curve上,则直接放回错误(对于特定的seeds和program id,这种情况发生的概率为50/50,若发生,可更换seed直到生成有效的program address)。

pub fn create_with_seed(
    base: &Pubkey,
    seed: &str,
    program_id: &Pubkey,
) -> Result<Pubkey, SystemError> {
    
    
    if seed.len() > MAX_ADDRESS_SEED_LEN {
    
    
        return Err(SystemError::MaxSeedLengthExceeded);
    }

    Ok(Pubkey::new(
        hashv(&[base.as_ref(), seed.as_ref(), program_id.as_ref()]).as_ref(),
    ))
}

create_program_address()并无法保证可生成off ed25519 curve的有效program address,可采用find_program_address()(可能会多次调用create_program_address())来确保生成的program address不在ed25519 curve上。
【根据 create_program_address()find_program_address() 生成的地址无法与其它公钥进行区分。runtime区分某address是否属于某合约 的唯一方法为:由合约提供生成该address的seed。然后runtime内部会调用create_program_address(),比对二者是否一样。】

/// Generate a derived program address
///     * seeds, symbolic keywords used to derive the key
///     * program_id, program that the address is derived for
pub fn create_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
) -> Result<Pubkey, PubkeyError>

/// Find a valid off-curve derived program address and its bump seed
///     * seeds, symbolic keywords used to derive the key
///     * program_id, program that the address is derived for
pub fn find_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
) -> Option<(Pubkey, u8)> {
    
    
    let mut bump_seed = [std::u8::MAX];
    for _ in 0..std::u8::MAX {
    
    
        let mut seeds_with_bump = seeds.to_vec();
        seeds_with_bump.push(&bump_seed);
        if let Ok(address) = create_program_address(&seeds_with_bump, program_id) {
    
    
            return Some((address, bump_seed[0]));
        }
        bump_seed[0] -= 1;
    }
    None
}

3.3 program address应用举例

客户端使用find_program_address来生成a destination address:

// find the escrow key and valid bump seed
let (escrow_pubkey2, escrow_bump_seed) = find_program_address(&[&["escrow2"]], &escrow_program_id);

// construct a transfer message using that key
let message = Message::new(vec![
    token_instruction::transfer(&alice_pubkey, &escrow_pubkey2, 1),
]);

// process the message which transfer one 1 token to the escrow
client.send_and_confirm_message(&[&alice_keypair], &message);

合约内采用相同的规则来生成相同的destination program address:

fn transfer_one_token_from_escrow2(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    
    
    // User supplies the destination
    let alice_pubkey = keyed_accounts[1].unsigned_key();

    // Iteratively derive the escrow pubkey
    let (escrow_pubkey2, bump_seed) = find_program_address(&[&["escrow2"]], program_id);

    // Create the transfer instruction
    let instruction = token_instruction::transfer(&escrow_pubkey2, &alice_pubkey, 1);

    // Include the generated bump seed to the list of all seeds
    invoke_signed(&instruction, accounts, &[&["escrow2", &[bump_seed]]])
}

参考资料

[1] Solana Calling Between Programs

猜你喜欢

转载自blog.csdn.net/mutourend/article/details/121788076