19_solana的SPL Token Mint创建流程

问题: 为什么创建token mint时,除了提供mint的pubkey之外,还需要提供mint的私钥来签名?

分析 spl-token库中的 createMint函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
export async function createMint(
connection: Connection,
payer: Signer,
mintAuthority: PublicKey,
freezeAuthority: PublicKey | null,
decimals: number,
keypair = Keypair.generate(),
confirmOptions?: ConfirmOptions,
programId = TOKEN_PROGRAM_ID
): Promise<PublicKey> {
const lamports = await getMinimumBalanceForRentExemptMint(connection);

const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: keypair.publicKey,
space: MINT_SIZE,
lamports,
programId,
}),
createInitializeMint2Instruction(keypair.publicKey, decimals, mintAuthority, freezeAuthority, programId)
);

await sendAndConfirmTransaction(
connection,
transaction,
[
payer, // 支付手续费
keypair // 问题:为什么需要提供 mint的私钥签名?
],
confirmOptions
);

return keypair.publicKey;
}



export function createInitializeMint2Instruction(
mint: PublicKey,
decimals: number,
mintAuthority: PublicKey,
freezeAuthority: PublicKey | null,
programId = TOKEN_PROGRAM_ID
): TransactionInstruction {


const keys = [{
pubkey: mint,
isSigner: false,
isWritable: true // 为什么是Writeable?
}];

const data = Buffer.alloc(initializeMint2InstructionData.span);
initializeMint2InstructionData.encode(
{
instruction: TokenInstruction.InitializeMint2,
decimals,
mintAuthority,
freezeAuthority,
},
data
);

return new TransactionInstruction({ keys, programId, data });
}



其中, 包含了2条指令

我们逐个分析

首先我们分析 SystemProgram.createAccount的源码

客户端源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

static createAccount(params: CreateAccountParams): TransactionInstruction;

type CreateAccountParams = {
/** The account that will transfer lamports to the created account */
fromPubkey: PublicKey;
/** Public key of the created account */
newAccountPubkey: PublicKey;
/** Amount of lamports to transfer to the created account */
lamports: number;
/** Amount of space in bytes to allocate to the created account */
space: number;

// 这里 programId, 即 TOKEN_PROGRAM_ID , TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
/** Public key of the program to assign as the owner of the created account */
programId: PublicKey;
};

根据代码注释可知, 其中 programId被用作 owner, 即 Token Mint账户的owner, 即: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#[allow(clippy::too_many_arguments)]
fn create_account(
from_account_index: IndexOfAccount,
to_account_index: IndexOfAccount,
to_address: &Address,
lamports: u64,
space: u64,
owner: &Pubkey, // 根据上面分析可知,owner即是 TOKEN_PROGRAM_ID
signers: &HashSet<Pubkey>,
invoke_context: &InvokeContext,
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
) -> Result<(), InstructionError> {
// if it looks like the `to` account is already in use, bail
{
let mut to = instruction_context
.try_borrow_instruction_account(transaction_context, to_account_index)?;

// 如果账户已经存在,则不能创建
if to.get_lamports() > 0 {
ic_msg!(
invoke_context,
"Create Account: account {:?} already in use",
to_address
);
return Err(SystemError::AccountAlreadyInUse.into());
}
// 注意, 到此处为止, token mint的owner 是 SYSTEM_PROGRAM_ID

// 分配空间, 并指派owner权限
allocate_and_assign(&mut to, to_address, space, owner, signers, invoke_context)?;
}

// 注意, 到这里为止, token mint的owner 已经是 owner, 即 TOKEN_PROGRAM_ID

// 交租金
transfer(
from_account_index,
to_account_index,
lamports,
invoke_context,
transaction_context,
instruction_context,
)
}

// 源码 https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/programs/system/src/system_processor.rs#L132
fn allocate_and_assign(
to: &mut BorrowedAccount,
to_address: &Address,
space: u64,
owner: &Pubkey,
signers: &HashSet<Pubkey>,
invoke_context: &InvokeContext,
) -> Result<(), InstructionError> {
// 为新账户分配空间
allocate(to, to_address, space, signers, invoke_context)?;

// 为新账户指派owner
assign(to, to_address, owner, signers, invoke_context)


}

