Solana的programming model

Posted mutourend

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solana的programming model相关的知识,希望对你有一定的参考价值。

1. 引言

app可通过发送transaction来与Solana cluster交互,transaction中可包含一个或多个instruction。Solana runtime会将这些instruction发送给app开发者之前部署的program。
instruction可为:

  • tell a program to transfer lamports from one account to another。
  • create an interactive contract that governs how lamports are transferred。

每个transaction内的instructions为顺序原子执行的。若某一instruction为invalid的,则该笔交易内的所有account changes都将丢弃回滚。

当前支持的Solana cluster有:

  • devnet: https://api.devnet.solana.com
  • testnet:https://api.testnet.solana.com
  • mainnet-beta:https://api.mainnet-beta.solana.com
  • local:使用solana-test-validator工具,配置http://127.0.0.1:8899

2. Solana中的transaction

当transaction提交到cluster,program开始执行。Solana runtime将execute a program to process each of the instructions contained in the transaction, in order, and atomically。

交易中包含了:

  • a compact-array of signatures:在该array中的每个元素为a digital signature of the given message。Solana runtime会验证签名的数量 与 message header前8bit代表的数量是否一致。同时Solana runtime会验证每个签名 是否 与 message的account addresses array中的the same index public key 一致。
  • a message:

solana/web3.js/src/transsaction.ts 中有:

class Transaction {
  /**
   * Signatures for the transaction.  Typically created by invoking the
   * `sign()` method
   */
  signatures: Array<SignaturePubkeyPair> = [];
  /**
   * The transaction fee payer
   */
  feePayer?: PublicKey;
  /**
   * The instructions to atomically execute
   */
  instructions: Array<TransactionInstruction> = [];
  /**
   * A recent transaction id. Must be populated by the caller
   */
  recentBlockhash?: Blockhash;
  /**
   * Optional Nonce information. If populated, transaction will use a durable
   * Nonce hash instead of a recentBlockhash. Must be populated by the caller
   */
  nonceInfo?: NonceInformation;
........  
}

solana/web3.js/src/message.ts 中有:

/**
 * Message constructor arguments
 */
export type MessageArgs = {
  /** The message header, identifying signed and read-only `accountKeys` */
  header: MessageHeader;
  /** All the account keys used by this transaction */
  accountKeys: string[];
  /** The hash of a recent ledger block */
  recentBlockhash: Blockhash;
  /** Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. */
  instructions: CompiledInstruction[];
};

详细的测试可参看solana/rpc/src/rpc.rs中的test_rpc_simulate_transaction,会模拟一笔交易。

account address format:
account address为32字节的任意数据。当用于签名时,Solana runtime会将其解析为ed25519 keypair中的公钥。

签名格式:

  • 每个数字签名为ed25519 binary format,对应为64 bytes。

instruction格式为:

  • a program id index:为an unsigned 8-bit index to an account address in message’s array of account addresses。
  • a compact-array of account address indexes:每个都为an unsigned 8-bit index into the same array。
  • a compact-array of opaque(不透明的) 8-bit data

compact-array 格式为:
a compact-array is serialized as the array length, followed by each array item。其中array length为特殊的multi-byte encoding——名为compact-u16。

compact-u16格式为:
A compact-u16 is a multi-byte encoding of 16 bits. The first byte contains the lower 7 bits of the value in its lower 7 bits. If the value is above 0x7f, the high bit is set and the next 7 bits of the value are placed into the lower 7 bits of a second byte. If the value is above 0x3fff, the high bit is set and the remaining 2 bits of the value are placed into the lower 2 bits of a third byte.

message中包含:

  • message header:包含3个unsigned 8-bit values。第一个value为交易中所需的签名数,第二个为只读的account address数,第三为只读但不需要签名的account address数。
  • a compact array of account addresses:有签名要求的address将在account address array的靠前位置,后面是需要write access的address,最后是只读(无签名要求)account。
  • a recent blockhash:为32-byte的SHA-256 hash。用于表明when a client last observed the ledger。Validators将reject transactions when the blockhash is too old。
  • a compact-array of instructions:

2.1 Solana交易中的instruction

Solana中的instruction会明确规定:

  • a single program:该program会解析data array,并根据instruction对accounts做相应操作。program要么成功执行,要么返回error code,若返回错误,则整笔交易立刻fail。
  • a subset of the transaction’s accounts that should passed to the program
  • a data byte array that is passed to the program

