关于区块链共识机制
pow、pos、poa、poh
Solana 全方位介绍 —— 共识、钱包、生态、合约 | 登链社区 | 区块链技术社区
指南:用 Anchor 构建 Solana 程序 | 登链社区 | 区块链技术社区
3 分钟在 Solana 链创建代币教程:无代码发币平台 PandaTool 支持 | 登链社区 | 区块链技术社区
新的共识机制 —— 历史证明机制(PoH)
世界上最快的 “高速公链”——Solana
Solana 的基础共识为 PoS(即常见的权益证明机制),简单来说,就是根据用户持有货币的多少和时间(币龄)来决定发放利息额度的制度。
历史证明机制(PoH)是 Solana 一个巧妙又实用性极高的创新。传统的区块链,比如比特币和以太坊,将时间和状态耦合在一起,只有新区块诞生才能产生全局一致的状态。Solana 则巧妙得将基于哈希的时间链和状态分离,不是将每个区块的哈希链接在一起,而是网络中的验证者对区块内的哈希本身进行哈希,这种机制就是 PoH(Proof of history)。
Solana 开发学习笔记 (一)—— 从 Hello World 出发 | 登链社区 | 区块链技术社区
Solana 编程模型:Solana 开发入门 | 登链社区 | 区块链技术社区
集群#
是 Solana 架构的核心,一组验证者共同处理交易并维护单个分类账(ledger)。Solana 有几个不同的集群,每个集群都有特定的用途:
本地主机:默认端口 8899 的本地开发集群。Solana 命令行界面 (CLI) 有一个内置的测试验证者,根据开发者个人需求可定制,无需空投没有速率限制。
开发网络 Devent:进行测试和实验的无价值环境
测试网络 Testnet:核心人员实验新更新和功能的场所,也可进行性能测试。
主网测试版 Mainnet Beta:实时,无许可,产生真实的货币交易的场所。
Solana 账户#
- 存储数据的账户
- 存储可执行程序的账户
- 存储原生程序的账户
根据功能可以区分为:
- 可执行账户
- 不可执行账户 (不包含代码)
每个不可执行账户还有不同类型:
- 关联 Token 账户 - 一个包含特定代币信息、其余额和所有者信息的账户(例如,Alice 拥有 10 个 USDC)
- 系统账户 - 由系统程序创建和拥有的账户
- 质押账户 - 用于将代币委托给验证者以潜在获得奖励的账户
账户结构#
pub struct AccountInfo<'a> {
pub key: &'a Pubkey,
pub lamports: Rc>,
pub data: Rc>,
pub owner: &'a Pubkey,
pub rent_epoch: Epoch,
pub is_signer: bool,
pub is_writable: bool,
pub executable: bool,
}
账户通过其地址(key)进行标识,这是一个唯一的 32 字节公钥。
lamports 字段保存着该账户拥有的lamports数量。一个 lamport 等于 Solana 的原生代币 SOL 的十亿分之一。
data 指的是由该账户存储的原始数据字节数组。它可以存储从数字资产的元数据到代币余额等任何内容,并可由程序进行修改。
owner 字段包含了此账户的所有者,由程序账户的地址表示。关于账户所有者有一些规则:
- 只有账户的所有者才能更改其数据并提取 lamports
- 任何人都可以向账户存入 lamports
- 账户的所有者可以将所有权转移给新所有者,前提是账户的数据被重置为零
rent_epoch 字段指示此账户将在下一个 epoch 时期欠租金。一个epoch是 leader 调度的插槽数。与操作系统中的传统文件不同,Solana 上的账户具有以 lamports 表示的寿命。账户的持续存在取决于其 lamport 余额,这让我们引入了租金的概念。
is_signer 字段是一个布尔值,指示交易是否已由涉及账户的所有者签名。换句话说,它告诉交易中涉及的程序账户,账户是否是签名者。作为签名者意味着账户持有公钥对应的私钥,并有权批准提议的交易。
is_writable 字段是一个布尔值,指示账户的数据是否可以修改。Solana 允许交易将账户指定为只读,以促进并行处理。虽然运行时允许不同程序同时访问只读账户,但它使用交易处理顺序处理潜在的可写账户写入冲突。这确保只有非冲突的交易可以并行处理。
executable 字段是一个布尔值,指示账户是否可以处理指令。是的,这意味着程序存储在账户中,我们将在下一节中深入探讨这一点。首先,我们需要介绍租金的概念。
租金 (Rent)#
保持账户活跃并确保账户保存在验证者内存中而产生的存储成本。租金的收取取决于 epoch 评估,他是由时间段定义的时间单位。
- 租金收取 - 租金每个 epoch 收取一次。当账户被交易引用时,也可以收取租金
- 租金分配 - 收取的一部分租金被销毁,意味着它被永久性地从流通中移除。其余部分在每个插槽后分配给投票账户
- 租金支付 - 如果一个账户没有足够的 lamports 来支付租金,那么它的数据将被移除,并且账户将在一个被称为垃圾回收的过程中被取消分配
- 租金豁免 - 如果账户保持等于两年租金支付的最低余额,则账户可以成为租金豁免。所有新账户必须满足此租金豁免门槛,这取决于账户的大小
- 租金检索 - 用户可以关闭一个账户以取回其剩余的 lamports。这允许用户检索存储在账户中的租金
可以使用 getMinimumBalanceForRentExemption RPC 端点来估算特定账户大小的租金。Test Drive通过接受 usize 中的账户数据长度来简化此过程。Solana rent CLI 子命令也可以用于估算账户成为租金豁免所需的最低 SOL 金额。例如,在撰写本文时,运行命令 solana rent 20000 将返回租金豁免最低值:0.14009088 SOL。
Solana 地址#
Solana 上有两种 “类型” 地址。
Solana 使用ed25519,一种使用SHA-512(SHA-2)和Curve22519椭圆曲线的 EdDSA 签名方案来创建地址。 生成 32 字节的公钥,它们作为主要地址格式可以直接使用,因为它们没有被哈希。
为了使地址有效,它必须是ed25519曲线上的一个点。然而,并非所有地址都需要从此曲线派生。程序派生地址(PDA)是在曲线之外生成的,这意味着它们没有对应的私钥,也不能用于签名。PDAs 是通过系统程序创建的,当程序需要管理账户时使用。
Solana 账户与以太坊账户的不同#
以太坊包含两种账户 (EOA、合约账户),合约账户有合约代码管理,不能发起交易。
Solana 任何账户都可能成为程序。代码与数据分离。
solana 没有状态,与各种数据账户交互,无需冗余部署。在不同程序之间交互无需转移资产。
Solana 需要支付租金,要求有个最低余额,以保持活动状态。不使用或资金不足会被回收。
什么是程序#
程序是由BPF Loader拥有的可执行账户。它们由Solana Runtime执行,该运行时旨在处理交易和程序逻辑。
Solana 编程模型特点:代码与数据分离。程序没有状态,不存储状态。所有数据存在账户中,通过交易以引用的方式传给程序。
Solana 程序能力:
- 拥有额外账户
- 从其他账户存取 / 读取资金
- 修改数据 / 扣除拥有的账户
程序的两种类型
- 链上程序 - 部署在 Solana 上的用户编写的程序。它们可以由其升级权限进行升级,升级权限通常是部署程序的账户
- 原生程序 - 这些程序集成到 Solana 核心(Core)中。它们提供验证者运行所需的基本功能。原生程序只能通过网络范围的软件更新进行升级。常见的示例包括系统程序、BPF Loader 程序和投票程序。
通常使用 rsut 语言进行程序开发,借助开发框架:Anchor,来简化程序创建
什么是交易#
是链上活动的支柱。是调用程序和实施状态更改的机制。solana 上的交易是一系列指令的捆绑。
交易的组成:
- 一个要读取或写入的账户数组
- 一个或多个指令
- 一个或多个签名
Solana 交易结构,提供了网络处理和验证操作所需的信息
pub struct Transaction {
pub signatures: Vec,
pub message: Message,
}
signatures 字段包含与序列化 Message 对应的一组签名。每个签名与 Message 的 account_keys 列表中的一个账户密钥相关联,从 fee payer 开始。 fee payer 是在处理交易时负责支付交易费用的账户。这通常是发起交易的账户。所需签名的数量等于消息的 MessageHeader 中定义的 num_required_signatures。
message 本身是类型为 Message 的结构。它定义如下:
pub struct Message {
pub header: MessageHeader,
pub account_keys: Vec,
pub recent_blockhash: Hash,
pub instructions: Vec,
}
header 包含三个无符号 8 位整数:所需签名的数量(即 num_required_signatures)、只读签名者的数量和只读非签名者的数量。
account_keys 字段列出了交易中涉及的所有账户地址。请求读写访问权限的账户首先出现,然后是只读账户。
recent_blockhash 是一个最近的区块哈希,包含一个 32 字节的 SHA-256 哈希。这是为了指示客户端上次观察到账本的时间,并作为最近交易的生命周期。验证者将拒绝具有旧区块哈希的交易。此外,最近区块哈希的包含有助于防止重复交易,因为任何与先前完全相同的交易都将被拒绝。如果出于任何原因,交易需要在提交到网络之前很长时间签名,可以使用持久交易 nonce来代替最近的区块哈希,以确保它是唯一的交易。
instructions 字段包含一个或多个 CompiledInstruction 结构,每个结构都指示网络验证者执行特定操作。
指令#
指令是对单个 Solana 程序调用的指令。它是程序中执行逻辑的最小单位,也是 Solana 上最基本的操作单元。程序解释从指令传递的数据,并对指定的账户进行操作。Instruction 结构定义如下:
pub struct Instruction {
pub program_id: Pubkey,
pub accounts: Vec,
pub data: Vec,
}
program_id 字段指定要执行的程序的公钥。这是将处理指令的程序的地址。由该公钥指示的程序帐户的所有者指定了负责初始化和执行程序的加载器。加载器一旦部署,就会将链上 Solana 字节码格式(SBF)程序标记为可执行。Solana 的运行时将拒绝任何试图调用未标记为可执行的帐户的交易。
accounts 字段列出了指令可能从中读取或写入的账户。这些账户必须作为 AccountMeta 值提供。任何可能被指令改变数据的账户必须被指定为可写,否则交易将失败。这是因为程序不能向它们不拥有或没有必要权限的账户写入。这也适用于改变账户的 lamports:从程序不拥有的账户中减去 lamports 将导致交易失败,而向任何账户添加 lamports 是允许的。accounts 字段还可以指定程序不会读取或写入的账户。这是为了通过运行时影响程序执行的调度,但是这些账户将被忽略。
data 是一个包含 8 位无符号整数的通用向量,用作传递给程序的输入。该字段至关重要,因为它包含程序将执行的编码指令。
Solana 对指令数据的格式是不可知的。但是,它内置了对 bincode 和 borsh(用于哈希的二进制对象表示序列化器)的支持。序列化是将复杂数据结构转换为一系列可以传输或存储的字节的过程。数据的编码方式选择应考虑解码的开销,因为所有这些都发生在链上。通常更倾向于使用Borsh序列化,而不是bincode,因为它具有稳定的规范,JavaScript 实现,并且通常更有效。
程序使用辅助函数来简化支持指令的构建。例如,系统程序提供了一个辅助函数来构建 SystemInstruction::Assign 指令:
pub fn assign(pubkey: &Pubkey, owner: &Pubkey) -> Instruction {
let account_metas = vec![AccountMeta::new(*pubkey, true)];
Instruction::new(
system_program::id(),
&SystemInstruction::Assign { owner: *owner },
account_metas,
)
}
该函数构造一个指令,当处理时,将把指定账户的所有者更改为提供的新所有者。
单个交易可以包含多个指令,这些指令按顺序依次执行并具有原子性。这意味着要么所有指令成功,要么都不成功。这也意味着指令的顺序可能至关重要。程序必须经过加固,以安全地处理任何可能的指令序列,以防止任何潜在的利用。
例如,在去初始化期间,程序可能会尝试通过将其 lamport 余额设置为零来去初始化一个账户。这假设 Solana 运行时将删除该账户。这个假设在交易之间是有效的,但在指令之间或跨程序调用(我们将在以后的文章中介绍跨程序调用)是无效的。程序应明确将账户的数据清零,以加固去初始化过程中的潜在缺陷。否则,攻击者可以发出后续指令来利用假定的删除,例如在交易完成之前重新使用该账户。