返回 SC 笔记
SC Day 81

多链对比: EVM vs Solana vs Move 架构对比(上) - 编程模型与账户体系

### 一、三大平台设计哲学总览

2026-06-28
第四阶段:综合实战
多链对比EVMSolanaMove账户模型编程范式

日期: 2026-06-28 方向: 多链 阶段: 第四阶段:综合实战 标签: #多链对比 #EVM #Solana #Move #账户模型 #编程范式


今日目标

经过80天对 Solidity、Anchor(Solana)、Move(Sui/Aptos) 的深入学习,今天开始进行系统性的多链架构对比。本篇聚焦底层架构差异——账户模型、执行模型、存储模型、Gas/费用机制和编程范式,从"第一性原理"角度理解三大智能合约平台的设计哲学。

对于产品经理和架构师来说,理解这些底层差异直接决定了:

  1. 你的产品应该部署在哪条链
  2. 用户体验会受到哪些约束
  3. 安全模型有哪些根本区别
  4. 开发成本和迭代速度如何

核心概念

一、三大平台设计哲学总览

维度EVM (Ethereum/L2)Solana (SVM)Move (Sui/Aptos)
诞生年份201520202022
设计哲学通用世界计算机高性能交易引擎安全资产管理
核心语言Solidity/VyperRust (Anchor)Move
虚拟机EVM (栈机器)SVM (BPF/SBF)Move VM
共识PoS (Gasper)PoH + Tower BFTNarwhal & Bullshark (Sui)
TPS (实际)~30 (L1), ~2000 (L2)~3000-5000~10000+ (Sui)
最终确认~12分钟 (L1)~400ms~500ms (Sui)
编程范式OOP (面向对象)Rust风格 (系统编程)资源导向 (Resource-oriented)

二、账户模型深度对比

2.1 EVM: 全局状态树模型

EVM 采用**全局状态树(Global State Trie)**模型,所有状态存储在一棵 Merkle Patricia Trie 中。

EVM 账户结构:
┌──────────────────────────────┐
│        Account                │
│  ┌────────────────────────┐  │
│  │ nonce: uint256         │  │  ← 交易计数
│  │ balance: uint256       │  │  ← ETH余额
│  │ storageRoot: bytes32   │  │  ← 存储树根哈希
│  │ codeHash: bytes32      │  │  ← 合约代码哈希
│  └────────────────────────┘  │
│                              │
│  Storage (合约账户才有):      │
│  ┌────────────────────────┐  │
│  │ slot[0] → value        │  │
│  │ slot[1] → value        │  │
│  │ slot[keccak(key)] → v  │  │  ← mapping存储
│  │ ...                    │  │
│  └────────────────────────┘  │
└──────────────────────────────┘

关键特征:

  • 两种账户类型: EOA(外部拥有账户) 和 Contract Account(合约账户)
  • 合约拥有自己的存储空间,状态数据存储在合约内部
  • 所有 ERC20 代币余额存储在代币合约的 mapping 中,而非用户账户中
  • 全局状态意味着状态读写可能产生冲突,难以并行执行
// EVM 中的代币存储方式 — 余额存在合约里
contract ERC20 {
    // 所有用户的余额都存储在这个合约的 storage 中
    mapping(address => uint256) private _balances;

    // 转账: 修改合约内部的两个 storage slot
    function transfer(address to, uint256 amount) external {
        _balances[msg.sender] -= amount;  // slot = keccak256(msg.sender, 0)
        _balances[to] += amount;          // slot = keccak256(to, 0)
    }
}

2.2 Solana: Account Model (显式账户模型)

Solana 采用显式账户模型,程序(Program)和数据(Account)完全分离。

Solana 账户结构:
┌──────────────────────────────┐
│        Account                │
│  ┌────────────────────────┐  │
│  │ lamports: u64          │  │  ← SOL余额(1 SOL = 10^9 lamports)
│  │ data: Vec<u8>          │  │  ← 任意字节数据
│  │ owner: Pubkey          │  │  ← 拥有此账户的程序
│  │ executable: bool       │  │  ← 是否是程序
│  │ rent_epoch: u64        │  │  ← 租金周期
│  └────────────────────────┘  │
└──────────────────────────────┘

┌─────────────────────────────────────────────┐
│  Solana 的程序-数据分离                       │
│                                             │
│  Program Account        Data Account        │
│  ┌─────────────┐       ┌──────────────┐    │
│  │ executable  │       │ owner =      │    │
│  │ = true      │◄──────│ program_id   │    │
│  │ code (BPF)  │       │ data = {...} │    │
│  └─────────────┘       └──────────────┘    │
│                                             │
│  一个 Program 可以拥有无数个 Data Account     │
└─────────────────────────────────────────────┘

