系统由三个主要组件组成:
- SVM 智能合约(1024chain):发送端和接收端合约
- EVM 智能合约(Arbitrum Sepolia):发送端和接收端合约
- Relayer 中继服务:监听事件、签名验证、交易提交
用户 → 发送端合约(质押) → 触发事件 → Relayer监听 → 签名验证 → 接收端合约(解锁)
设计原则:
- 管理操作:使用 Squad 多签钱包,提高安全性
- 业务操作:使用 PDA 金库,提高效率
- 职责分离:管理操作由多签控制(外部处理),业务操作由 PDA 自动执行(合约控制)
架构优势:
- 性能优化:解锁操作使用 PDA,无需多签投票,快速执行
- 安全性:管理接口多签保护,防止单点故障;金库资金 PDA 控制,只能通过合约逻辑操作
- 灵活性:可以随时增加/减少金库流动性;多签成员可以变更(外部处理)
- 简洁性:合约逻辑保持简洁,不处理多签提案
Squad 多签程序:
- 程序地址:
SMPLecH534NA9acB4bMolv7X6RBpK4rjn3LkN1gZXYjy - 用于管理操作的多签钱包创建和管理
- 多签投票在外部处理,合约不关心多签逻辑
关键设计决策:SVM 和 EVM 各自使用其原生的密码学算法,以最大化安全性和性能。
签名算法:Ed25519(Solana 原生)
- 使用 Solana 的 Ed25519Program 预编译合约进行签名验证
- 程序 ID:
Ed25519SigVerify111111111111111111111111111 - 签名长度:64 字节
- 公钥长度:32 字节
数据序列化:Borsh(Solana 原生)
- 使用 Anchor 框架的 Borsh 编码
- 直接序列化
StakeEventData结构体 - 确保字节级别的一致性
哈希算法:无需额外哈希
- Ed25519 签名直接对序列化后的数据进行
- 签名本身已包含消息完整性验证
验证流程:
- 客户端使用
Ed25519Program.createInstructionWithPublicKey()创建签名验证指令 - Solana 运行时在交易执行前验证签名
- 合约通过 Instructions Sysvar 检查 Ed25519Program 指令的存在和正确性
- 验证参数匹配:签名、公钥、消息内容
签名算法:ECDSA (secp256k1)(Ethereum 原生)
- 使用
ecrecover预编译合约进行签名验证 - 签名长度:65 字节 (r: 32, s: 32, v: 1)
- 曲线:secp256k1(与比特币、以太坊相同)
数据序列化:JSON 字符串格式
- 将事件数据序列化为 JSON 字符串
- 格式:
{"sourceContract":"...","targetContract":"...","chainId":"...","blockHeight":"...","amount":"...","receiverAddress":"...","nonce":"..."} - 确保跨语言的可读性和一致性
哈希算法:两层哈希
- 第一层 - SHA-256:对 JSON 序列化的事件数据进行哈希
bytes32 dataHash = sha256(jsonString);
- 第二层 - Keccak256 (EIP-191):应用 EIP-191 签名消息格式
bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
验证流程:
- 对事件数据进行 JSON 序列化
- 计算 SHA-256 哈希
- 应用 EIP-191 前缀并计算 Keccak256 哈希
- 使用
ecrecover从签名中恢复签名者地址 - 验证恢复的地址与预期的 Relayer 地址匹配
由于两条链使用不同的密码学算法,Relayer 需要根据目标链选择相应的签名方式:
- 监听 SVM 链的
StakeEvent事件 - 获取事件数据(Borsh 格式)
- 转换为 EVM 格式:
- 将事件数据序列化为 JSON 字符串
- 计算 SHA-256 哈希
- 应用 EIP-191 格式
- 使用 ECDSA (secp256k1) 私钥签名
- 提交签名到 EVM 接收端合约
- 监听 EVM 链的
StakeEvent事件 - 获取事件数据(EVM event logs)
- 转换为 SVM 格式:
- 构造
StakeEventData结构体 - 使用 Borsh 序列化
- 构造
- 使用 Ed25519 私钥签名
- 创建
Ed25519Program验证指令 - 提交签名到 SVM 接收端合约
| 特性 | SVM (Solana) | EVM (Ethereum) |
|---|---|---|
| 签名算法 | Ed25519 | ECDSA (secp256k1) |
| 签名长度 | 64 字节 | 65 字节 |
| 公钥长度 | 32 字节 | 20 字节(地址) |
| 数据序列化 | Borsh(二进制) | JSON(字符串) |
| 哈希算法 | 无需额外哈希 | SHA-256 + Keccak256 |
| 签名格式 | 标准 Ed25519 | EIP-191 |
| 验证方式 | Ed25519Program | ecrecover |
| 性能 | 极快(原生支持) | 快(预编译) |
| 安全级别 | 128 位(Ed25519) | 128 位(secp256k1) |
- 性能最优:使用链的原生算法可以充分利用预编译合约,获得最佳性能
- 安全性最高:经过充分测试和审计的原生实现,安全性最可靠
- Gas 成本最低:原生算法的 Gas 消耗最少
- 生态兼容性:与各链生态的标准工具和钱包完全兼容
- 简化实现:无需在合约中实现复杂的密码学库
虽然两条链使用不同的签名算法,但通过以下机制保证跨链兼容性:
- 事件数据结构统一:两条链使用相同的
StakeEventData结构 - Relayer 转换层:Relayer 负责在两种格式之间转换
- 独立验证:每条链独立验证其原生格式的签名
- Nonce 机制统一:两条链使用相同的 nonce 递增判断机制防重放
- 阈值计算统一:两条链使用相同的 2/3 阈值计算公式
由于 Solana 账户大小限制(最大 10MB,但实际使用中建议不超过 10KB),需要采用 PDA(Program Derived Address)来支持无限请求和最多 18 个 relayer。
存储固定大小的配置数据:
pub struct ReceiverState {
pub vault: Pubkey, // 32 bytes (PDA金库地址)
pub admin: Pubkey, // 32 bytes (多签钱包地址)
pub relayer_count: u64, // 8 bytes
pub source_contract: Pubkey, // 32 bytes
pub source_chain_id: u64, // 8 bytes
pub target_chain_id: u64, // 8 bytes
pub relayers: Vec<Pubkey>, // 4 + 32 * 18 = 580 bytes (最多18个relayer)
pub last_nonce: u64, // 8 bytes (用于nonce递增判断)
}设计说明:
vault: PDA金库地址,由程序控制,支持自动转账admin: 多签钱包地址,用于管理操作(合约层面只验证签名,不关心多签逻辑)
账户大小计算:
- Base: 120 bytes
- Relayers (18个): 580 bytes
- Last nonce: 8 bytes
- 总计: ~708 bytes (在 10KB 限制内)
设计说明:
relayers: 最多支持 18 个 relayerlast_nonce: 记录最后一个已使用的 nonce,用于判断新 nonce 是否递增- Nonce 使用 64 位无符号整数(u64),通过递增判断来防止重放攻击
- 当 nonce 溢出时(达到 u64::MAX),重置为 0
为每个跨链请求(nonce)创建独立的 PDA 账户来存储 relayer 签名缓存:
pub struct CrossChainRequest {
pub nonce: u64, // 8 bytes
pub signed_relayers: Vec<Pubkey>, // 4 + 32 * 18 = 580 bytes (最多18个relayer)
pub signature_count: u8, // 1 byte
pub is_unlocked: bool, // 1 byte
pub event_data: StakeEventData, // 事件数据(用于验证和转账)
}PDA 种子: [b"cross_chain_request", nonce.to_le_bytes()]
账户大小: ~600+ bytes(固定大小,支持最多 18 个 relayer)
设计优势:
- 支持至少 100 个未完成的请求:每个请求独立账户,可同时存在 100+ 个未完成的请求
- 支持 1200 个签名缓存:100 个请求 × 18 个 relayer = 1800 个签名(超过要求的 1200 个)
- 每个请求独立账户,支持无限 nonce
- 固定大小,易于管理
- 解锁后可以关闭账户回收租金
Nonce 递增判断逻辑:
- 新 nonce 必须大于
last_nonce(递增) - 如果新 nonce <=
last_nonce,则视为重放攻击,拒绝处理 - 当 nonce 达到
u64::MAX时,重置为 0(溢出处理) - 解锁成功后,更新
last_nonce = nonce
| 方案 | 优点 | 缺点 |
|---|---|---|
| 当前方案(PDA) | 支持无限 nonce,固定大小 | 需要为每个 nonce 创建账户 |
| Vec 存储所有记录 | 简单直接 | 账户大小会无限增长 |
| 位图跟踪 | 空间效率高 | 实现复杂,需要压缩算法 |
选择 PDA 方案的原因:
- Solana 账户大小限制严格(10KB)
- 需要支持理论上无限次请求
- 每个 nonce 的签名记录是独立的,适合分离存储
- 解锁后可以关闭账户,回收租金
EVM 合约使用映射(mapping)存储,无大小限制:
struct NonceSignature {
mapping(address => bool) signedRelayers;
uint8 signatureCount;
bool isUnlocked;
}
mapping(uint256 => NonceSignature) public nonceSignatures;
mapping(uint256 => bool) public usedNonces;重要变更: 一个平台的接收端和发送端初始化函数合并为一个 initialize 指令。
在初始化时,同时创建 SenderState 和 ReceiverState 账户,共享相同的配置(vault、admin、chain_id 等)。
// 发送端状态
pub struct SenderState {
pub vault: Pubkey,
pub admin: Pubkey,
pub usdc_mint: Pubkey, // USDC mint account地址(SVM)或合约地址(EVM)
pub nonce: u64, // 64位无符号整数,初始为0
pub target_contract: Pubkey,
pub source_chain_id: u64,
pub target_chain_id: u64,
}
// 接收端状态
pub struct ReceiverState {
pub vault: Pubkey, // 与发送端共享
pub admin: Pubkey, // 与发送端共享
pub usdc_mint: Pubkey, // USDC mint account地址(SVM)或合约地址(EVM),与发送端共享
pub relayer_count: u64,
pub source_contract: Pubkey,
pub source_chain_id: u64,
pub target_chain_id: u64,
pub relayers: Vec<Pubkey>, // 最多18个
pub last_nonce: u64, // 用于nonce递增判断
}重要设计决策: 金库使用 PDA(Program Derived Address)而不是普通钱包或多签钱包。
PDA 种子: [b"vault"]
设计优势:
- 自动转账:合约可以直接控制 PDA,无需外部签名即可执行转账
- 安全性:PDA 由程序控制,无法被外部直接操作
- 解锁效率:达到阈值后立即执行解锁,无需等待多签投票
- 简化实现:无需处理多签提案机制,保持合约逻辑简洁
Token Account PDA:
- 种子:
[b"vault_token", usdc_mint] - 所有者:vault PDA
- 用途:存储 USDC 代币
重要设计决策: 所有管理接口使用多签钱包调用,但合约层面不关心多签逻辑。
设计说明:
admin字段存储多签钱包地址- 合约只验证
admin签名,不关心是否是多签 - 多签逻辑在外部(Squad 程序)处理
- 管理接口包括:
initialize,configure_usdc,configure_peer,add_relayer,remove_relayer,add_liquidity,withdraw_liquidity
优势:
- 合约保持简洁,无需处理多签提案
- 管理操作需要多签保护,提高安全性
- 向后兼容,现有代码基本不需要修改
-
initialize: 统一初始化发送端和接收端合约
- 创建
SenderState账户 - 创建
ReceiverState账户 - 创建 PDA 金库(vault)和对应的 token account
- 设置 admin 为多签钱包地址
- 初始化 nonce 为 0
- 初始化 last_nonce 为 0
- 创建
-
configure_usdc: 配置USDC代币地址
- 同时配置发送端和接收端的
usdc_mint字段 - SVM端:传入USDC的SPL Token mint account地址
- EVM端:传入USDC的ERC20合约地址
- 因为两端使用同一个USDC代币,所以共享相同的地址
- 同时配置发送端和接收端的
-
configure_peer: 统一配置对端合约和链ID
- 同时配置发送端的
target_contract、source_chain_id、target_chain_id - 同时配置接收端的
source_contract、source_chain_id、target_chain_id - 因为对端是同一个,所以两个配置共享相同的参数
- 同时配置发送端的
-
stake (发送端): 质押代币并触发事件
- 验证USDC地址已配置:如果
sender_state.usdc_mint为无效地址(如Pubkey::default()),返回错误 - Nonce 递增逻辑:
- 当前 nonce =
sender_state.nonce - 新 nonce =
current_nonce + 1 - 如果
new_nonce == 0(溢出),重置为 0 - 更新
sender_state.nonce = new_nonce
- 当前 nonce =
- 验证USDC地址已配置:如果
-
add_relayer / remove_relayer (接收端): Relayer 白名单管理
- 重要变更: 移除了
ecdsa_pubkey参数,直接使用relayer的Solana公钥(Ed25519) add_relayer(relayer: Pubkey)- 只需要relayer地址- Relayer的Solana密钥就是Ed25519密钥,无需额外存储
- 重要变更: 移除了
-
submit_signature (接收端): 提交签名并检查阈值
- 验证USDC地址已配置:如果
receiver_state.usdc_mint为无效地址(如Pubkey::default()),返回错误 - Ed25519签名验证:使用Solana的Ed25519Program进行真实的密码学验证
- 客户端必须在交易中包含
Ed25519Program.createInstructionWithPublicKey()指令 - 合约检查Instructions Sysvar,确认Ed25519Program验证通过
- 解锁操作使用 PDA 金库作为 authority,自动执行转账
- 验证USDC地址已配置:如果
-
add_liquidity (接收端): 增加流动性
- 从多签钱包(admin)转账到 PDA 金库
- 需要 admin 签名(多签钱包)
- 用于向金库注入资金
-
withdraw_liquidity (接收端): 提取流动性
- 从 PDA 金库转账到多签钱包(admin)
- 需要 admin 签名(多签钱包)
- 使用 PDA 作为 authority 执行转账
- 用于从金库提取资金
1. 验证 relayer 在白名单中
2. 验证USDC地址已配置:如果 usdc_mint 为无效地址,返回错误 "USDC address not configured"
3. 验证源链合约地址和 chain ID
4. 检查 nonce 是否递增:
- 如果 nonce <= last_nonce,拒绝(重放攻击)
- 如果 nonce > last_nonce,继续处理
5. 获取或创建 CrossChainRequest PDA 账户
6. **初始化或验证 event_data 一致性**:
- 如果这是第一个签名(signatureCount == 0):
* 将传入的 event_data 存储为"标准答案"
* 这是后续所有 relayer 必须遵循的 event_data
- 如果不是第一个签名(signatureCount > 0):
* **关键安全机制**:验证传入的 event_data 是否与已存储的 event_data 完全一致
* 检查所有字段:sourceContract, targetContract, sourceChainId, targetChainId,
blockHeight, amount, receiverAddress, nonce
* 如果任何字段不匹配,拒绝并返回错误 "Invalid event data"
* 这防止恶意 relayer 提交不同的 event_data 导致数据不一致
7. 检查该 relayer 是否已为此 nonce 签名
8. **验证Ed25519签名**(真实密码学验证):
- 从Instructions Sysvar加载当前指令之前的所有指令
- 查找Ed25519Program指令(程序ID: Ed25519SigVerify111111111111111111111111111)
- 验证Ed25519Program指令中的签名、公钥、消息与我们的参数匹配
- 验证签名是否匹配传入的 event_data
- 如果找到匹配的Ed25519Program指令,说明签名已被密码学验证
- 如果没有找到或不匹配,拒绝签名
9. 记录签名到 CrossChainRequest.signed_relayers
10. 计算签名数量,检查是否达到阈值(> 2/3 relayer_count)
11. 如果达到阈值:
- 从 PDA 金库转账到接收地址(使用配置的 usdc_mint)
- **重要**:使用存储的 event_data(第一个 relayer 提交的)而不是函数参数
- 使用 PDA 作为 authority,无需外部签名
- 更新 last_nonce = 存储的 event_data.nonce(标记为已使用)
- 标记 CrossChainRequest.is_unlocked = true
- 可选:关闭 CrossChainRequest 账户回收租金
设计原则:以第一个提交的 event_data 为准
系统采用"第一个提交者决定"的设计原则:
- 第一个 relayer 提交的
event_data会被存储并成为"标准答案" - 后续所有 relayer 必须提交完全相同的
event_data才能通过验证 - 解锁时使用存储的
event_data,确保一致性
安全性分析:
-
防止恶意 relayer 提交错误数据:
- 如果第一个 relayer 提交错误的
event_data(如错误的 amount 或 receiver) - 正常 relayer 无法提交不同的
event_data(会被一致性检查拒绝) - 正常 relayer 也无法提交相同的错误
event_data(因为他们的签名是对正确数据的签名,签名验证会失败) - 结果:无法达到阈值,流程会卡住,不会执行错误的解锁
- 如果第一个 relayer 提交错误的
-
正常情况下的工作流程:
- 所有正常 relayer 监听链上事件,获取相同的正确
event_data - 第一个 relayer 提交正确的
event_data并存储 - 后续 relayer 提交相同的正确
event_data,通过一致性检查 - 每个 relayer 的签名都匹配他们提交的
event_data,通过签名验证 - 达到阈值后,使用存储的正确
event_data解锁
- 所有正常 relayer 监听链上事件,获取相同的正确
-
安全性保证:
- ✅ 需要 >2/3 的 relayer 签名才能解锁
- ✅ 所有 relayer 必须对相同的
event_data签名 - ✅ 签名验证确保每个 relayer 的签名匹配其提交的
event_data - ✅ 一致性检查确保所有 relayer 提交相同的
event_data - ✅ 即使第一个 relayer 是恶意的,正常 relayer 无法通过签名验证来确认错误的
event_data
潜在攻击场景与防护:
-
场景1:第一个 relayer 提交错误的
event_data- 正常 relayer 无法提交不同的数据(一致性检查拒绝)
- 正常 relayer 无法提交相同的错误数据(签名验证失败)
- 结果:无法达到阈值,系统安全
-
场景2:多个 relayer(< 阈值)被劫持并提交错误的
event_data- 如果第一个 relayer 正常,后续恶意 relayer 无法提交不同的数据
- 如果第一个 relayer 恶意但数量不足,正常 relayer 无法通过签名验证
- 结果:无法达到阈值,系统安全
-
场景3:超过 2/3 的 relayer 被同时劫持
- 这是系统威胁模型假设的攻击场景
- 如果超过 2/3 的 relayer 被劫持,系统无法防止恶意行为
- 这是所有多重签名系统的固有风险
为什么使用Ed25519Program?
- Solana BPF程序无法直接使用ed25519-dalek等库(需要std或getrandom)
- Ed25519Program是Solana原生预编译合约,提供高效的Ed25519验证
- 程序ID:
Ed25519SigVerify111111111111111111111111111
工作原理:
- 客户端创建两个指令:
Ed25519Program.createInstructionWithPublicKey()- 执行密码学验证submit_signature()- 执行业务逻辑
- Ed25519Program在交易执行前先验证签名
- 我们的合约通过Instructions Sysvar检查Ed25519Program指令
- 验证指令中的参数(签名、公钥、消息)与我们的匹配
- 如果交易成功执行到这里,说明签名验证通过
安全性:
- ✅ 真实的密码学验证,无法伪造签名
- ✅ 与Solana交易签名相同的安全级别
- ✅ 防止恶意relayer提交虚假签名
- ✅ 结合白名单+2/3阈值提供多层保护
当 nonce 达到 u64::MAX (18,446,744,073,709,551,615) 时:
- 下一次
stake调用时,nonce 会溢出并重置为 0 - 此时需要特殊处理:如果
last_nonce接近u64::MAX,允许 nonce 从 0 开始 - 实现逻辑:
let new_nonce = sender_state.nonce.wrapping_add(1); if new_nonce == 0 && sender_state.nonce != u64::MAX { // 异常情况,不应该发生 return Err(Error::NonceOverflow); } sender_state.nonce = new_nonce;
重要设计决策: EVM 合约中的金库地址(vault)和管理员地址(admin)完全支持多签钱包(如 Gnosis Safe),合约层面将其视为普通地址。
设计说明:
- 多签钱包兼容性:在 EVM 中,多签钱包(如 Gnosis Safe)实际上就是一个普通的
address类型 - 合约透明性:从合约的角度来看,多签钱包和普通 EOA(Externally Owned Account)没有区别
- 权限检查:合约只需要验证
msg.sender == admin或msg.sender == vault,不需要关心它们是否是多签 - 多签逻辑外部化:多签的投票、阈值检查等逻辑在外部处理(Gnosis Safe 合约),当多签通过后,会以多签钱包地址的身份调用我们的合约
优势:
- 向后兼容:支持普通 EOA 和多签钱包,无需修改合约代码
- 安全性提升:管理员和金库可以使用多签钱包,提高资金和管理操作的安全性
- 实现简洁:合约保持简洁,无需处理多签提案机制
- 灵活性:部署时可以选择使用普通地址或多签地址,根据安全需求灵活配置
使用示例:
// 初始化时,admin 可以是多签钱包地址,vault 自动设置为合约本身
initialize(
adminAddress // 可以是 Gnosis Safe 多签钱包地址
);
// 权限检查时,合约只验证地址,不关心是否是多签
modifier onlyAdmin() {
require(msg.sender == admin, "Only admin");
_;
}
// 转账时,从金库(可能是多签钱包)转账
IERC20(usdcContract).transferFrom(vault, receiver, amount);与 SVM 发送端合约功能相同,使用 Solidity 实现。
账户结构:
struct SenderState {
address vault; // 金库地址(可以是多签钱包)
address admin; // 管理员地址(可以是多签钱包)
address usdcContract; // USDC ERC20合约地址(未配置时为address(0))
uint64 nonce;
address targetContract;
uint64 sourceChainId;
uint64 targetChainId;
}stake 函数验证:
- 检查
usdcContract != address(0),否则返回错误 "USDC address not configured" - 从用户地址转账到金库地址(
vault),金库可以是多签钱包
使用 mapping 存储签名记录,无大小限制。
账户结构:
struct ReceiverState {
address vault; // 金库地址(可以是多签钱包)
address admin; // 管理员地址(可以是多签钱包)
address usdcContract; // USDC ERC20合约地址(未配置时为address(0))
uint64 relayerCount;
address sourceContract;
uint64 sourceChainId;
uint64 targetChainId;
address[] relayers; // 最多18个
uint64 lastNonce;
}submit_signature 函数验证:
- 检查
usdcContract != address(0),否则返回错误 "USDC address not configured" - 达到阈值后,从金库地址(
vault)转账到接收地址,金库可以是多签钱包
- initialize: 初始化合约(设置vault和admin,可以是多签钱包地址)
- configure_usdc: 配置USDC ERC20合约地址(必须在使用前配置)
- configure_peer: 配置对端合约和链ID
Gateway 服务是一个独立的 HTTP 服务,用于完成跨链桥的第二步:从 Arbitrum 到 1024chain。与 Relayer 服务不同,Gateway 服务不监听链上事件,而是接收外部 HTTP 请求。
与 Relayer 的区别:
- Relayer:监听链上事件、签名验证、多签提交(双向跨链)
- Gateway:接收外部 HTTP 请求,使用中转钱包调用 EVM stake 接口(单向:Arbitrum → 1024chain)
工作流程:
- 用户使用成熟的跨链桥(如 LiFi)将资产从任意链跨链到 Arbitrum
- USDC 转入中转钱包地址
- Gateway 服务接收 HTTP 请求(参数:USDC 金额、目标地址)
- 服务使用中转钱包调用 EVM stake 合约接口
- 完成从 Arbitrum 到 1024chain 的第二步跨链
-
HTTP API 服务器
- 使用 Axum 框架(Rust)
- 接收 POST
/stake请求 - 请求参数:
amount(USDC 金额,字符串格式)、target_address(1024chain 接收地址) - 返回:
success、message、tx_hash
-
USDC 余额检查
- 自动检查中转钱包的 USDC 余额
- 如果余额不足,返回错误信息
-
USDC 授权管理
- 自动检查 USDC allowance
- 如果 allowance 不足,自动调用
approve函数 - 使用最大授权金额(10^18),避免频繁 approve 操作
-
交易发送
- 使用 Mutex 序列化交易发送,避免 nonce 冲突和余额检查竞态
- 调用 EVM stake 合约接口
- 等待交易确认并返回交易哈希
- 异步运行时:Tokio 1.35
- HTTP 框架:Axum 0.7
- EVM 交互:Ethers-rs 2.0
- 日志系统:tracing + tracing-subscriber
- 配置管理:dotenvy(环境变量)
RPC_URL:Arbitrum RPC 地址PRIVATE_KEY:中转钱包私钥(hex 格式,带或不带 0x 前缀)BRIDGE_CONTRACT_ADDRESS:Bridge 合约地址USDC_CONTRACT_ADDRESS:USDC ERC20 合约地址CHAIN_ID:链 ID(默认 421614,Arbitrum Sepolia)PORT:HTTP 服务端口(默认 8084)
- 私钥管理:私钥通过环境变量配置,不应硬编码
- 并发控制:使用 Mutex 序列化关键操作,避免 nonce 冲突
- 余额检查:每次请求前检查余额,防止余额不足的交易
- 授权管理:使用最大授权金额,减少 approve 操作频率
- 1024chain → 任意链:待实现
- 监听 1024chain 事件
- 调用 SVM stake 接口
- 完成从 1024chain 到任意链的跨链
EVM 事件监听:
- 监听 EVM 链(Arbitrum)的
StakeEvent事件 - 使用 Web3.js 或 Ethers.js 连接到 EVM RPC
- 解析事件日志获取
StakeEventData
SVM 事件监听:
- 监听 SVM 链(Solana/1024chain)的
StakeEvent事件 - 使用 Solana Web3.js 连接到 SVM RPC
- 解析 Anchor 事件获取
StakeEventData
Relayer 需要同时支持两种签名算法,根据目标链选择:
Ed25519 签名(用于提交到 SVM):
- 算法:Ed25519(Solana 原生)
- 数据格式:Borsh 序列化的
StakeEventData - 签名库:
@noble/ed25519或tweetnacl - 密钥对:Solana Keypair (32 字节私钥)
- 输出:64 字节签名 +
Ed25519Program指令
ECDSA 签名(用于提交到 EVM):
- 算法:ECDSA (secp256k1)(Ethereum 原生)
- 数据格式:JSON 序列化 → SHA-256 → EIP-191 格式
- 签名库:
ethers.js或web3.js - 密钥对:Ethereum 私钥 (32 字节)
- 输出:65 字节签名 (r, s, v)
SVM → EVM 转换:
// 1. 从 SVM 事件获取数据(Borsh 格式)
const svmEventData = parseAnchorEvent(event);
// 2. 转换为 EVM 格式
const evmEventData = {
sourceContract: svmEventData.sourceContract.toBase58(), // 转换为字符串
targetContract: evmEventData.targetContract, // EVM 地址
chainId: svmEventData.sourceChainId.toString(),
blockHeight: svmEventData.blockHeight.toString(),
amount: svmEventData.amount.toString(),
receiverAddress: svmEventData.receiverAddress,
nonce: svmEventData.nonce.toString()
};
// 3. JSON 序列化
const jsonString = JSON.stringify(evmEventData);
// 4. 计算哈希(SHA-256 + EIP-191)
const sha256Hash = createHash('sha256').update(jsonString).digest();
const ethSignedHash = keccak256(
Buffer.concat([
Buffer.from('\x19Ethereum Signed Message:\n32'),
sha256Hash
])
);
// 5. 使用 ECDSA 私钥签名
const signature = await ecdsaWallet.signMessage(ethSignedHash);EVM → SVM 转换:
// 1. 从 EVM 事件获取数据
const evmEvent = parseEthereumEvent(log);
// 2. 转换为 SVM 格式(构造 Anchor 类型)
const svmEventData: StakeEventData = {
sourceContract: new PublicKey(evmEvent.sourceContract),
targetContract: new PublicKey(svmEvent.targetContract),
sourceChainId: new BN(evmEvent.chainId),
targetChainId: new BN(evmEvent.targetChainId),
blockHeight: new BN(evmEvent.blockHeight),
amount: new BN(evmEvent.amount),
receiverAddress: evmEvent.receiverAddress,
nonce: new BN(evmEvent.nonce)
};
// 3. Borsh 序列化
const message = program.coder.types.encode("StakeEventData", svmEventData);
// 4. 使用 Ed25519 私钥签名
const signature = await ed25519.sign(message, keypair.secretKey.slice(0, 32));
// 5. 创建 Ed25519Program 验证指令
const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({
publicKey: keypair.publicKey.toBytes(),
message: message,
signature: signature
});提交到 EVM:
- 构造
submitSignature交易 - 使用 Ethers.js 发送交易
- 处理 Gas 估算和失败重试
- 监控交易确认状态
提交到 SVM:
- 构造交易包含两个指令:
Ed25519Program验证指令submitSignature业务指令
- 使用 Solana Web3.js 发送交易
- 处理计算单元和失败重试
- 监控交易确认状态
每个 Relayer 需要维护两对密钥:
Ed25519 密钥对(用于 SVM):
// Solana Keypair
const svmKeypair = Keypair.generate();
// 或从现有密钥恢复
const svmKeypair = Keypair.fromSecretKey(secretKey);ECDSA 密钥对(用于 EVM):
// Ethereum Wallet
const evmWallet = new ethers.Wallet(privateKey);
// 或生成新钱包
const evmWallet = ethers.Wallet.createRandom();- 支持最多 18 个 relayer
- 每个 relayer 独立运行,维护双密钥对(Ed25519 + ECDSA)
- 阈值:
Math.ceil(relayer_count * 2 / 3) - 支持至少 100 个未完成的跨链请求
- 支持至少 1200 个签名缓存
Squad 多签程序:
- 程序地址:
SMPLecH534NA9acB4bMolv7X6RBpK4rjn3LkN1gZXYjy - 主要功能:创建多签账户、提案和投票、执行提案
实现方式:
- 所有管理接口使用 Squad 多签钱包作为
admin - 合约层面只验证
admin签名,不关心多签逻辑 - 多签投票在外部(Squad 程序)处理
- 管理接口包括:
initialize,configure_usdc,configure_peer,add_relayer,remove_relayer,add_liquidity,withdraw_liquidity
使用示例:
// 创建 Squad 多签账户
const squad = new Squad(provider);
const multisig = await squad.createMultisig({
threshold: 2, // 需要2个签名
members: [admin1, admin2, admin3],
});
// 使用多签钱包作为 admin 初始化合约
await program.methods
.initialize()
.accounts({
admin: multisig.publicKey, // 多签钱包地址
vault: vaultPda, // PDA 金库地址
// ...
})
.rpc(); // 需要多签成员签名(外部处理)注意事项:
- 多签操作需要额外的 gas 费用
- 多签投票需要等待,但只影响管理操作,不影响业务操作(解锁使用 PDA)
- 需要确保 Squad 程序在目标链(1024chain)上可用
- 多签成员可以变更(外部处理)
- Nonce 递增判断机制:
- 每个质押请求有唯一的 nonce(64 位无符号整数)
- 新 nonce 必须大于
last_nonce(递增判断) - 如果 nonce <=
last_nonce,视为重放攻击,拒绝处理 - Nonce 溢出处理:当达到
u64::MAX时,重置为 0
- 区块高度验证:事件中包含 block_height,可用于额外验证
- 阈值计算:
Math.ceil(relayer_count * 2 / 3) - 当签名数 >= 阈值时解锁(即 2/3 向上取整的 relayer 签名)
- 最多支持 18 个 relayer,阈值范围:2-12
- 管理员权限:初始化、配置、relayer 管理
- Relayer 权限:只能提交签名
- 用户权限:只能调用质押接口
- 使用 PDA 确保账户所有权
- 解锁后可以关闭 CrossChainRequest 账户回收租金
- Nonce 递增判断确保不会重放已处理的请求
问题背景:
在修复前,系统存在一个潜在的漏洞:如果第一个 relayer 提交错误的 event_data(例如错误的 amount 或 receiver),后续 relayer 可能会提交不同的 event_data,导致跨链请求参数不一致,最终可能按照错误的数据解锁代币。
修复方案: 系统实现了严格的 event_data 一致性验证机制,采用"第一个提交者决定"的设计原则:
-
第一个 relayer 提交时:
- 将传入的
event_data存储为本次跨链请求的"标准答案" - 所有字段(sourceContract, targetContract, sourceChainId, targetChainId, blockHeight, amount, receiverAddress, nonce)被永久固定
- 将传入的
-
后续 relayer 提交时:
- 强制一致性检查:验证传入的
event_data是否与已存储的event_data完全一致 - 如果任何字段不匹配,直接拒绝并返回错误 "Invalid event data"
- 这确保所有 relayer 必须对相同的事件数据签名
- 强制一致性检查:验证传入的
-
解锁时:
- 使用存储的
event_data(第一个 relayer 提交的),而不是函数参数的event_data - 这确保即使后续 relayer 传入的参数被修改,解锁操作仍使用最初存储的正确数据
- 使用存储的
安全性保证:
- ✅ 防止数据不一致:所有 relayer 必须提交完全相同的
event_data - ✅ 防止参数篡改:即使恶意 relayer 尝试提交不同的参数,也会被一致性检查拒绝
- ✅ 签名验证双重保护:签名验证确保每个 relayer 的签名匹配其提交的
event_data,一致性检查确保所有 relayer 提交相同的event_data - ✅ 第一个 relayer 保护:如果第一个 relayer 恶意提交错误数据,正常 relayer 无法通过签名验证(他们的签名是对正确数据的签名),导致无法达到阈值
攻击场景分析:
| 攻击场景 | 防御机制 | 结果 |
|---|---|---|
第一个 relayer 提交错误的 event_data |
正常 relayer 的签名验证会失败(签名是对正确数据的签名) | ❌ 无法达到阈值,流程卡住,不会执行错误的解锁 |
后续 relayer 提交不同的 event_data |
一致性检查拒绝(与存储的数据不匹配) | ❌ 交易被拒绝 |
| 部分 relayer 被劫持(< 2/3) | 阈值机制(需要 > 2/3 签名)+ 签名验证 | ❌ 无法达到阈值 |
| 超过 2/3 的 relayer 被同时劫持 | 这是系统威胁模型假设的攻击场景 |
实现位置:
- SVM 合约:
submit_signature函数中的else分支(第 219-232 行) - EVM 合约:
submitSignature函数中的else分支(第 312-327 行) - 错误代码:
InvalidEventData(SVM) 和InvalidEventData()(EVM)
- ReceiverState 主账户:~708 bytes(固定大小)
- CrossChainRequest PDA:~600+ bytes(固定大小,每个请求独立账户)
- 支持无限 nonce(每个 nonce 独立账户)
- 支持至少 100 个未完成的请求同时存在
last_nonce使用单值存储,O(1) 查找- Nonce 递增判断:O(1) 比较操作
signed_relayers使用 Vec 存储,O(n) 查找(n <= 18)
- 每个 CrossChainRequest PDA 账户需要租金(约 0.001 SOL)
- 解锁后可以关闭账户回收租金
- 建议定期清理已解锁的账户
- 使用位图压缩:如果 relayer 数量固定,可以使用位图跟踪签名状态
- 批量清理:定期批量关闭已解锁的 CrossChainRequest 账户
- Nonce 递增判断:通过
last_nonce实现 O(1) 的重放检查,无需存储历史记录
- Relayer 数量:最多 18 个(由 ReceiverState.relayers Vec 大小决定)
- Nonce 数量:理论上无限(每个 nonce 使用独立 PDA)
- 未完成请求数量:至少支持 100 个(每个请求独立 PDA 账户)
- 签名缓存容量:至少 1200 个签名(100 个请求 × 18 个 relayer = 1800 个签名)
- Nonce 类型:64 位无符号整数(u64),溢出时重置为 0
| 日期 | 变更内容 |
|---|---|
| 2025-11-14 | 初始设计文档,采用 PDA 方案支持无限请求和 21 个 relayer |
| 2025-11-15 | 重构设计:支持 18 个 relayer;nonce 使用递增判断机制;统一初始化函数;支持 100+ 未完成请求和 1200+ 签名缓存 |
| 2025-11-15 | 多签钱包集成:管理接口使用多签钱包(Squad),合约层面不关心多签逻辑;金库使用 PDA 支持自动转账;新增流动性管理接口(add_liquidity, withdraw_liquidity) |
| 2025-11-15 | 整合 Squad 多签文档内容到设计文档和 API 文档,删除独立的 squad_multisig.md 文档 |
| 2025-11-15 | 密码学算法设计:明确 SVM 使用 Ed25519(原生),EVM 使用 ECDSA (secp256k1) + EIP-191(原生);Relayer 负责格式转换;最大化性能和安全性 |