app可通过发送transaction来与Solana cluster交互,transaction中可包含一个或多个instruction。Solana runtime会将这些instruction发送给app开发者之前部署的program。
instruction可为:
每个transaction内的instructions为顺序原子执行的。若某一instruction为invalid的,则该笔交易内的所有account changes都将丢弃回滚。
当前支持的Solana cluster有:
当transaction提交到cluster,program开始执行。Solana runtime将execute a program to process each of the instructions contained in the transaction, in order, and atomically。
交易中包含了:
在solana/web3.js/src/transsaction.ts
中有:
class Transaction {
/**
* Signatures for the transaction. Typically created by invoking the
* `sign()` method
*/
signatures: Array = [];
/**
* The transaction fee payer
*/
feePayer?: PublicKey;
/**
* The instructions to atomically execute
*/
instructions: Array = [];
/**
* 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中的公钥。
签名格式:
instruction格式为:
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中包含:
Solana中的instruction会明确规定:
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。
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.
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.
每笔交易会明确列出该交易内instructions所引用的所有accout的public key。该public key列表的subset中的每一个会对应一个transaction signature。这些签名用于告知on-chain program,签名对应私钥的拥有者已授权了该交易。该program使用授权来允许对account进行借记或者修改数据。
交易中会包含一个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.
提交到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种支付方式:
Account::data
will be retained for continual access/usage。这种存储至少2年租金的account会标记为exempt。定义为2年是基于硬件成本每2年会减半。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
不收租金的条件为:
// updates this account's lamports and status and returns
// the account rent collected, if any
//
#[must_use = "add to Bank::collected_rent"]
pub fn collect_from_existing_account(
&self,
address: &Pubkey,
account: &mut AccountSharedData,
rent_for_sysvars: bool,
) -> u64 {
if account.executable() // executable accounts must be rent-exempt balance
|| account.rent_epoch() > self.epoch
|| (!rent_for_sysvars && sysvar::check_id(account.owner()))
|| *address == incinerator::id()
{
0
}
..........
}
有2个时间从account中收集rent:
但是,rent collection会错开以下时间:
尽管会错开以上时间,但是2)可保证最终可处理到所有manipulated accounts。
rent为按epoch收取的,account中的Accout::rent_epoch
值要么为current_epoch
,要么为current_epoch+1
,具体由rent regime决定。
Account::rent_epoch
会简单更新为current_epoch
。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.
在当前设计中,不可能有长期存在、从未接触过、从未支付过租金的账户。除了rent-exempt账户、sysvar账户和executable账户外,账户始终为每个epoch支付一次租金。
这是一个预期的设计选择。否则,任何可能从租金中获得不公平利润(当前leader)或在预期租金成本波动的情况下节省租金的人,都有可能在Noop
指令下触发未经授权的租金征收。
作为这种选择的另一个副作用,还要注意,这种定期收取租金的做法有效地迫使验证者不要乐观地将过期账户存储到冷库中,从而节省存储成本,这对账户所有者不利,并可能导致他们的交易比其他人拖延更长的时间。另一方面,这可以防止恶意用户创建大量垃圾帐户,加重验证程序的负担。
作为此设计的总体结果,所有帐户都平等地存储在Validator的working set中,并具有相同的性能特征,反映了统一的租金定价结构。
会根据需要收集rent(即whenever accounts were loaded/accessed)was considered,相应的方法有:
可以考虑通过System instruction来收集rent,因为其天然支持distribute rent to active and stake-weighted nodes and could have been done incrementally,但是:
[1] Solana Programming Model
[2] Solana rent设计
[3] Solana Storage rent economics