关键特征:

  • 程序(Program)不存储状态——所有状态存储在独立的 Account 中
  • 每个 Account 有一个 owner 字段,指向拥有它的 Program
  • 只有 owner Program 才能修改 Account 的 data 字段
  • 交易必须声明所有将要读写的 Account (有利于并行执行)
  • Token 余额存储在用户自己的 Token Account 中(Associated Token Account)
// Solana/Anchor 中的代币存储方式 — 每个用户有自己的 Token Account
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

#[derive(Accounts)]
pub struct TransferTokens<'info> {
    #[account(mut)]
    pub sender: Signer<'info>,

    // 发送方的 Token Account (独立账户,存储余额)
    #[account(
        mut,
        constraint = sender_token.owner == sender.key(),
    )]
    pub sender_token: Account<'info, TokenAccount>,

    // 接收方的 Token Account (独立账户)
    #[account(mut)]
    pub receiver_token: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
}

pub fn transfer(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
    // CPI: 调用 Token Program 修改两个独立 Account 的数据
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.sender_token.to_account_info(),
                to: ctx.accounts.receiver_token.to_account_info(),
                authority: ctx.accounts.sender.to_account_info(),
            },
        ),
        amount,
    )?;
    Ok(())
}

2.3 Move (Sui): Object Model (对象模型)

Sui 采用对象模型(Object Model),一切皆对象(Object),资源天然不可复制。

Sui Object 模型:
┌──────────────────────────────────────────┐
│  Object                                   │
│  ┌────────────────────────────────────┐  │
│  │ id: UID (全局唯一)                  │  │
│  │ owner: 地址 / Shared / Immutable   │  │
│  │ type: 结构体类型                    │  │
│  │ data: 结构体字段                    │  │
│  └────────────────────────────────────┘  │
│                                          │
│  所有权类型:                              │
│  ├── Owned: 单一地址拥有 (可并行)        │
│  ├── Shared: 所有人可访问 (需共识)       │
│  └── Immutable: 不可变 (可并行)          │
└──────────────────────────────────────────┘

关键特征:

  • Move 语言的线性类型系统: 资源(Resource)不能被复制或隐式丢弃
  • Coin<SUI> 就是一个 Object,拥有所有权和转移语义
  • Owned Object 之间的交易可以跳过共识,实现亚秒级确认
  • 天然防止双花和重入——资源在同一时刻只能存在于一个位置
// Move (Sui) 中的代币存储方式 — 代币就是对象
module example::token_transfer {
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;
    use sui::transfer;
    use sui::tx_context::TxContext;

    // 转账: 将 Coin 对象的所有权从发送方转移到接收方
    public entry fun send_coin(
        coin: Coin<SUI>,       // 发送方拥有的 Coin 对象
        recipient: address,
        _ctx: &mut TxContext,
    ) {
        // 转移所有权 — 编译器保证 coin 之后不能再被使用
        transfer::public_transfer(coin, recipient);
        // coin 在此处已经"消失",不可能双花
    }

    // 拆分 + 转账
    public entry fun split_and_send(
        coin: &mut Coin<SUI>,  // 可变引用
        amount: u64,
        recipient: address,
        ctx: &mut TxContext,
    ) {
        let split_coin = coin::split(coin, amount, ctx);
        transfer::public_transfer(split_coin, recipient);
    }
}

三、执行模型对比

维度EVMSolana SVMMove VM
执行方式顺序执行并行执行 (Sealevel)并行执行 (Owned Object)
并行策略无 (L1),乐观并行 (部分L2)声明式并行——交易预声明读写Account基于对象所有权自动并行
调用模型内部调用 (CALL/DELEGATECALL)CPI (Cross-Program Invocation)Module 函数调用
栈深度限制1024~4层CPI无硬性限制
计算单位Gas (21000基础)Compute Units (200K默认/1.4M最大)Gas (类似EVM但更便宜)

并行执行的关键差异

EVM 顺序执行:
TX1 ──► TX2 ──► TX3 ──► TX4    (一个一个来)

Solana 声明式并行:
TX1 (accounts: [A, B]) ──┐
TX2 (accounts: [C, D]) ──┤──► 并行执行 (无冲突)
TX3 (accounts: [A, E]) ──┘──► TX1 和 TX3 冲突,顺序执行

Sui 基于所有权并行:
TX1 (owned obj X) ────────┐
TX2 (owned obj Y) ────────┤──► 全部并行 (owned = 无共识)
TX3 (shared obj Z) ───────┘──► shared 才需要共识

