EVM和SVM开发者的Sui Move指南:第一部分 - 心理模型

本文深入探讨了EVM、Solana和Sui Move在处理代币转账和访问控制等基本操作上的根本差异。EVM 依赖存储槽和运行时检查,Solana 通过签名验证将代码与数据分离,Sui 将所有内容视为具有编译时安全性的可拥有对象。理解这些模型对于开发至关重要,因为它们影响从数据组织到权限管理的各个方面。

心智模型:从存储到对象

EVM围绕智能合约内的存储槽组织状态,所有变量都以连续的 256 位槽存在。Solana 将所有内容都视为一个账户:程序是无状态的,数据存在于单独的账户结构中。Sui 将所有内容都视为具有唯一 ID 和清晰所有权的对象:你从字面意义上拥有事物,甚至包括权限(稍后会详细介绍)。每种方法都会改变你思考组织和访问数据的方式。

EVM:整体式合约存储

EVM 将每个智能合约视为一台具有自身存储空间的微型计算机。当你部署合约时,所有的状态变量、映射、余额和/或数组都会被打包到合约地址内连续的 256 位槽中。所有内容都位于一个地方,这使得推理变得很简单,但当合约变得庞大而复杂时,可能会变得混乱。对于来自传统编程的开发者来说,这是最熟悉的模型,在传统编程中,你会自然地将相关的数据和函数分组到一个类或模块中。

evm storage image

SVM:基于账户的架构

Solana 的运作原则是所有内容都是一个账户:程序、数据,甚至系统本身。智能合约代码存在于程序账户中,用户余额存在于代币账户中,任何持久性数据都存储在专用的数据账户中。这意味着你不能简单地声明一个变量并期望它能持久存在;你需要设计账户结构来保存你的数据,并明确管理你的程序与哪些账户进行交互。好处是能够进行大规模并行化,因为运行时确切地知道每个交易将触及哪些账户,但这需要以账户关系而不是传统的合约存储来思考。

Sui Move:以对象为中心的模型

Sui 将所有内容都视为具有唯一 ID 和清晰所有权规则的对象——代币、NFT、智能合约和系统资源都作为单独的对象存在,而不是存储在合约或账户中的数据。与其他区块链将逻辑和数据捆绑在一起不同,Sui 将代码(模块对象)与数据(实例对象)分开。你的代币余额作为一个独立的个体存在,它引用共享模块对象来获取其行为。当你转移价值或更新状态时,你实际上是在移动或修改这些对象,并且像代币转账这样的简单操作可以完全绕过验证器共识,因为它们只影响单所有者对象。这种以对象为中心的模型使组合变得自然,因为对象可以包含其他对象或引用,但需要围绕所有权模式和对象交互而不是传统的基于合约的逻辑来重新思考应用程序架构。

跨生态系统的基本操作

现在,让我们讨论一下每个智能合约中涉及的基本操作,并看看与 SVM 和 EVM 相比,它们在 Move 中是如何实现的。

原生代币转账

Ethereum

在 Ethereum 中,原生 ETH 转移通过全局区块链状态中的直接余额更新来实现。当 Alice 向 Bob 发送 1 ETH 时,EVM 会在单个交易中原子地减少 Alice 的账户余额并增加 Bob 的账户余额。两个余额都存储在全局状态 trie 中,这意味着每个 ETH 转移都需要世界状态更新和完整的验证器共识。这是 Ethereum 中最简单的价值转移形式 - 不涉及智能合约,只是协议通过状态转换函数原生处理的直接账户到账户的余额修改。

Solana

在 Solana 中,原生 SOL 转移由系统程序处理,该程序管理账户创建和 SOL 余额更新。当 Alice 向 Bob 发送 1 SOL 时,她会创建一个包含 system_instruction::transfer 指令的交易,该指令会调用系统程序。然后,系统程序会从 Alice 的账户中减少 lamports(SOL 的最小单位)并将其添加到 Bob 的账户中。与 EVM 的全局状态模型不同,每个用户都有自己的账户,该帐户将其 SOL 余额存储为账户数据。系统程序通过读取两个账户、通过加密签名验证所有权以及原子地更新两个账户余额来执行此转移。这种基于账户的方法允许 Solana 并行处理多个非冲突转移,因为运行时可以识别每个交易将修改哪些账户。