program会提供helper function来指导如何构建其所支持的instructions。

instruction中的program id会明确规定由哪个program来处理该instruction。该program‘s account’s owner specifies which loader should be used to load and execute the program and the data contains information about how the runtime should execute the program。

对于on-chain BPF program,the owner is the BPF Loader and the account data holds the BPF bytecode。Program accounts are permanently marked as executable by the loader once they are successfully deployed. The runtime will reject transactions that specify programs that are not executable。

与on-chain program不同,Native program可直接被built into the Solana runtime。

instruction中引用的accounts,代表的是on-chain state,可同时作为a program的input和output。

2.1.1 instruction data

Each instruction caries a general purpose byte array that is passed to the program along with the accounts. The contents of the instruction data is program specific and typically used to convey what operations the program should perform, and any additional information those operations may need above and beyond what the accounts contain.

Programs are free to specify how information is encoded into the instruction data byte array. The choice of how data is encoded should take into account the overhead of decoding since that step is performed by the program on-chain. It’s been observed that some common encodings (Rust’s bincode for example) are very inefficient.

The Solana Program Library’s Token program gives one example of how instruction data can be encoded efficiently, but note that this method only supports fixed sized types. Token utilizes the Pack trait to encode/decode instruction data for both token instructions as well as token account states.

2.1.2 multiple instructions in a single transaction

A transaction can contain instructions in any order. This means a malicious user could craft transactions that may pose instructions in an order that the program has not been protected against. Programs should be hardened to properly and safely handle any possible instruction sequence.

One not so obvious example is account deinitialization. Some programs may attempt to deinitialize an account by setting its lamports to zero, with the assumption that the runtime will delete the account. This assumption may be valid between transactions, but it is not between instructions or cross-program invocations. To harden against this, the program should also explicitly zero out the account’s data.

An example of where this could be a problem is if a token program, upon transferring the token out of an account, sets the account’s lamports to zero, assuming it will be deleted by the runtime. If the program does not zero out the account’s data, a malicious user could trail this instruction with another that transfers the tokens a second time.

2.2 Solana交易中的签名

每笔交易会明确列出该交易内instructions所引用的所有accout的public key。该public key列表的subset中的每一个会对应一个transaction signature。这些签名用于告知on-chain program,签名对应私钥的拥有者已授权了该交易。该program使用授权来允许对account进行借记或者修改数据。

2.3 Solana交易中的recent blockhash

交易中会包含一个recent blockhash来防止duplication,同时给transaction lifetime。Any transaction that is completely identical to a previous one is rejected, so adding a newer blockhash allows multiple transactions to repeat the exact same action. Transactions also have lifetimes that are defined by the blockhash, as any transaction whose blockhash is too old will be rejected.

3. rent

提交到Solana ledger中的每笔交易都将增加成本。
交易费是由submitter来支付的,并由validator收集,交易费用于支付the acute, transactional, costs of validating and adding that data to the ledger。
然而,交易费并未覆盖 由 the rotating validator set维护的 the mid-term storage of active ledger state 的成本,这种成本不仅会增加validator的成本,也会随着网络运行active state的增加,而增加data transmission and validation overhead。
为此,Solana中设计了storage rent来覆盖这些成本。

Solana中的accounts可以有owner-controlled state (Account::data),独立于account的balance (Account::lamports)。由于网络中的Validators需要在memory中维护a working copy of this state,因此以租金的方式来charge a time-and-space based fee for this resource consumption。

Storage rent有2种支付方式:

  • 方式一,set it and forget it:此时accounts会存入价值2年的租金。通过维护该租金,可减少流动性,同时account holder 可trust that their Account::data will be retained for continual access/usage。这种存储至少2年租金的account会标记为exempt。定义为2年是基于硬件成本每2年会减半。
  • 方式二,pay per byte:若an account的租金不足2年,可在每个epoch的基础上支付租金,在当前epoch支付下一epoch的租金。租金的扣除方式是由创世块中定义的lamports per kilobyte-year来计算的。Account::rent_epoch会跟踪下次从该account收集rent的事件。

当前rent cost是固定在创世块中的,未来会调整为动态的,以反映实际的硬件存储成本,随着技术的进步,rent cost应随着硬件成本的降低而减少。

