理解 tokio 中的 Task

什么是 Task

在 tokio 里, task 是一种轻量级、非阻塞的执行单元,它类似于操作系统线程,但对它的调度不是由操作系统来完成的,而是 tokio runtime 来完成的。这种通用的模式,还有一个名称就是 green threads。它很类似于 Go’s goroutines, Kotlin’s coroutines, or Erlang’s processes

它有三个主要特点,分别是 轻量级协作式调度非阻塞

轻量级

与Golang里的 goroutine一样,相比操作系统线程而言,它的创建速度非常的快,且很小。对它的调用完成是由用户态代码来完成的。不像操作系统线程那样,每一次调度都需要上下文的切换,因此在用户态切换任务的成本也是极其的低。同样对于它的运行与销毁也是低成本的。

协作式调度

多数操作系统实现的是抢占式多任务处理,对于多任务调度一般是由调度器(如操作系统调度器)来负责的,它会根据任务的执行时长,动态的抢占并暂停它,然后执行其它任务,最终实现每个任务都在同步执行的样子。

但 tokio 属于 协作式调度 机制,这允许一个任务执行一段时间并 主动出让 执行权,告知 tokio runtime 它目前无法继续执行,你可以去执行其它任务,等一会会有人告诉你什么时候可以再过来执行我(tokio 里的 driver)。

Golang 里的调度机制的是抢占式调度机制,而 tokio 里使用的是协作式调度机制,这是它们的区别

非阻塞

worker thread 执行异步任务时,如果遇到阻塞操作,则此任务将主动出让执行权。这时 worker thread 重新获取一个新的任务(优先从本地任务队列获取,如果找不到,再依次全局任务队列、从其它 worker thread队列窃取、各类 driver )。

注意,这里的任务不应该是执行系统调用或其他可能阻塞线程的操作,因为这会阻止同一线程上的其他任务执行。

这里说的阻塞是指 会阻塞操作系统线程的任务,从而导致无法被tokio 调度的任务,如从磁盘读取一个大文件内容,这时这个worker thread 将一直处于阻塞状态,导致它无法执行其它任务。

Task 分类

根据任务执行是否可以出让划分,大致可以分成两类:可出让 和 不可出让。

可出让执行权

这里是指哪些允许在执行期间主动出让执行权的任务,这样就可以允许 worker thread 继续执行其它的任务。这类任务可以充分发挥系统硬件,最大化执行效率。

read more

为什么MySQL里必须要有主键

有一道面试题,是问"为什么在 MySQL 表里一定要有一个主键"。注意这里提到的主键包括用户自定义的主键ID或MySQL默认隐含添加的主键ID。

对于主键的设置,在日常开发中已经深深的刻在我们的大脑里,基本形成了肌肉记忆,于是在设置表结构时,对每个表必须手动设置一个主键,一般推荐使用自增的INT类型,且有时间还需要无业务逻辑。但如果突然遇到这个问题,一时还真不知道应该如何系统的回答。

下面我将根据自己的理解来分析一下,虽然可能并不完整,但应该可以成为一个及格的答案了。 这里主要指的是InnoDB存储引擎,回答它可能需要从表引擎本身上下手。

存储结构

首先我们知道在MySQL里的,InnoDB 是一个以"聚簇索引"组织数据进行存储,它是将主键与表数据存储在一起的。主键也是一种特殊的unique key,这个主键不可能重复(这句是废话)。一个主键就代表一条记录,也就是说,如果我们想找到一条记录,则必须根据主键才能找到对应的记录。如果没有这个唯一键,将无法对应多条记录,更不可能多条记录对应一个主键,或多个主键对应一条记录。

img

如图所示,蓝色字段是主键,绿色字段data就是记录数据,两者是一块连续的存储地址,即它们是一起存储的。

查询优化

当我们执行一条主键查询语句时,如 SELECT * FROM t WHERE id = 10 FOR UPDATE;,这时它会将这条记录锁定,别人将无法对这条记录进行修改,即 Record lock。重点记住锁一行数据 = 锁主键记录

如果没有主键 ,可能使用 间隙锁 (gap lock) , 大大的增加了锁的粒度,这在高并发下,很可能会出现锁冲突,导致性能下降,尤其是在写多、读写混合型业务里非常致命。

除此之外,还有一些索引查询时的回表问题。在普通索引(二级索引)里,我们找到的结果是主键,而不是完整的记录(这里仍是innodb,不是myisam),没有主键,如何回表?或者说回表性能又如何呢?