四、存储模型与成本

维度EVMSolanaSui
存储位置合约内部 Storage独立 AccountObject
存储成本一次性写入(20000 gas)租金(Rent)或免租金(2年)存储基金(Storage Fund)
存储大小限制理论无限(Gas限制)10MB/AccountObject大小限制
状态膨胀问题严重(永久存储)较好(租金机制)较好(存储基金)
删除退款是(SSTORE清零退Gas)是(关闭Account退租金)是(删除Object退费)
// EVM 存储成本示例
contract StorageCost {
    uint256 public value;

    // 冷 SSTORE (从0到非0): 20,000 gas ≈ $0.5-2 (取决于Gas价格)
    // 热 SSTORE (非0到非0): 2,900 gas
    // SSTORE 清零: 退还 4,800 gas
    function setValue(uint256 _value) external {
        value = _value;  // 首次写入最贵
    }
}
// Solana 租金计算
// rent_exempt_minimum = 账户数据大小 * 每字节费率 * 2年
// 例: 165 bytes 的 Token Account ≈ 0.00203928 SOL
// 关闭账户时,租金全额退还

#[account]
pub struct GameState {
    pub player: Pubkey,      // 32 bytes
    pub score: u64,          // 8 bytes
    pub level: u8,           // 1 byte
    // 总计 ~41 bytes + 8 bytes (Anchor discriminator) = 49 bytes
    // rent_exempt ≈ 0.00114144 SOL
}

五、Gas/费用机制对比

维度EVMSolanaSui
费用单位Gas × Gas Price固定基础费 + Priority FeeGas Units × Gas Price
典型转账费~$0.5-5 (L1), ~$0.01 (L2)~$0.00025~$0.001-0.01
复杂交易费$5-100+ (L1)~$0.001-0.01~$0.01-0.1
费用可预测性低 (Gas Price波动)高 (基础费固定)
费用支付只能ETH只能SOLSUI (支持赞助交易)
费用赞助ERC-4337 Paymaster无原生支持原生支持(Sponsored TX)

六、编程范式深度对比

6.1 同一逻辑的三种实现: 简易金库(Vault)

Solidity (OOP 面向对象):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleVault {
    // 状态变量 — 存储在合约内部
    mapping(address => uint256) public balances;
    IERC20 public immutable token;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    constructor(address _token) {
        token = IERC20(_token);
    }

    function deposit(uint256 amount) external {
        token.transferFrom(msg.sender, address(this), amount);
        balances[msg.sender] += amount;
        emit Deposited(msg.sender, amount);
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
        emit Withdrawn(msg.sender, amount);
    }
}

Anchor/Rust (系统编程风格):

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

declare_id!("Vault111111111111111111111111111111111111111");

#[program]
pub mod simple_vault {
    use super::*;

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        // CPI: 转移代币到 vault token account
        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.user_token.to_account_info(),
                    to: ctx.accounts.vault_token.to_account_info(),
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            amount,
        )?;

        // 更新用户余额记录
        ctx.accounts.user_state.deposited += amount;
        Ok(())
    }

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        require!(
            ctx.accounts.user_state.deposited >= amount,
            VaultError::InsufficientBalance
        );

        // PDA 签名的 CPI
        let seeds = &[b"vault", &[ctx.bumps.vault_auth]];
        let signer = &[&seeds[..]];

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault_token.to_account_info(),
                    to: ctx.accounts.user_token.to_account_info(),
                    authority: ctx.accounts.vault_auth.to_account_info(),
                },
                signer,
            ),
            amount,
        )?;

        ctx.accounts.user_state.deposited -= amount;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(mut, seeds = [b"user", user.key().as_ref()], bump)]
    pub user_state: Account<'info, UserState>,
    #[account(mut)]
    pub user_token: Account<'info, TokenAccount>,
    #[account(mut)]
    pub vault_token: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
}

#[account]
pub struct UserState {
    pub deposited: u64,
}

Move/Sui (资源导向):

module vault::simple_vault {
    use sui::coin::{Self, Coin};
    use sui::balance::{Self, Balance};
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    // Vault 是一个 shared object
    struct Vault<phantom T> has key {
        id: UID,
        balance: Balance<T>,
    }

    // 用户存款凭证 — 一个 owned object
    struct Receipt<phantom T> has key, store {
        id: UID,
        amount: u64,
    }

    // 创建 Vault
    public fun create_vault<T>(ctx: &mut TxContext) {
        let vault = Vault<T> {
            id: object::new(ctx),
            balance: balance::zero(),
        };
        transfer::share_object(vault);
    }