// 源码 https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/programs/system/src/system_processor.rs#L70
fn allocate(
account: &mut BorrowedAccount,
address: &Address,
space: u64,
signers: &HashSet<Pubkey>,
invoke_context: &InvokeContext,
) -> Result<(), InstructionError> {

// 需要 Token Mint账户的签名
if !address.is_signer(signers) {
ic_msg!(
invoke_context,
"Allocate: 'to' account {:?} must sign",
address
);
return Err(InstructionError::MissingRequiredSignature);
}

// if it looks like the `to` account is already in use, bail
// (note that the id check is also enforced by message_processor)
if !account.get_data().is_empty() || !system_program::check_id(account.get_owner()) {
ic_msg!(
invoke_context,
"Allocate: account {:?} already in use",
address
);
return Err(SystemError::AccountAlreadyInUse.into());
}

if space > MAX_PERMITTED_DATA_LENGTH {
ic_msg!(
invoke_context,
"Allocate: requested {}, max allowed {}",
space,
MAX_PERMITTED_DATA_LENGTH
);
return Err(SystemError::InvalidAccountDataLength.into());
}

// 设置账户空间
account.set_data_length(space as usize)?;

Ok(())
}


// 源码 https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/programs/system/src/system_processor.rs#L112
fn assign(
account: &mut BorrowedAccount,
address: &Address,
owner: &Pubkey,
signers: &HashSet<Pubkey>,
invoke_context: &InvokeContext,
) -> Result<(), InstructionError> {
// no work to do, just return
if account.get_owner() == owner {
return Ok(());
}

// 需要 Token Mint账户的签名
if !address.is_signer(signers) {
ic_msg!(invoke_context, "Assign: account {:?} must sign", address);
return Err(InstructionError::MissingRequiredSignature);
}

// 设置owner
account.set_owner(&owner.to_bytes())
}

从上面2处对Token Mint的账户判断 address.is_signer(signers), 即需要token mint的签名。

到此为止, token mint的账户已经完成了创建,并且AccountInfo中的owner已经设置为 TOKEN_PROGRAM_ID

接下来,需要对 Token Mint账户的 AccountInfo中的data进行初始化,即对 Mint进行初始化

因为,此时 Token mint的账户的owner是 TOKEN_PROGRAM_ID, 因此, TOKEN程序是有权限直接修改 token mint的

解析来我们再分析createInitializeMint2Instruction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

/// Processes an [InitializeMint2](enum.TokenInstruction.html) instruction.
pub fn process_initialize_mint2(
accounts: &[AccountInfo],
decimals: u8,
mint_authority: Pubkey,
freeze_authority: COption<Pubkey>,
) -> ProgramResult {
Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, false)
}

fn _process_initialize_mint(
accounts: &[AccountInfo],
decimals: u8,
mint_authority: Pubkey,
freeze_authority: COption<Pubkey>,
rent_sysvar_account: bool,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();

//
let mint_info = next_account_info(account_info_iter)?;
let mint_data_len = mint_info.data_len();
let rent = if rent_sysvar_account {
Rent::from_account_info(next_account_info(account_info_iter)?)?
} else {
Rent::get()?
};

let mut mint = Mint::unpack_unchecked(&mint_info.data.borrow())?;
if mint.is_initialized {
return Err(TokenError::AlreadyInUse.into());
}

if !rent.is_exempt(mint_info.lamports(), mint_data_len) {
return Err(TokenError::NotRentExempt.into());
}

mint.mint_authority = COption::Some(mint_authority);
mint.decimals = decimals;
mint.is_initialized = true;
mint.freeze_authority = freeze_authority;


// 将mint结构体,序列化到 mint_info.data, 这里需要提供mint_info的写入权限
Mint::pack(mint, &mut mint_info.data.borrow_mut())?;

Ok(())
}

至此,我们完整地分析了Token Mint的创建细节。

总结一下:

  • 第1步, 通过调用系统程序的 create_account 创建新的mint账户,并分配空间,转入租金,并将owner设置为 Token Program
  • 第2步, 通过 Token Program的 process_initialize_mint2 指令,对mint账户进行初始化

最后,我们回答一下,文章开头的问题:

问:为什么创建token mint账户时,除了提供mint的pubkey之外,还需要提供mint的私钥来签名?
答:因为在第1步调用系统程序的create_account时, 转移mint账户权限时需要校验账户签名,因此需要传入 mint的私钥。在create_account结束之后,mint账户的owner已经变成了 Token Program, 此后,就不再需要 mint的私钥了。


(完)

微信小程序爬虫

这里以 Windows 微信小程序作为分析对象。

分析步骤:

  • 通过 Charles 抓包分析微信小程序的鉴权和api路径
  • 获取登陆态token
  • 使用token请求api

Charles 安装并设置

  • 重启 Charles, 打开PC微信

PC(Windows)微信小程序抓包

打开微信小程序

关于微信授权登录的流程

官方文档: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

实际案例:微信小程序登录

授权登录

业务接口:

直接使用token请求api

编写python程序请求api

略。 和普通python爬虫一样。

逆向分析pump.fun的BondingCurve算法

找到 div id, 或者 用报错文本 进行全局搜索