img

删除记录

上面提到一个主键就表示一条记录。如果没有主键的话,那应该如何删除指定记录呢?逐行扫描?还有如果遇到多条记录相同的呢?如何判断是否要删除的记录?

主从复制

在主从复制场景里,当 MySQL 在执行 Binlog(复制时)和 Redo/Undo(崩溃恢复时)需要"精确找到某一行"来重放操作。如果你没有主键,MySQL 没有办法快速准确地定位那一行。它只能全表扫描,这时性能很差,甚至会出现遇到多条记录数据完全一样,不知道要修改哪条记录的情况或修改错误数据导致的主从节点数据不一致的严重问题。

总结

以上只是其中的几点,可能并不能完全覆盖所有的原因,但在开发中,通过这些问题,基本可以判断主键的重要性。另外或者有人会说可以用唯一键来代替主键,可是这样的话,这个唯一键就变在了主键了。所谓主键就是一个特殊的不能重复定义多个的主键而已罢了。

read more

Rust 里 thread::park() 与 thread::yield_now() 的区别

在看 tokio 调度源码时,会有一些操作线程park的函数,而在rust标准库里也同样有类似的方法,那就是 thread::park() ,同时还有一个咋一看效果类似的函数 thread::yield_new(), 两个函数都有实现 **类似**暂停执行代码的效果,那它们到底又何区别呢?

希望通过这篇文章可以让大家搞明白它们两者的区别和使用场景。

我们先看一下 thread::park()

thread:park()

对于 park 函数的作用主要是实现当前线程的阻塞,并出让CPU,这时OS调度器会将其它线程调度到CPU,继续执行其它任务。但是一旦调用这个函数后,后续线程将一直处于阻塞状态,也就是说此线程将无法获取CPU处理任务,直到调用 unpark() 函数,才恢复正常。

从线程状态角度来看,它的转换

running ---> [thread::park()] ---> blocked (等待唤醒)
blocked ---> [thread::unpark()]---> runnable ---> running (被unpark唤醒后)

总结

  • 一经调用 thread::park() 函数后,线程将一直处于阻塞状态
  • 期间没有机会获得CPU
  • 需要外部干预(调用unpark)才可以恢复执行状态

这是官方文档的一个示例

use std::thread;
use std::time::Duration;
let parked_thread = thread::Builder::new()
 .spawn(|| {
 println!("Parking thread");
 thread::park();
 println!("Thread unparked");
 })
 .unwrap();
// Let some time pass for the thread to be spawned.
thread::sleep(Duration::from_millis(10));
println!("Unpark the thread");
parked_thread.thread().unpark(); // 这里恢复阻塞的线程状态,继续执行后续语句 println!("Thread unparked")

parked_thread.join().unwrap();

执行结果

read more

从Vec的 swap_remove 方法中学到的性能优化

今天在看 tokio 源码时,发现一个 unpark_worker_by_id 函数里调用了标准库Vec 的 swap_remove方法,swap_remove 实现如下

 pub fn swap_remove(&mut self, index: usize) -> T {
 #[cold]
 #[cfg_attr(not(panic = "immediate-abort"), inline(never))]
 #[optimize(size)]
 fn assert_failed(index: usize, len: usize) -> ! {
 panic!("swap_remove index (is {index}) should be < len (is {len})");
 }
 let len = self.len();
 if index >= len {
 assert_failed(index, len);
 }
 unsafe {
 // We replace self[index] with the last element. Note that if the
 // bounds check above succeeds there must be a last element (which
 // can be self[index] itself).
 let value = ptr::read(self.as_ptr().add(index));
 let base_ptr = self.as_mut_ptr();
 ptr::copy(base_ptr.add(len - 1), base_ptr.add(index), 1);
 self.set_len(len - 1);
 value
 }
 }

它的作用是将 vec 里指定 index 元素从vec中移除并返回,同时将 vec 最后一个位置的元素移动到指定 index 位置,同时减小vec的长度,保证元素内容与长度一致,从而达到删除元素的效果。

read more

理解 Solana 中的 Slot、Block、Epoch、Entry、Shred 和 PoH

对于刚刚接触Solana的开发者来讲,会经常遇到 Slot Block Epochs Entry 和 Shred等一些概念术语,但很验证记住它们的意思和作用。

本篇将通过 Solana 的交易原理,让大家轻记信它们并了解它们之间的联系,并加深对Solana交易的理解。由于能力有限,文章中难免有误,大家可以在评论区留言,共同学习进步!

