Solana 是一个备受欢迎的区块链平台,以其高吞吐量和低交易成本而闻名。然而,在 Solana 的生态系统中,有一项技术可能被忽视,但却是其去中心化金融(DeFi)和其他应用程序的关键组成部分:程序派生地址(Program Derived Addresses,简称 PDA)。
1.什么是程序派生地址(PDA)
程序派生地址是 Solana 区块链上的一种特殊账户类型,只有特定的程序(program_id)有权签名。这些地址采用与 Solana 公钥相同的格式,但确保不在 ed25519 曲线上存在对应的私钥。PDA 的设计目的是为了允许程序在调用其他程序时以编程方式生成签名,从而实现自动化的交易逻辑。
PDA的两个好处:
- PDA 可以作为用户拥有的每个代币的身份代币帐户(相对于铸币厂Mint)
- 代币可以直接使用用户的 PDA 作为传输目的地发送给任何用户,例如Airdrop。
PDA 在 Solana 生态系统中具有广泛的应用,包括但不限于以下方面:
- 去中心化金融(DeFi)应用程序:PDA 可以用于托管资产、执行智能合约以及实现自动化的交易策略。
- 去中心化交易所:PDA 可以用于执行匹配买卖订单之间的资产转移,从而实现去中心化交易。
- 拍卖和游戏应用程序:PDA 可以用于收集奖品并将其重新分配给获胜者,实现公平和透明的游戏或拍卖。
2.PDA的生成方式
由于程序地址不能位于 ed25519 曲线上,因此可能存在无效的种子和程序 ID 组合。为了克服这一问题,会计算额外的种子(称为凹凸种子),以产生偏离曲线的点。寻找有效程序地址的过程是通过反复尝试实现的,即使在给定一组输入的情况下它是确定性的,但在不同的输入上成功需要不同的时间。这意味着当从链上调用程序时,可能会产生可变数量的程序计算预算。想要高性能的程序可能不愿使用此函数,因为它可能需要相当长的时间。已经面临超出计算预算风险的程序应谨慎调用此方法,因为程序的预算可能会偶尔且不可预测地超出。
由于链上 Solana 程序访问的所有账户地址都必须显式传递给该程序,因此 PDA 通常在链外客户端程序中派生,从而避免了生成链上地址的计算成本。一般的使用分成两种形式:
- 在链外创建PDA账户,然后传递给合约使用,例如ATA账户。
/**
* Async version of findProgramAddressSync
* For backwards compatibility
*
* @deprecated Use {@link findProgramAddressSync} instead
*/
static async findProgramAddress(
seeds: Array<Buffer | Uint8Array>,
programId: PublicKey,
): Promise<[PublicKey, number]> {
return this.findProgramAddressSync(seeds, programId);
}
- 在合约内部创建PDA账户,只有该合约可以使用,例如存管NFT账户。
pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
Self::try_find_program_address(seeds, program_id)
.unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"))
}
3.ATA账户案例
(1)创建代币
luca@LucadeMacBook-Air ionet % spl-token create-token
Creating token vJELotX5KpAoPpCVPXB6QXpUkqMdA14me9JqSWEjJ1r under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Address: vJELotX5KpAoPpCVPXB6QXpUkqMdA14me9JqSWEjJ1r
Decimals: 9
Signature: 51YBgrs4xSragi9Mqc4AHtJMs1xfMxZr33a7DFhvy5AQvkdK1GxVXMzbTJNDEUjp3kWMHS9ry2JDU3HAaxKZ192u
(2)创建ATA账户
luca@LucadeMacBook-Air ionet % spl-token create-account vJELotX5KpAoPpCVPXB6QXpUkqMdA14me9JqSWEjJ1r
Creating account 3E4UDwxVrKULutSt6ckBsVmg8XHuRsm7MVAnDq4RY4t9
Signature: 3hFg8m1dg8K98aiCsVsyiV4bkkVWXZCsEg4WJF9t9Z4vhDzGa22NSvaEW93crJTqpQrvQ2X41g7cjFyM1x7DJ8YU
这里的3E4UDwxVrKULutSt6ckBsVmg8XHuRsm7MVAnDq4RY4t9账号就是我们的地址子在vJELotX5KpAoPpCVPXB6QXpUkqMdA14me9JqSWEjJ1r token下的ATA账号。
生成方式:
ATA账户地址是用 SOL钱包地址,目标代币地址以及SPL-Token地址作为Seed,在SPL-Token合约下生成的。
Pubkey::find_program_address(
&[
&wallet_address.to_bytes(),
&token_program_id.to_bytes(),
&token_mint_address.to_bytes(),
],
program_id,
)
接下来,我们可以用一个Rust程序来验证一下生成方式:
use std::str::FromStr;
use solana_program::pubkey::Pubkey;
fn main() {
// sol钱包地址
let sol_addr: Pubkey = Pubkey::from_str("E3NAyseUZrgwSbYe2g47TuFJr5CxAeneK63mR4Ufbqhh").unwrap();
// SPL Token官方地址
let spl_token_addr : Pubkey= Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap();
// 创建好的SPl Token代币Mint地址
let token_addr: Pubkey = Pubkey::from_str("vJELotX5KpAoPpCVPXB6QXpUkqMdA14me9JqSWEjJ1r").unwrap();
let ata_program_addr: Pubkey = Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap();
let seeds = [
&sol_addr.to_bytes()[..],
&spl_token_addr.to_bytes()[..],
&token_addr.to_bytes()[..],
];
let (ata_addr ,_seed )= Pubkey::find_program_address(&seeds[..], &ata_program_addr);
println!("ata_addr is {}", ata_addr);
println!("_seed is {}", _seed);
}
运行结果:
luca@LucadeMacBook-Air src % cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.37s
Running `/Users/luca/dev/rust-workspace/helloworld/target/debug/helloworld`
ata_addr is 3E4UDwxVrKULutSt6ckBsVmg8XHuRsm7MVAnDq4RY4t9
_seed is 255