不同于以太坊和比特币,为了keep accounts alive,Solana中引入了称为rent的storage cost。
当前主网和测试网的fixed rent fee为19.055441478439427 lamports per byte-epoch。一个epoch对应约2天。(1 lamport=10^{-9} SOL)
Rent计算中会包含account metadata(address, owner, lamports等等)的size,因此,最小的account,其rent计算的大小为128bytes,对应一个epoch的租金为2439 lamports。
而对于具有15000bytes的account,其每个epoch的租金为0.00028827 6SOL,2年的租金为0.10529088 SOL。

# ./solana rent 15000
Rent per byte-year: 0.00000348 SOL
Rent per epoch: 0.000288276 SOL
Rent-exempt minimum: 0.10529088 SOL

3.1 从account 收集rent的时间

有2个时间从account中收集rent:

  • 1)当被一笔交易引用时:包含the transaction to create the new account itself, and it happens during the normal transaction processing by the bank as part of the load phase.
  • 2)periodically once an epoch:用于保证从stale accounts中收集rent,尽管这些accounts未在近期的epochs中引用。此时,会全局扫描所有accounts,同时会分散在整个epoch,根据account address prefix来避免rent collection中的尖峰加载。

但是,rent collection会错开以下时间:

  • the distribution of rent collection itself (否则,可能会导致重复收集rent)
  • the distribution of staking rewards at the start of every epoch (尽可能减少每个新epoch初期的处理尖峰)
  • the distribution of transaction fee at the end of every slot

尽管会错开以上时间,但是2)可保证最终可处理到所有manipulated accounts。

3.2 实际收集rent的流程

rent为按epoch收取的,account中的Accout::rent_epoch值要么为current_epoch,要么为current_epoch+1,具体由rent regime决定。

  • 若account为exempt regime的,Account::rent_epoch会简单更新为current_epoch
  • 若account为non-exempt regime,the difference between the next epoch and Account::rent_epoch 用于计算the amount of rent owed by this account (via Rent::due())。计算结果中的小数位会去掉。租金会从Account::lamports中扣除,同时Account::rent_epoch会更新为current_epoch+1 (= next epoch)。若租金值低于1 lamport,则不会有任何资金扣除。

balance不足以支付租金的账号,将fail to load。

收集到的rent的一部分会销毁掉,另外一部分会在每一个slot结束时,根据质押权重分发给validator accounts。

Finally, rent collection happens according to the protocol-level account updates like the rent distribution to validators, meaning there is no corresponding transaction for rent deductions. So, rent collection is rather invisible, only implicitly observable by a recent transaction or predetermined timing given its account address prefix.

3.3 rent设计思想

3.3.1 rent当前设计原理

在当前设计中,不可能有长期存在、从未接触过、从未支付过租金的账户。除了rent-exempt账户、sysvar账户和executable账户外,账户始终为每个epoch支付一次租金。
这是一个预期的设计选择。否则,任何可能从租金中获得不公平利润(当前leader)或在预期租金成本波动的情况下节省租金的人,都有可能在Noop指令下触发未经授权的租金征收。

作为这种选择的另一个副作用,还要注意,这种定期收取租金的做法有效地迫使验证者不要乐观地将过期账户存储到冷库中,从而节省存储成本,这对账户所有者不利,并可能导致他们的交易比其他人拖延更长的时间。另一方面,这可以防止恶意用户创建大量垃圾帐户,加重验证程序的负担。

作为此设计的总体结果,所有帐户都平等地存储在Validator的working set中,并具有相同的性能特征,反映了统一的租金定价结构。

3.3.2 Ad-hoc collection

会根据需要收集rent(即whenever accounts were loaded/accessed)was considered,相应的方法有:

  • 交易中以“credit only”方式加载的accounts,可认为是have rent due,但是相应的交易中不可writable。
  • 一种“打破僵局”(即寻找需要支付租金的账户)的机制是可取的,以免不经常加载的帐户获得免费访问。

3.3.3 收集rent的System instruction

可以考虑通过System instruction来收集rent,因为其天然支持distribute rent to active and stake-weighted nodes and could have been done incrementally,但是:

  • 它会影响网络吞吐量
  • 需要在runtime中支持special-casing,因为accounts with non-SystemProgram owners may be debited by this instruction
  • someone would have to issue the transactions

参考资料

[1] Solana Programming Model
[2] Solana rent设计
[3] Solana Storage rent economics

以上是关于Solana的programming model的主要内容,如果未能解决你的问题,请参考以下文章

Solana中的native program

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

Solana区块链智能合约开发简要流程

Solana区块链智能合约开发简要流程

Solana区块链智能合约开发简要流程

Solana中的托管合约