好了,下面让我们开始彻底搞明白这些术语及它们在Solana交易中的作用吧!

在开始之前我们先看一下在Soalna链中一笔交易是如何进行的。

交易流程

Visualization of where Turbine lies in the lifecycle of a Solana transaction

交易流程

  1. 用户客户端发起一笔交易,并使用钱包私钥进行签名
  2. 通过rpc协议将交易发送到用户指定的 Solana RPC 节点。这里RPC节点在区块链网络中存在多个,起到了负载均衡的作用
  3. RPC节点验证交易合法性,如进行一系列的检查 ,一般指签名验证/账户检查(账户是否存在,读取权限合法)/账户余额是否足够支付手续费等,最后进行交易模拟执行,就是我们经常说的模拟交易阶段(这一步可以手动配置 --skip-preflightkipPreflight: true 跳过)。如果这一步失败,则整个交易直接失败,并返回给客户端。否则将交易转换到 Leader 节点。
  4. Leader节点收到交易后,会放入自己的交易队列(Transaction Queue),按账户读写锁进行是否允许并发执行(solana交易快的原因之一就是并发执行),同时进行预检查(Pre-Check),防止账户余额不足支付交易手续费。同时还会做更多的工作,这些工作与PoH有关,设计到slot/blocks/Epochs/Entry 和 shred,这也正是本文的重点。
  5. 在区块链里一笔交易是否合法需要得到大多数节点(在solana里实际是质押量)的认可才行。因此这时 Leader 节点将交易状态设置为 processed。需要将这笔交易发送到一些验证节点(Validator)。如果每一次发送一笔交易的话,网络性能太差了,因此需要将多笔一起打包发送到验证节点,在Solana里一般是通过 Turbine(分层树状网络) 广播的方式将交易信息发送到 validator节点。
  6. 验证节点收到交易信息后,会重放执行交易,再次对交易进行验证,如果与leader节点验证的结果一致,则表示交易是合法的,就会对LEADER节点发送一个投票交易( Tower BFT )到 leader节点。这一阶段称为投票。同时这些验证节点还需要发送到下一层的邻近 validator 节点,这些节点组织成一个类似树状网络。
  7. 当Leader节点不断的收到多个验证节点的投票,并不断的计算总stake数量,如果发现达到了 2/3 ,则表示交易得到了多数节点的认可,则表示交易是合法的,这时再将交易状态修改为 confirmed
  8. 后续随着新交易的不断增加,交易越来越不可能被修改,最终将交易状态修改为 finalized,表示交易永远不可回滚。

以上就是一笔交易的大概流程,这里涉及交易的三种状态:

  • processed:Leader 本地执行并打包交易

  • confirmed:交易得到网络多数 validator 的认可(>2/3 stake 投票)

read more

零拷贝bytemuck与 borsh

在上一篇博文《深入理解 Serde、Bincode 与 Borsh 的关系与区别》介绍了常用的几种解析二进制数据的方法,主要有 bincode 与 borsh, 并提到过在区块链领域里一般推荐使用 borsh 解析数据。但随着合约的开发使用borsh的地方越来越多,会经常遇到提示超出 4K Stack 大小的错误。这是因为在solana里,虚拟机 sbf 限制了一个合约最大允许使用的statck大小上限为 4k。尽管我们使用完一个大变量通过一些方法,如变量作用域、通过Box将内存移动到heap、或手动drop立即释放内存。但仍有些场景是没有采用这种办法的,这时应该如何办呢?

如果经常看一些优秀的开源项目的话,会发现有一个 bytemuck 的crate,它是一个 zerocopy 库,可以避免内存复制带来的开销,加速解析数据速度,这里给出一个测试代码