    // 存款: Coin 被消耗(线性类型保证),返回 Receipt
    public fun deposit<T>(
        vault: &mut Vault<T>,
        coin: Coin<T>,
        ctx: &mut TxContext,
    ): Receipt<T> {
        let amount = coin::value(&coin);
        let coin_balance = coin::into_balance(coin);
        // coin 在此已被消耗,不可能双花
        balance::join(&mut vault.balance, coin_balance);

        Receipt<T> {
            id: object::new(ctx),
            amount,
        }
    }

    // 取款: Receipt 被消耗,返回 Coin
    public fun withdraw<T>(
        vault: &mut Vault<T>,
        receipt: Receipt<T>,
        ctx: &mut TxContext,
    ): Coin<T> {
        let Receipt { id, amount } = receipt;
        object::delete(id);
        // receipt 被销毁,不可能重复取款
        let withdrawn = balance::split(&mut vault.balance, amount);
        coin::from_balance(withdrawn, ctx)
    }
}

6.2 编程范式总结

范式特征Solidity (OOP)Rust/Anchor (系统编程)Move (资源导向)
状态管理合约内 mapping/变量独立 Account + PDAObject (Owned/Shared)
所有权无原生概念Rust 所有权 (编译期)线性类型 (编译期+运行时)
安全保证运行时检查编译期(Rust)+运行时编译期+类型系统
可组合性高 (任意合约调用)中 (CPI + Account约束)高 (模块+泛型)
学习曲线低 (类似JS/Java)高 (Rust + Solana概念)中 (新语言但直觉清晰)
代码量最少最多 (Account声明冗长)中等

关键要点总结

  1. EVM 的优势在生态和可组合性: 全球最大的开发者社区、最丰富的工具链、最多的 DeFi 协议。缺点是性能和并行能力有限。

  2. Solana 的优势在性能: 通过声明式并行和高效共识实现高TPS低延迟。缺点是开发体验相对复杂、Account 模型学习曲线陡。

  3. Move/Sui 的优势在安全模型: 线性类型从根本上消除了双花、重入等问题。Object 模型让并行执行更自然。缺点是生态仍在早期。

  4. 账户模型决定了一切: EVM的全局状态→难并行但好组合;Solana的分离账户→好并行但代码冗长;Move的对象模型→安全但生态小。

  5. 没有"最好"的链,只有"最适合"的链: 选择取决于你的产品需求、目标用户、安全要求和开发团队能力。


常见误区

  1. 误区: Solana 比 Ethereum 更好因为更快 — 速度只是一个维度。对于高价值 DeFi 操作,安全性和去中心化可能更重要。

  2. 误区: Move 的线性类型完全消除了安全问题 — 线性类型消除了资源层面的问题,但逻辑漏洞(如错误的价格计算、不当的访问控制)仍然存在。

  3. 误区: EVM 顺序执行意味着不能扩展 — L2 方案(Rollups)极大提升了 EVM 生态的可扩展性,且保留了可组合性。

  4. 误区: 选链 = 选语言 — 实际上选链更多取决于生态(用户在哪里)、流动性(资金在哪里)、工具链成熟度。


面试关联

核心面试题: "Compare smart contract platforms — EVM vs Solana vs Move"

30秒版本:

三大平台代表三种不同的设计哲学。EVM 优先可组合性和开发者体验,用全局状态和顺序执行换取最大生态兼容性;Solana 优先性能,用声明式账户模型实现并行执行和亚秒级确认;Move/Sui 优先安全性,用线性类型系统从根本上消除资源类漏洞。选择取决于产品需求——高价值 DeFi 选 EVM 生态,高频交易选 Solana,新范式应用可以考虑 Move。

追问准备:

  • Q: 如果你要做一个 NFT 游戏,选哪条链?

    • A: Sui。原因: (1) Object 模型天然适合 NFT 管理,每个 NFT 就是一个 owned object;(2) 亚秒级确认适合游戏交互;(3) 并行执行支持高并发;(4) 存储基金模式对长期存储友好。
  • Q: 为什么 EVM 仍然主导 DeFi?

    • A: 网络效应。(1) 最多的流动性和用户;(2) 最成熟的基础设施(预言机、桥、钱包);(3) 最大的开发者社区;(4) 可组合性——新协议可以直接调用已有协议。

参考资源

  1. Ethereum Yellow Paper — EVM 规范
  2. Solana Documentation - Programming Model — Solana 账户模型
  3. Sui Move Documentation — Sui Object 模型
  4. Aptos Move Book — Move 语言规范
  5. L2Beat — L2 生态数据对比
  6. DeFiLlama — 多链 TVL 和活跃度数据