Sui

在 Sui Move 中,原生 SUI 转移通过对象操作而不是余额更新来实现。当 Alice 向 Bob 发送 1 SUI 时,她现有的 Coin 对象会被分成两个单独的对象:

  • 一个保留在 Alice 手中,包含剩余余额,以及
  • 创建一个新的 Coin 对象,其中包含转移金额。然后,通过更改其所有权字段将这个新对象转移给 Bob。

由于每个 Coin 对象都有一个唯一的 ID 和明确的所有权,因此简单的转移不需要验证者的共识 - 它们会立即被处理,因为交易只涉及具有单所有者的对象。这种以对象为中心的方法消除了对全局状态更新的需求,使得点对点转移比传统区块链架构更有效,在传统区块链架构中,每个交易都必须经过验证者共识。

访问控制

Ethereum

EVM 通过函数修饰符和使用调用者地址的运行时验证来处理访问控制。当调用像 mint() 这样的函数时,合约会根据存储的权限映射(如 owner 地址或 admins 映射)检查 msg.sender(交易发起者)。如果调用者没有所需的权限,修饰符中的 require() 语句将失败,并且整个交易将回滚,消耗 gas 但不进行任何状态更改。这种模式通过 onlyOwneronlyAdmin 等修饰符在合约级别强制执行,这些修饰符充当在实际函数逻辑之抢跑的网守。在下面的示例中,只有合约所有者可以调用 addAdmin()emergencyStop(),而所有者和指定的管理员都可以调用 mint()。这个系统的美妙之处在于它的简单性:权限是通过地址比较存储在合约状态中的,使用像 address public ownermapping(address => bool) public admins 这样的变量,这使得实现基于角色的访问控制、多重签名要求或复杂的权限层次结构变得容易。

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

contract TokenContract {
    address public owner;
    mapping(address => bool) public admins;
    uint256 private totalSupply;
    constructor() {
        owner = msg.sender;
    }
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }
    modifier onlyAdmin() {
        require(admins[msg.sender] || msg.sender == owner, "Not an admin");
        _;
    }
    function addAdmin(address admin) external onlyOwner {
        admins[admin] = true;
    }
    function mint(address to, uint256 amount) external onlyAdmin {
        // Only admins can mint tokens
        totalSupply += amount;
        // ... minting logic
    }
    function emergencyStop() external onlyOwner {
        // Only owner can stop the contract
        // ... emergency logic
    }
}

Solana

Solana 的访问控制遵循与 EVM 类似的原则,但使用不同的方法。两种系统都会在允许受限操作之前验证授权。EVM 将 msg.sender 与存储的地址进行比较,而 Solana 使用 require!(ctx.accounts.admin.key() == ctx.accounts.config.admin) 针对存储的 pubkeys 验证交易签名。关键的区别在于 Solana 更多地依赖于加密签名进行授权,而 EVM 在签名验证的基础上添加了运行时权限检查。PDAs 让 Solana 变得独特,允许程序拥有自己的签名权限 - 从 ["authority"] 种子派生的 authority 账户可以通过 CpiContext::new_with_signer() 以编程方式签署交易,从而无需人类持有协议密钥。两种方法都实现了限制访问的相同目标,只是通过不同的机制。

use anchor_lang::prelude::*;

declare_id!("9UPnoepLi34JnCrq6qJPVo9fSxZeHsJbnJZyfovt6eoZ");

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

    pub fn initialize(ctx: Context<Initialize>, admin: Pubkey) -> Result<()> {
        let config = &mut ctx.accounts.config;
        config.admin = admin;
        config.value = 0;
        Ok(())
    }

    pub fn update_value(ctx: Context<UpdateValue>, new_value: u64) -> Result<()> {
        // Simple access control: only admin can update
        require!(
            ctx.accounts.signer.key() == ctx.accounts.config.admin,
            ErrorCode::Unauthorized
        );
        ctx.accounts.config.value = new_value;
        Ok(())
    }

    pub fn change_admin(ctx: Context<ChangeAdmin>, new_admin: Pubkey) -> Result<()> {
        // Only current admin can change admin
        require!(
            ctx.accounts.signer.key() == ctx.accounts.config.admin,
            ErrorCode::Unauthorized
        );
        ctx.accounts.config.admin = new_admin;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = payer, space = 8 + 32 + 8)]
    pub config: Account<'info, Config>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateValue<'info> {
    #[account(mut)]
    pub config: Account<'info, Config>,
    pub signer: Signer<'info>,
}