use borsh::{BorshDeserialize, BorshSerialize};
use bytemuck::{Pod, Zeroable};
use solana_program::pubkey::Pubkey;
use std::time::Instant;
/// -------- 零拷贝结构 (定长布局) --------
#[repr(C, packed)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
pub struct AccountZC {
 pub lamports: u64, // 8字节
 pub data: [u8; 32], // 32字节
 pub owner: Pubkey, // 32字节
}
/// -------- Borsh 结构 --------
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct AccountBorsh {
 pub lamports: u64,
 pub data: [u8; 32],
 pub owner: Pubkey,
}
fn main() {
 // 模拟数据
 let lamports: u64 = 123456789;
 let data: [u8; 32] = [7u8; 32]; // 32字节全是 7
 let owner = Pubkey::new_unique();
 // ---------- 用 Borsh 序列化测试数据 ----------
 let account_borsh = AccountBorsh {
 lamports,
 data,
 owner,
 };
 let serialized_raw = borsh::to_vec(&account_borsh).unwrap();
 println!("raw序列化长度 = {}", serialized_raw.len());
 println!("serialized_raw首地址 = {:p}", serialized_raw.as_ptr());
 // ---------- 1. 用 Borsh 反序列化解析 ----------
 let start = Instant::now();
 let parsed_borsh = AccountBorsh::try_from_slice(&serialized_raw).unwrap();
 println!("零拷贝解析结果 = {:?}", parsed_borsh);
 println!("反序列化耗时: {:?}", start.elapsed());
 println!("Borsh struct首地址 = {:p}", &parsed_borsh);
 // ---------- 2. 零拷贝解析 ----------
 // 因为 AccountZC 是定长布局,可以直接按字节存放
 let start = Instant::now();
 assert!(serialized_raw.len() >= std::mem::size_of::<AccountZC>());
 let acc: &AccountZC = bytemuck::from_bytes(&mut serialized_raw.as_ref());
 println!("零拷贝解析结果 = {:?}", acc);
 println!("反序列化耗时: {:?}", start.elapsed(),);
 println!("Bytemuck struct首地址 = {:p}", acc);
}

输出

read more

深入理解 Serde、Bincode 与 Borsh 的关系与区别

在Rust开发中,无论是构建网络服务、存储数据还是开发区块链程序,**序列化(Serialization)和反序列化(Deserialization)**都是不可或缺的操作。序列化是将内存中的数据结构(struct)转换成字节序列或者其他格式(JSON, vec),以便存储或传输;反序列化则是将字节序列还原成原来的数据结构。

Rust 生态中常用的序列化工具包括 SerdeBincodeBorsh。初学者在阅读文档或实际开发中可能会发现它们名字都很熟悉,但它们的定位、使用方式和特点却不完全相同。

本文将系统梳理三者的关系、差异和使用场景。

Serde

Serde(Serialization / Deserialization) 是 Rust官方生态最流行的序列化框架。它提供了一种 抽象接口,让你可以将 Rust 类型序列化为多种格式,而不关心底层具体实现。

特点

  • 通用性强:支持 JSON、YAML、TOML、Bincode 等多种格式。
  • 灵活:可以自定义序列化逻辑。
  • 宏支持:通过 #[derive(Serialize, Deserialize)] 自动生成序列化代码。
  • 安全性:类型系统保证序列化和反序列化操作类型安全。

示例

use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
 name: String,
 age: u32,
}
fn main() {
 let user = User { name: "Alice".to_string(), age: 30 };
 let json = serde_json::to_string(&user).unwrap();
 println!("JSON: {}", json);
}
// Output:
// JSON: {"name":"Alice","age":30}

注意:Serde 本身不提供具体的存储格式,它是框架,依赖具体格式库(如 serde_jsonbincode)来实现序列化。

Bincode

Bincode 是 Serde 生态下的一个高性能二进制序列化库,专注于 速度和空间效率

read more

solana中MEV监听交易的几种方法

策略分类

在 Solana 上 MEV(最大可提取价值) 的几种策略主要有:

  • 套利(Arbitrage)
  • 清算(Liquidation)
  • 抢跑(Front-Running)
  • 三明治攻击(Sandwich Attack)
  • 后置插队(Back-Running)
  • Jito MEV 竞标

套利(Arbitrage)

价格差异猎手 想象你是一个精明的商人,在不同的市场上发现同一件商品价格不同。比如,在A市场一个苹果卖1元,在B市场卖1.2元。你会怎么做?立即在A市场买入,然后在B市场卖出,赚取0.2元的差价。

在加密世界中,这就是套利。交易员快速在不同交易所或流动性池之间捕捉加密货币价格差异,瞬间完成买卖,赚取微小但稳定的利润。

清算(Liquidation)

充当风险管理者角色。想象银行放贷时,借款人需要提供抵押物品,如果抵押物品价值下跌到无法覆盖贷款,银行会立即收回资产。

在加密借贷平台,当用户的抵押品价值跌破安全线,MEV机器人(清算人Liquidator) 通常可以以折扣价买入资产。这是因为在借贷平台(如 SolendMango Markets)中,如果用户的抵押资产价值不足以覆盖其贷款,借贷平台会进行 清算,并出售抵押资产来弥补贷款人的损失。MEV机器人(清算人)通过买入这些资产并偿还借款人欠的平台,可以获得折扣或者奖励。