全局搜索

可以看到 onChange事件处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
onChange:  es ? e=>{
if (et)
return;
let t = parseFloat(e.target.value);
if (isNaN(t)) {
g(""),
f("");
return
}
g(t),
f(I(new ex.BN(Math.floor(1e9 * t)), !0).toNumber() / 1e6)
}
: e=>{
let t;
if (et)
return;
let n = parseFloat(e.target.value);
if (isNaN(n)) {
f(""),
g("");
return
}
f(n);
let s = new ex.BN(1e6 * n);
t = u ? I(s, !1) : A(s),
g((0,
p.s)(t))
}
})

可知, 函数I正是算法函数的实现, 接下来,需要定位 I函数的位置,

我们在 onChange函数中打两个断点, 然后在输入框输入数量,触发执行到断点处

此时,将鼠标放置在I上就可以查看函数的位置:

或者,直接在调试窗口的下方控制台,直接输入 I, 然后双击输出, 也可以查看I的定义,

至此,我们已经找到pump.fun的bonding curve的算法实现函数:

根据前面的分析,

  • 参数 n 是个bool值, 表示的是按照sol还是按照token买入
  • 参数 e 是数量

因此,
其中 i 是 bigint库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 // 买入
function x(e,n)=>{
let s, a;
if (e.eq(new i.BN(0)) || !t)
return new i.BN(0);
if (n) {
// 按照 sol数量买入
let n = t.virtualSolReserves.mul(t.virtualTokenReserves)
, r = t.virtualSolReserves.add(e)
, o = n.div(r).add(new i.BN(1));
a = t.virtualTokenReserves.sub(o),
a = i.BN.min(a, t.realTokenReserves),
s = e
} else
// 按照 token数量买入
s = (e = i.BN.min(e, t.realTokenReserves)).mul(t.virtualSolReserves).div(t.virtualTokenReserves.sub(e)).add(new i.BN(1)),
a = e;
let r = _(s); // 手续费
return n ? a : s.add(r) //SOL加上手续费
}

// 卖出
sellQuote: e=>{
if (e.eq(new i.BN(0)) || !t)
return new i.BN(0);
let n = e.mul(t.virtualSolReserves).div(t.virtualTokenReserves.add(e))
, s = _(n); // 手续费
return n.sub(s) // 扣除手续费
}

Bonding Curve公式

特别说明:

  • 前端计算使用BN:,
  • 合约计算过程中使用 u128 , 最终计算结果保存在数据账户使用u64即可

用户买入Token

买入公式(按照SOL数量)
$$ \Delta{t} = T_v - \bigg ( \frac{S_v \times T_v}{S_v + \Delta{s}} + 1 \bigg) $$

买入公式(按照Token数量)

$$ \Delta{s} = \frac{\Delta{t} \times S_v }{T_v - \Delta{t}} + 1 $$

买入后状态更新:

用户卖出Token

$$ \Delta{s} = \frac{\Delta{t} \times S_v }{T_v + \Delta{t}} $$

卖出后状态更新:


推导过程

基础公式

买入Token(按照token数量)

$$(T_v - \Delta t)(S_v + \Delta s) = k = T_v \times S_v$$

$$\Delta t = T_v - \frac{T_v \times S_v}{S_v + \Delta s}$$

用户买入Token(按照SOL)

$$(T_v + \Delta t)(S_v - \Delta s) = k = T_v \times S_v$$

$$\Delta s = S_v - \frac{T_v \times S_v}{T_v + \Delta t}$$

$$\Delta s = \frac{S_v \times (T_v +\Delta t) - T_v \times S_v}{T_v + \Delta t} $$

$$\Delta s = \frac{S_v \times T_v + S_v \times \Delta t - T_v \times S}{T_v + \Delta t} $$

$$\Delta s = \frac{S_v \times \Delta t }{T_v + \Delta t} $$

价格

$$p = \frac{S_v}{T_v}$$

18_Solana高级交易Durable_Nonce

官方文档:

本质问题: 如何避免双花?

Recent Blockhash 做了时间戳,也充当了唯一标识(类似ETH的nonce)的作用, 防止双花

有了 Recent Blockhash 为什么还需要 Durable Nonce?

Recent Blockhash 的窗口是 150个区块(约 150 * 0.4 = 60s), 因此,签名之后的交易必须在一分钟内被提交执行,否则交易就会过期。

几个特殊场景:

  • 大批量交易, 不想因为blockhash重复而失败 ?
  • 多重签名交易?
  • 离线签名?

因此,就需要 Durable Nonce 方案, nonce 是 32字节, 其作用就是确保交易的唯一

  • Copyrights © 2021-2024 youngqqcn

请我喝杯咖啡吧~