#[derive(Accounts)]
pub struct ChangeAdmin<'info> {
    #[account(mut)]
    pub config: Account<'info, Config>,
    pub signer: Signer<'info>,
}

#[account]
pub struct Config {
    pub admin: Pubkey,
    pub value: u64,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Unauthorized: Only admin can perform this action")]
    Unauthorized,
}

Move

Move 通过必须显式拥有的能力对象(如 EVM 中的 NFT)来处理访问控制,但在类型系统级别强制执行。在 Move 中,所有权不是存储在合约状态中,而是直接由你的地址拥有。我们示例中的 AdminCap 是你钱包中的一个真实对象,要调用 update_balance(),你必须将其作为参数传递:public fun updatebalance(: &AdminCap, treasury: &mut Treasury, new_balance: u64)。没有能力对象?你无法调用该函数 - 编译器会阻止你。与其他系统可能会传递伪造的账户地址不同,Move 的对象系统确保你只能传递你实际拥有的能力 - 无法伪造或伪造能力引用。当模块使用 init() 初始化时,它会创建一个 AdminCap 并使用 transfer::transfer(admin_cap, tx_context::sender(ctx)) 将其直接转移给发布者。如果你想稍后给某人管理权限,你只需将你的 AdminCap 对象转移给他们即可。不需要复杂的权限映射 - 如果你拥有该能力,你就有权访问。

module treasury::admin {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    // Capability object that proves admin rights
    // 能力对象,证明管理权限
    public struct AdminCap has key, store {
        id: UID,
    }

    // Treasury that holds funds
    // 持有资金的金库
    public struct Treasury has key {
        id: UID,
        balance: u64,
    }

    // Only called once during module initialization
    // 仅在模块初始化期间调用一次
    fun init(ctx: &mut TxContext) {
        let admin_cap = AdminCap {
            id: object::new(ctx),
        };
        let treasury = Treasury {
            id: object::new(ctx),
            balance: 1000,
        };
        // Transfer AdminCap to module publisher
        // 将 AdminCap 转移给模块发布者
        transfer::transfer(admin_cap, tx_context::sender(ctx));
        transfer::share_object(treasury);
    }

    // Only callable if you own an AdminCap
    // 只有当你拥有 AdminCap 才能调用
    public fun update_balance(
        _: &AdminCap,               // Proof of authorization
        // 授权证明
        treasury: &mut Treasury,    // Treasury to update
        // 要更新的金库
        new_balance: u64
    ) {
        treasury.balance = new_balance;
    }
}

结论

我们已经介绍了 EVM、Solana 和 Sui Move 在处理代币转账和访问控制等基本操作方面的主要区别。EVM 使用存储槽和运行时检查,Solana 通过签名验证将代码与数据分离,而 Sui 将所有内容都视为具有编译时安全性的可拥有对象。

理解这些心智模型至关重要,因为它们塑造了你处理开发的各个方面(从数据组织到权限管理)的方式。相同的业务逻辑根据底层架构的不同,实现方式也会大相径庭。

第 2 部分中,我们将深入探讨来自其他生态系统的开发人员需要掌握的 Move 特有模式。我们将介绍类型安全性的见证模式、测试策略、时间处理、集合使用、动态对象字段以及其他使该语言在构建安全智能合约方面功能强大的 Move 习惯用法。

请继续关注更多深入探讨、真实世界的错误和智能合约最佳实践。

X / LinkedIn 上关注 @AdevarLabs - 安全交付。

  • 原文链接: adevarlabs.com/blog/sui-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Adevar labs
Adevar labs
Blockchain Security Firm | Rust, Solidity, Move, and beyond. Vulnerability discovery, practical remediation, and complete audit reports | Ship Safely.