抢跑(Front-Running)

想象在股市,有人获得内部消息,得知一家上市公司发生重大利好消息,他要在股市正式开市前抢先买入大量股票(盘前交易),这被称为"抢跑"。

MEV机器人监听交易池,当发现即将发生大额交易时,快速插入自己的交易,在原交易之前完成,从中获利。这就像是站在股市交易大厅的最前排,抢先一步。

三明治攻击(Sandwich Attack)

假如你要在一个拥挤的市场买入大量的苹果。一些精明的商贩知道这个消息后,他会在你购买之前大量的收购市场里的苹果,然后再出售给你,从而获利。

由于在一笔大额交易的前后各插入一笔交易,通过制造价格滑点来获利,就像把目标交易"夹"在中间,形成一个"三明治"。

 攻击者的前置交易
 ↓
 目标用户的大额交易
 ↓
 攻击者的后置交易

在加密交易中,MEV机器人 会在大额交易前后插入自己的交易,通过制造价格波动来获利,就像把目标交易"夹"在中间。

后置插队(Back-Running)

可以想象在一个拍卖会上,一位艺术家的艺术品被以高价成交后,紧接着他的其它类似艺术品的价值大概率也会上涨。

MEV机器人 会紧随大额交易之后立即交易,利用这个交易带来的市场变动获利,就像是紧跟在大浪后的冲浪者。

Jito MEV 竞标

区块空间拍卖 你可以将其想象成一个竞标会,不同的商人为获得 最佳展位 而竞争。谁出价最高,谁就能获得最好的位置。

在Solana区块链上,MEV机器人通过Jito平台向验证者竞标区块空间。出价最高的机器人可以将自己的交易打包到区块中,从MEV中获得收益。

总结

总的来说,MEV策略就像是金融世界的高频交易算法,需要极快的计算能力、复杂的算法和对市场的敏锐洞察。它们在捕捉微小利润的同时,也为市场提供了一定的流动性和价格发现机制。

不过,这些策略也存在争议。它们可能被视为不道德,因为它们在某种程度上"操纵"了市场。

对于以上这些MEV策略,在开发时,一般通过以下几种监听交易的方式实现


监听实时交易日志(onLogs)

适用场景:抢跑(Front-Running)、三明治攻击(Sandwich Attack)

📌 思路:

  • 监听目标 DEX(如 Raydium、Orca、Jupiter)的交易日志
  • 解析 交易内容(Swap / 清算 / 预言机更新)
  • 立即提交 更高 Gas 费的交易 抢跑

示例代码

import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const RAYDIUM_PROGRAM_ID = new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
connection.onLogs(
 RAYDIUM_PROGRAM_ID,
 async ({ logs, signature }) => {
 console.log(`⚡ 检测到新交易: ${signature}`);
 // 获取交易详情
 const tx = await connection.getTransaction(signature, {
 maxSupportedTransactionVersion: 0,
 commitment: "confirmed",
 });
 if (tx && tx.meta) {
 console.log(`🛠 解析交易详情:`, tx.meta.logMessages);
 // 检测是否是套利交易
 if (tx.meta.logMessages.some(log => log.includes("swap"))) {
 console.log(`🚀 发现 DEX 交易,尝试抢跑!`);
 sendHigherFeeTransaction(tx);
 }
 }
 },
 "confirmed"
);

优点:

read more

SPL-Token CLI 使用入门教程

本篇文章将对官方教程的基本上进行整理和完善,同时对一些官方未提到的一些知识点和实践进行一些介绍,方便一些刚入门solana开发的同学能够对solana开发有更完整的了解。

本篇文章使用 spl-token 命令行进行演示操作,分别介绍 Mint Account 和 token Account 的创建、转账、关闭账户并回收租金SOL等操作。

这里假如用户本机已经安装好solana命令行,并通过 solana-test-validator 命令在本地启动开发集群。

环境准备

  1. 打开一个终端,在本地创建一个新集群
(base) ➜ ~ solana-test-validator --ledger test-ledger -r
Ledger location: test-ledger
Log: test-ledger/validator.log
⠠ Initializing... Waiting for fees to stabilize 1...
Identity: 7U4BYYhgGtjXyujPxX7BrqwYJmLA5BGijUhEv6BACphi
Genesis Hash: ATwFvEZBVuQjAYbp2Y8FyrbLL8AQxN3sPxci2uKkNxFe
Version: 2.2.3
Shred Version: 45925
Gossip Address: 127.0.0.1:1024
TPU Address: 127.0.0.1:1027
JSON RPC URL: http://127.0.0.1:8899
WebSocket PubSub URL: ws://127.0.0.1:8900
⠚ 00:00:10 | Processed Slot: 22 | Confirmed Slot: 22 | Finalized Slot: 0 | Full Snapshot Slot: - | Incremental Snapshot Slot: - | Transactions: 21 | にじゅうまる499.999895000

这进成功在本地启动了一个solana集群环境。

read more

Solana中的 Native Programs

在Solana中有一些少量的内置原生程序,它们的程序ID格式一般为 xxx11111111111111111111111111111111。它们与第三方自定义程序不同,原生程序是 validator node 运行所必须的一部分。同时它们也是集群升级的一部分,这些升级可能包括新功能的添加、BUG修复,又或者是性能的优化提升。这些内置原生程序极少的发生变化,目前提供的所有内置原生程序可以在 https://docs.anza.xyz/runtime/programs 找到。

其中有两个非常重要的程序很值得我们关注,因为我们在开发中,需要经常用到他们。

System Program

  • 作用:

    • 创建新帐户
    • 分配帐户数据
    • 将帐户分配给所属的程序,即指定账户与程序的对应关系
    • 转账最小 lamports 给账户(租金),防止账户被系统回收
  • Program id: 11111111111111111111111111111111

  • Instructions: SystemInstruction

BPF Loader

  • 作用:
    • 部署所有自定义程序,并将其作为的所有自定义程序的 owner
    • 升级和执行链条上的程序
  • Program id: BPFLoaderUpgradeab1e11111111111111111111111
  • Instructions: LoaderInstruction

BPF可升级加载器将自己标记为它为存储程序而创建的可执行文件和程序数据帐户的"Owner"。当用户通过程序id调用指令时,Solana运行时会加载您的程序及其所有者BPF Upgradeable Loader。然后,运行时将程序传递给BPF可升级加载器,以便其处理指令。

对于 BPFLoaderUpgradeab1e11111111111111111111111程序它是作为 Token2022 专用的,它是对 Token 的扩展版,对于 Token 对应的内置程序是 BPFLoader2111111111111111111111111111111111,两者的区别是后者是无法升级的。

关系图

对于这些内置原生程序与三方程序的关系图大概是这个样子的。

image-20250305184730290

参考资料

read more

Solana中 PDA、ATA 与 普通Account 的区别与关系

普通账户地址

对于账户地址的创建是由一个密钥对来生成的,但在Solana中账户地址与以太坊中的账户地址还是有一些区别的。

以太坊账户地址

以太坊账户地址的生成过程:

  1. 通过私钥生成公钥
  2. 对公钥进行 Keccak-256 哈希
  3. 取哈希值的最后 160 位(20 字节)作为地址
  4. 将地址以 0x 开头,并根据需要选择是否使用 EIP-55 格式

地址中通常是小写字母,但也有大写字母的变种,称为 EIP-55 格式。在 EIP-55 中,某些字符会根据哈希值的大小写进行区分,从而增加地址的错误检查能力。

Solana 账户地址

Solana账户地址的生成过程:

  1. 通过私钥生成公钥,一般通过调用 Keypair.generate() 生成
  2. 公钥直接映射为账户的地址,长度为 32 字节
  3. 为了使用方便,一般对其进行 Base58 编码,将公钥转换为地址字符串

代码:

const { Keypair } = require('@solana/web3.js');
// 生成一个新的密钥对
const keypair = Keypair.generate();
// 获取公钥,实际上就是账户地址
const publicKey = keypair.publicKey;
// 转换公钥为 Base58 编码的字符串(即账户地址)
const address = publicKey.toBase58();
console.log("Solana Account Address (Base58):", address);

可以看到 Solana 中的账户地址就是公钥,平时使用的账户地址,一般都是指 Base58 编码后的字符串。

总结

特点Solana以太坊
生成公钥直接作为地址公钥通过 Keccak-256 哈希,取哈希的最后 160 位
长度32 字节(256 位),Base58 编码后 43 字符20 字节(160 位),16 进制编码后 40 字符
哈希算法无哈希处理,直接使用公钥使用 Keccak-256 哈希处理公钥
编码方式Base58 编码16 进制编码,0x 前缀
示例4Erv6yZoXckm6QqsbU6y6TbS8o8wVkAXw7KwHjsAVB9D0x5b1a49d2c631eeed5d2b8e6abdd87e07d8e1a3b3

虽然 Solana 和以太坊都基于公钥生成地址,但它们在地址生成的具体方式、哈希算法和编码格式上存在不同。

read more

Solana中如何实现转账

在 Solana 中转账有两种方式。

修改结构体字段

一种是在不调用(invoking )系统程序( System Program )的情况下,将 lamports 从一个账户转移到另一个账户。它的实现是直接通过修改结构体的 lamports 字段值来实现的。

这种方法可以实现将 lamports 从任何由您的程序拥有的账户转移到任何账户。文档

/// Transfers lamports from one account (must be program owned)
/// to another account. The recipient can by any account
fn transfer_service_fee_lamports(
 from_account: &AccountInfo,
 to_account: &AccountInfo,
 amount_of_lamports: u64,
) -> ProgramResult {
 // Does the from account have enough lamports to transfer?
 if **from_account.try_borrow_lamports()? < amount_of_lamports {
 return Err(CustomError::InsufficientFundsForTransaction.into());
 }
 // Debit from_account and credit to_account
 **from_account.try_borrow_mut_lamports()? -= amount_of_lamports;
 **to_account.try_borrow_mut_lamports()? += amount_of_lamports;
 Ok(())
}

上面是直接操作底层数据库的 AccountInfo.lamports 字段,不过这种方式在链上没有变更历史,因此一般不推荐使用这种转账方式。

read more

Solana中账户类型 Account、AccountInfo与 SystemAccount 的区别

在Solana中 Account 的角色很重要,它就像Linux中一切皆文件的概念一样,无处不在。了解它也是开发Solana的基础,本节主要介绍我们最经常使用的 AccountAccountInfoSystemAccount 这三种账户类型的区别与使用场景。

当然除此之外还有一些账户类型也很重要,如 UncheckedAccountSignerTokenAccountMintCpiAccountLoaderProgramAssociatedToken 等,我们这里就不再一一讲解,有兴趣的话可以参考官方相关文档。

由于多数情况下都是使用anchor框架开发Solana合约,因此本文主要是根据 anchor-lang 文档里介绍账户来讲解

账户类型

以下我们分别对这三种账户类型做一些简单的介绍。

AccountInfo

在 Solana 中 AccountInfo 是最基础的账户类型。

AccountInfo

其它几种账户类型都是对它的封装,它的定义

#[repr(C)]
pub struct AccountInfo<'a> {
 pub key: &'a Pubkey,
 pub lamports: Rc<RefCell<&'a mut u64>>,
 pub data: Rc<RefCell<&'a mut [u8]>>,
 pub owner: &'a Pubkey,
 pub rent_epoch: u64,
 pub is_signer: bool,
 pub is_writable: bool,
 pub executable: bool,
}

字段解释

key 公钥地址,当前账户的address

lamports 账户中的 lamports。可由程序修改。

data 此账户中保存的数据。

owner 当前账户的所有者(下面会有一个图解释owner关系),它也是一个公钥地址

rent_epoch 租约纪元,此账户下次需支付租金的时间点(每个账户要想在在网络中存在,必须支付一定的存储空间费用,如果达到租金两倍则可以免除)

is_signer 该交易是否由本账户的公钥签名

is_writable 是否可写, 用 account(mut) 声明

executable 是否为可执行文件。如果为合约可执行程序账户的话,则表示为可执行;否则就是普通的账户。

这里讲一下最经常用到的几个字段。

executable 字段如果值为 true 则表示当前账户是一个合约,因为只有合约才可以执行,否则就是一般的账户;lamports 就是当前账户里的代币金额;对于 data 就是我们平时开发中用到的自定义数据。如 pub account: Account<'info, NewData> 写法,是表示 NewData 为我们的自定义数据;还有 owner 字段也很重要,后面会讲到。

read more

什么是Layer2网络

我们平时提到的比特币、以太坊、Solana,它们都属于Layer 1网络,而Layer 2(L2) 网络是指基于Layer1网络之上构建的一层网络,它很类似于Layer 1 网络,也是一个独立的区块链。但它的主要目的并不是为了代替 Layer 1 ,而是为了通过扩展 Layer 1 层网络从而解决一些在 Layer 1 网络中存在的一些问题,它同时继承了 Layer 1 网络的安全性和去中心化性。

以太坊存在的问题

这里以以太坊为例,在 L1 网络上随着交易量越来越大,交易频率也越来越频繁,导致Gas(网络交易费)越来越高,一笔交易可能在网络繁忙的时候高达十几美元,导致一些交易可能需要花费好久才可以真正成交到区块网络。

这两个问题大大提高了用户使用门槛,那有没有好的解决办法呢?

解决方案

区块链的三个核心特性是 去中心化安全性可扩展性,一般简单的区块链架构只能实现其中两个特性(这一点很类似于分布式中的CAP理论)。想要一个安全且去中心化的区块链的话,只能牺牲可扩展性,而这也正是第二层网络发挥作用的地方。

以太坊生态系统坚定认为,第 2 层扩展是解决可扩展性三难问题的唯一途径,同时保持去中心化和安全性。

Layer 2网络工作原理

Layer 2 也是一个独立的区块链,它扩展了以太坊。这在很大程度上减轻了第一层的交易负担,使其变得不那么拥堵,从而使整体更具扩展性。

目前存在多种第二层解决方案,每种都有其自身的权衡和安全模型。如 RollupsState ChannelsSidechains Validium 混合解决方案 等。

其中Rollups 是一种目前比较流行的 Layer 2(L2)扩展技术方案,它在以太坊生态系统中得到了广泛应用,并被许多主流项目采用。它不仅提高了区块链(以太坊生态)的性能和可扩展性,同时保持 Layer 1(以太坊)的安全性和去中心化特性。

Rollups 的核心思想是将大量交易打包成一个批次(Rollup)链下处理,仅在 Layer 1 上存储交易数据验证证明,从而降低交易手续费。

在区块链中,链下 是指不在 Layer 1(主链)上直接处理交易或计算,而是将这些任务转移到 Layer 2 网络或其他链下环境中执行。

img

针对 Rollups 分两种类型,Optimistic rollups(乐观汇总) Zero knowledge rollups(零知识证明), 它们两者的主要区别在于如何将此交易数据提交到 Layer 1网络。

Optimistic rollups

它假设所有交易都是有效的,但如果怀疑存在无效交易,则提出挑战(质疑),此时将运行错误证明以查看是否发生这种情况。

read more

Solana中如何解析指令

开发过Solodity的同学都知道在合约开发中,不同指令对应的不同前端Endpoint(API接口),这种开发模式特别的清晰且易维护。那在开发Solana合约时没有有对应的方法呢?

Solana开发方式

开发 Solana 合约,一般分 Native 和 Anchor 框架开发。

Native 主要是开发者通过SDK 手动实现所有业务逻辑。 这种模式一般对开发者要求比较高,除了需要了解相关概念外,最重要的还需要知道对应的SDK实现,如PDA账户的创建。

Anchor框架 推荐使用,只需要一些宏即可以实现一些逻辑,不需要用户关心底层实现。这种开发方式对于指令的处理基本与Solidity中一致,开发者只要搞明白了基本用法就可以了。

下面主要讲一下在 Native 这种方式下,如何实现指令或附加数据的解析。

如果你对 指令这个概念不太理解的话,可以将其视为路由。其类于似在mvc开发中控制器路由,如 /user/info/user/base/user/changepwd 之类。

示例介绍

我们先看一个在 https://beta.solpg.io 网站上创建的一个 Native 示例

image-20250109120746106

/// Define the type of state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
 /// number of greetings
 pub counter: u32,
}
// Declare and export the program's entrypoint
entrypoint!(process_instruction);
// Program entrypoint's implementation
pub fn process_instruction(
 program_id: &Pubkey, // Public key of the account the hello world program was loaded into
 accounts: &[AccountInfo], // The account to say hello to
 _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos
) -> ProgramResult {
 msg!("Hello World Rust program entrypoint");
 // Iterating accounts is safer than indexing
 let accounts_iter = &mut accounts.iter();
 // Get the account to say hello to
 let account = next_account_info(accounts_iter)?;
 // The account must be owned by the program in order to modify its data
 if account.owner != program_id {
 msg!("Greeted account does not have the correct program id");
 return Err(ProgramError::IncorrectProgramId);
 }
 // Increment and store the number of times the account has been greeted
 let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
 greeting_account.counter += 1;
 greeting_account.serialize(&mut *account.data.borrow_mut())?;
 msg!("Greeted {} time(s)!", greeting_account.counter);
 Ok(())
}

首先通过宏 entrypoint!(process_instruction) 声明了合约入口函数为 process_instruction,它是整个合约程序的的入口,所有进入合约内部的逻辑必须经过这个函数才可以进入,它类似 C 语言中的 main()函数。

read more