给以太坊开发者的 Solana 开发完全指南

给以太坊开发者的 Solana 开发完全指南

在本文中,我们将深入探讨在以太坊和 Solana 上开发之间的主要区别,指导你如何在 Solana 上构建。对于来自以太坊的开发者来说,Solana 看起来和感觉都会有很大不同,并且具有多样化的工具集可供使用。本文将为你提供构建在 Solana 上所需的所有工具,以满足你在以太坊背景下构建在 Solana 上的需求。

Solana 与以太坊有何不同?

账户模型

在 Solana 上开发时,你将遇到的最显著区别是账户模型设计。了解 Solana 的账户模型为何设计不同是很有帮助的。与以太坊不同,Solana 旨在利用高端机器中的多个核心。在计算资源中存在这样一种趋势,即随着时间推移,可用核心数量增加并变得更便宜供人们购买。考虑到这一点,账户模型被设计为利用多个核心,创建一个可以使交易相互并行化的系统。这种并行化创造了进一步的优化,例如本地费用市场和更快的吞吐量,我们稍后将进行探讨。

那么“账户模型”是什么意思呢?在 Solana 上,账户类似于包含某些任意数据和特定修改规则的对象。在 Solana 上,一切都是账户,包括智能合约。与以太坊不同,以太坊中的每个智能合约都是一个包含执行逻辑和存储绑定在一起的账户,而 Solana 的智能合约是完全无状态的。

Solana 上的智能合约不携带自己的状态,必须将状态传递给它们以便执行。为了说明这一点,让我们看一下以太坊上的 Solidity 智能合约和 Solana 上使用 Rust 的两个计数器智能合约。

以太坊计数器智能合约

contract Counter {
  int private count = 0;

  function incrementCounter() public
  { count += 1;
  }

  function getCount() public constant returns (int) {
    return count;
  }
}

Solana 计数器程序

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

pub fn initialize_counter(_ctx: Context<InitializeCounter>) -> Result<()> {
  Ok(())
}

pub fn increment(ctx: Context<Increment>) -> Result<()> {

ctx.accounts.counter.count =   ctx.accounts.counter.count.checked_add(1).unwrap();
  Ok(())
 }
}

#[derive(Accounts)]
pub struct InitializeCounter<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
space = 8 + Counter::INIT_SPACE,
payer = payer
)]

pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
  #[account(mut)]
  pub counter: Account<'info, Counter>,
}

#[account]
#[derive(InitSpace)]
pub struct Counter {
  count: u64,
}

在 Solidity 中,你有 int private count = 0;,而在 Solana 上的 Rust 智能合约中,你有一个名为 initialize_counter 的结构,该初始计数器创建一个具有 count 为 0 的账户,然后你可以将此账户传递给 increment 以增加 count。这样可以避免在智能合约本身内部具有状态。

有单独的账户存储程序外部的数据。要执行程序中的逻辑,你需要传递要执行操作的账户。在这个 counter 程序的情况下,当调用 increment 函数时,你将向程序传递一个 counter 账户,程序将增加 counter 账户中的值。

Solana 账户模型的好处是什么?

Solana 账户模型最重要的好处之一是程序的可重用性。

以 ERC20 为例。ERC20 在以太坊上为代币定义了一个接口规范。每当有人想创建新代币时,开发者都必须重新部署具有指定值的 ERC20 智能合约到以太坊上,从而产生高昂的重新部署成本。

Solana 则不同。在创建新代币时,你无需在区块链上重新部署另一个智能合约。相反,你可以创建一个新账户,称为铸币账户,基于 Solana Token Program,其中该账户定义了一组值,包括流通中的代币数量、小数点、谁可以铸造更多代币以及谁可以冻结代币。

在 Solana 上,你无需编写任何 Rust 或智能合约即可部署新代币。只需向 Token Program 发送一个交易,以使用你选择的语言创建一个新代币,然后该代币将出现在你的钱包中。使用 Solana Program Library CLI,你可以通过一个命令完成此操作:

$ spl-token create-token

本地费用市场

拥有 Solana 账户模型的另一个幸运副作用是能够基于状态争用建模费用。如前所述,交易可以并行执行。但是,它们仅根据正在写入的账户来并行执行。例如,假设 Solana 上正在进行热门的 NFT 铸造活动。通常,这种热度会提高每个使用链的人的价格,但是在这种情况下,所有未参与 NFT 铸造的人不受影响。

顾名思义,费用市场是每个账户的本地费用。如果你在发送 USDC 转账时,而其他人都在铸造最热门的新 NFT,你将不受影响,并继续支付你在 Solana 上习惯的低费用。这适用于 Solana 中的任何应用程序,避免了你在以太坊上习惯的常见全局费用市场,同时降低了每个人的成本。

Solana 上的费用是如何工作的?

Solana 上的费用分为几个类别:基础费用、优先费用和租金。

基础费用可以根据交易中的签名数量计算。每个签名的成本为 5000 lamports(0.000000001 sol = 1 lamport)。如果你的交易需要 5 个签名,则基础费用将为 25000 lamports。这种基础费用为集群的签名验证增加了经济压力,这是更耗费计算资源的行为之一。基础费用的一半被销毁,另一半奖励给验证者。

优先费用是任何人都可以添加到交易中以优先于同时执行的其他交易的可选费用。优先费用是基于交易中使用的计算单元数量来衡量的。计算单元类似于以太坊上的 Gas,是衡量交易所需的计算资源的简单度量。与基础费用一样,优先费用的一半被销毁,另一半奖励给验证者。

最后的费用,租金,更像是押金而不是费用。当你在网络上创建账户或分配空间时,你必须为网络存入一些 SOL 以保持你的账户。租金是根据网络上存储的字节数计算的,并且为分配空间收取额外的基础费用。重要的是要注意,租金费用不会丢失;如果你关闭账户并允许集群重新收回分配的空间,那么这些租金费用可以被收回。

Solana 上的交易是如何工作的?

在执行交易时支付每笔费用时,了解交易如何工作是很重要的。一笔交易包括四个部分:

  • 一个或多个指令
  • 一个要读取或写入的账户数组
  • 一个或多个签名
  • 最近的区块哈希或 nonce 一个指令是 Solana 上最小的执行逻辑。指令是更新全局 Solana 状态的调用。指令调用程序,该程序调用 Solana 运行时以更新状态(例如,调用代币程序将代币从你的帐户转移到另一个帐户)。你可以将指令视为在以太坊智能合约上的函数调用。

以太坊和 Solana 之间的一个重要区别是单个交易中函数调用的数量,这取决于指令的数量。每个交易中有多个指令使开发人员受益,因为他们不必创建自定义智能合约来链接单个交易中的函数。每个指令可以是一个单独的函数调用,在交易中按顺序执行。交易是原子性的,这意味着如果其中任何指令失败,整个交易将失败,你只需支付交易费用。这就像由于未在以太坊上设置正确的滑点而导致交易失败。

另一个要记住的关键区别是在交易中使用最近的 blockhash 而不是增量 nonce。当钱包想要进行交易时,将从集群中提取最近的 blockhash 来创建有效的交易。这个最近的 blockhash 仅使交易在提取最近的 blockhash 后的 150 个区块内有效。这可以防止长期存在的交易签名在以后的某个时间执行。

交易在 Solana 上有哪些限制?

与以太坊的 gas 限制类似,Solana 的交易也有特定的计算单位限制。每个限制如下:

以太坊 Solana
单个交易计算上限 30,000,000 1,400,000 计算单位
区块计算上限 30,000,000 Gas 48,000,000 计算单位

Solana 对交易还设置了其他一些限制 。每个引用的账户在每个区块最多可使用 12,000,000 计算单位。这个限制防止一个账户在单个区块中过多地锁定写入,进一步防止本地费用市场被一个账户占据。

交易的另一个限制是你可以在单个指令中进行的指令调用深度。这个限制目前设置为 4,这意味着在交易将回滚之前,你只能在深度为 4 的位置调用指令。这使得 Solana 上不存在重入问题,与你在以太坊上需要担心的问题相比。

内存池在哪里?

与以太坊不同,Solana 上没有内存池。Solana 验证者将交易转发给排定的最多四个领导者。虽然 Solana 没有内存池,但它仍然有优先费用来帮助排序交易。没有内存池会导致交易从领导者跳到领导者,直到区块哈希过期,但它减少了跨集群传递内存池的开销。

我在哪里可以找到智能合约代码?

在 EVM 世界中,大多数人在查看智能合约地址时会在 Etherscan 上找到智能合约代码。然而,在 Solana 生态系统中,通过浏览器查看智能合约代码相对较新,需要与 EVM 标准相比建立。在撰写本文时,Solana.fm 是唯一支持基于可验证构建查看智能合约代码的浏览器。

你可以通过访问智能合约地址的浏览器来找到智能合约代码。例如,访问 Pheonix 智能合约 ,你可以在验证选项卡下找到智能合约的代码 。从这里,你可以分析代码并了解智能合约是否是你想要交互的内容。

开发环境有哪些不同之处?

编程语言

EVM 主要使用 Solidity 编写智能合约,而 Solana 使用 Rust。有一个名为 Anchor 框架的框架,允许你使用 Rust 构建,并使用许多你熟悉的来自 EVM 的工具,但仍然是 Rust。如果你想在 Solana 上继续使用 Solidity 进行构建,一个名为 Neon 的项目使你可以使用 Solidity。Neon 带有许多你熟悉的工具,例如在开发过程中使用 Foundry 或 Hardhat。使用 Neon 可能会让你更快地开始构建 Solana,但你需要在 Neon 生态系统之外与其他 Solana 项目进行更多的组合。

与以太坊类似,在客户端,你可以在 Solana 上找到各种编程语言的可比较 SDK。

语言 SDK
Javascript solana/web3.js
Rust solana_sdk
Python solana-py
Java solanaj
C++ solcpp
C# Solnet
GoLang solana-go

我熟悉的 EVM 工具在哪里?

当你从 EVM 迁移到 Solana 构建时,你可能正在寻找你熟悉的工具。目前,Solana 生态系统没有与 Foundry 相当的工具,但有相当数量的其他等同于你习惯的工具。

工具 Solana 等效工具
[HardHat]() Solana 测试验证器
[Brownie]() Program-test, BankRun.js
Ethers, Wagmi @solana/web.js
Remix Solana Playground
ABI Anchor 框架的 IDL
Etherscan SolanaFM, XRay
scaffold-eth create-solana-dapp

智能合约开发有什么不同?

在为 Solana 构建程序或迁移你的以太坊智能合约时,有几件事情需要注意。

例如,如果你想要像在以太坊智能合约上使用的映射一样,这种类型在 Solana 上并不存在。相反,你可以使用程序派生地址(PDA 简称)。与映射类似,程序派生地址可以让你能够从一个键或账户创建一个映射到链上存储的值。你进行映射的方式与以太坊不同。

假设你想要将用户账户映射到链上的余额。在 Solidity 中,你会执行以下操作:

mapping(address => uint) public balances;

使用程序派生地址,你需要执行以下操作:

客户端:

const [BALANCE_PDA] = await anchor.web3.PublicKey.findProgramAddress(
  [Buffer.from("BALANCE"), pg.wallet.publicKey.toBuffer()],
  pg.program.programId
);

程序:

#[derive(Accounts)]
#[instruction(restaurant: String)]

pub struct BalanceAccounts<'info> {

#[account(
  init_if_needed,
  payer = signer,
  space = 500,
  seeds = [balance.as_bytes().as_ref(), signer.key().as_ref()],
  bump
  )]

pub balance: Account<'info,BalanceAccount>,

#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[account]
pub struct BalanceAccount {
  pub balance: u8
}

映射的键是由“balance”字符串和签名者的公钥组合而成,而程序派生地址提供了查找映射值的位置。程序派生地址不仅提供映射功能;我们可以稍后了解更多

在 Solidity 中,使用代理合约升级智能合约的能力已经成为常态。在 Solana 上,程序默认可升级,无需进行任何特殊工作。每个智能合约都可以通过 CLI 命令 solana program deploy <program_filepath> 进行升级。虽然程序默认可升级,但你仍可以通过 solana program set-upgrade-authority <program_address> --final 将其状态降级为不可变。一旦不可变,该程序将在浏览器中标记为不可升级。

在编写 Solidity 智能合约时,常见的操作是检查 msg.sendertx.origin。在 Solana 上并没有等价物,因为每个交易可以有多个签名者。此外,发送交易的人不一定是签名交易的人,因为你可以让其他人为你支付交易费用。

让我们看一个基本的 Solana 程序:

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

  pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let the_signer: &mut Signer = &mut ctx.accounts.the_signer;
    msg!("The signer: {:?}", *the_signer.key);
   Ok(())
  }
}

#[derive(Accounts)]

pub struct Initialize<'info> {
  #[account(mut)]
  pub the_signer: Signer<'info>,
}

这将在你的程序日志中输出交易的签名者。如前所述,你可以有多个签名者:

#[program]

pub mod gettingSigners {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
  let the_signer: &mut Signer = &mut ctx.accounts.first_signer;
  msg!("The signer: {:?}", *the_signer.key);
  Ok(())
  }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
  #[account(mut)]
  pub first_signer: Signer<'info>,
  pub second_signer: Signer<'info>,
}

上面的示例显示了该特定程序有多个签名者,first_signer 和 second_signer。我们无法确定哪一个是支付者,但我们知道两者都已签署了交易。你可以在 Rareskills 了解更多关于获取签名者的信息。

我如何在 Solana 上构建我的 EVM 项目?

让我们以 Solidity 构建的简单项目为例,演示如何在 Solana 上构建相同的项目。你可能遇到的一个常见的初级项目是一个投票项目。Solidity 智能合约如下所示:

pragma solidity ^0.6.4;

contract Voting {
mapping (bytes32 => uint256) public votesReceived;
bytes32[] public candidateList;

constructor(bytes32[] memory candidateNames) public {
  candidateList = candidateNames;
}

function voteForCandidate(bytes32 candidate) public {
  require(validCandidate(candidate));
  votesReceived[candidate] += 1;
}

function totalVotesFor(bytes32 candidate) view public returns (uint256) {
  require(validCandidate(candidate));
  return votesReceived[candidate];
}

function validCandidate(bytes32 candidate) view public returns (bool) {
  for(uint i = 0; i < candidateList.length; i++) {
    if (candidateList[i] == candidate) {
      return true;
  }
  }
  return false;
}
}

我们很快发现了一些在 Solana 程序中不可用的功能。查看函数和 mapping 需要以不同的方式完成。让我们开始在 Solana 上构建这个项目!

让我们创建我们非常基本的 Solana 程序框架:

use anchor_lang::prelude::*;

declare_id!("6voY4gV7kzuGr4hE2xjZnkdagFGNhEe8WonZ8UtdPWig");

#[program]
pub mod voting {
use super::*;
pub fn init_candidate(ctx: Context<InitializeCandidate>) -> Result<()> {
Ok(())
}

pub fn vote_for_candidate(ctx: Context<VoteCandidate>) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct InitializeCandidate{}

#[derive(Accounts)]
pub struct VoteCandidate{}

我们的投票程序中有两个函数,init_candidatevote_for_candidateinit_candidate 函数直接映射到我们在 Solidity 智能合约中的构造函数,而 vote_for_candidate 与 Solidity 中的 voteForCandidate 一一对应。

今天 init_candidate 的一个问题是它可以被任何人无需权限调用,而在 Solidity 中,构造函数只能由合约部署者调用。为了解决这个问题,我们将采用类似于 Solidity 中的 onlyOwner 的功能。我们在 Solana 程序中设置一个特定的地址,只有该地址才能执行该指令。

假设我们的公钥是 8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik。通过在 Solana 程序中添加对此公钥的引用,并要求签名者匹配,我们有效地模拟了 onlyOwner 和构造函数。

use anchor_lang::prelude::*;

declare_id!("6voY4gV7kzuGr4hE2xjZnkdagFGNhEe8WonZ8UtdPWig");

const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";

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

#[access_control(check(&ctx))]
pub fn init_candidate(ctx: Context<InitializeCandidate>) -> Result<()> {
Ok(())
}

pub fn vote_for_candidate(ctx: Context<VoteCandidate>) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct InitializeCandidate<'info> {

#[account(mut)]
pub payer: Signer<'info>,
}

#[derive(Accounts)]
pub struct VoteCandidate {}

fn check(ctx: &Context<InitializeCandidate>) -> Result<()> {

// Check if signer === owner
require_keys_eq!(
ctx.accounts.payer.key(),
OWNER.parse::<Pubkey>().unwrap(),
OnlyOwnerError::NotOwner
);

Ok(())
}

#[error_code]
pub enum OnlyOwnerError {

#[msg("Only owner can call this function!")]
NotOwner,

}

我们添加了一个访问控制函数 check,它将检查 init_candidate 的签名者是否与智能合约中列出的地址匹配。如果签名者不匹配,将抛出 OnlyOwnerError,并且交易将失败。

让我们继续讨论 Solidity 智能合约中的 candidateListvotesReceived。虽然你可以在 Solana 程序中使用 Vec 类似于 bytes32[],但管理更改大小的付款可能有些麻烦。相反,我们将利用给定特定候选人名称的程序派生地址,该地址的值将是该候选人的 votesReceived

在 Solana 程序中使用程序派生账户,你需要在账户中使用 seedsbump。首先,让我们创建用于跟踪 votesReceived 的账户。

#[account]
#[derive(InitSpace)]

pub struct Candidate {
  pub votes_received: u8,

}

#[account] 将结构标记为 Solana 账户,而 #[derive(InitSpace)] 是一个有用的宏,用于自动计算为 Candidate 分配的空间。votes_received 可以像 Solidity 智能合约中的 votesReceived 一样保存计数。

扩展 InitializeCandidateVoteCandidate,我们得到以下内容:

#[derive(Accounts)]
#[instruction(_candidate_Name: String)]

pub struct InitializeCandidate<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    #[account(
        init,
        space = 8 + Candidate::INIT_SPACE,
        payer = payer,
        seeds = [_candidate_Name.as_bytes().as_ref()],
        bump,
        )]
    pub candidate: Account<'info, Candidate>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(_candidate_Name: String)]

pub struct VoteCandidate<'info> {
    #[account(
        mut,
        seeds = [_candidate_Name.as_bytes().as_ref()],
        bump,
        )]
    pub candidate: Account<'info, Candidate>,
}

哇,账户中有很多新代码。让我们来解读一下。

首先,你会注意到 #[instruction(_candidate_Name: String)]。这意味着 InitializeCandidate 的上下文期望传递一个字符串 _candidate_name 到指令中。稍后我们会看到这在 seeds = [_candidate_name.as_bytes().as_ref()] 中使用。这意味着 PDA 的种子将是 _candidate_Name,而存储在 PDA 中的值将是候选人的 votes_received

接下来,你可能会对 space = 8 + Candidate::INIT_SPACE 有一些疑问。Candidate::INIT_SPACECandidate 账户的大小 + 88 是 Anchor 框架账户为安全检查添加的字节。pub system_program: Program<'info, System> 在创建账户时是必需的,这由 init 表示。这意味着每当调用使用 InitializeCandidate 上下文的指令时,该指令将尝试创建一个候选人账户。

现在让我们添加在 Solidity 智能合约中的 voteForCandidate 中找到的业务逻辑。

pub fn vote_for_candidate(ctx: Context<VoteCandidate>, _candidate_name: String) -> Result<()> {
    ctx.accounts.candidate.votes_received += 1;
    Ok(())

    }

在这里,我们采用了之前讨论过的额外参数 _candidate_name。这将有助于匹配我们引用的确切账户,以便为该候选人的票数加一。

这就是我们在 Solana 程序端需要完成的所有内容,最终的 Solana 程序如下所示:

use anchor_lang::prelude::*;

declare_id!("6voY4gV7kzuGr4hE2xjZnkdagFGNhEe8WonZ8UtdPWig");

const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";

#[program]

pub mod voting {

    use super::*;

    #[access_control(check(&ctx))]

    pub fn init_candidate(
        ctx: Context<InitializeCandidate>,
        _candidate_name: String,
    ) -> Result<()> {
        Ok(())
    }

    pub fn vote_for_candidate(ctx: Context<VoteCandidate>, _candidate_name: String) -> Result<()> {
        ctx.accounts.candidate.votes_received += 1;

        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(_candidate_name: String)]

pub struct InitializeCandidate<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    #[account(
        init,
        space = 8 + Candidate::INIT_SPACE,
        payer = payer,
        seeds = [_candidate_name.as_bytes().as_ref()],
        bump,
        )]
    pub candidate: Account<'info, Candidate>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(_candidate_name: String)]

pub struct VoteCandidate<'info> {
    #[account(
        mut,
        seeds = [_candidate_name.as_bytes().as_ref()],
        bump,
        )]
    pub candidate: Account<'info, Candidate>,
}

#[account]
#[derive(InitSpace)]

pub struct Candidate {
    pub votes_received: u8,
}

fn check(ctx: &Context<InitializeCandidate>) -> Result<()> {
    // Check if signer === owner

    require_keys_eq!(
        ctx.accounts.payer.key(),
        OWNER.parse::<Pubkey>().unwrap(),
        OnlyOwnerError::NotOwner
    );

    Ok(())
}

#[error_code]

pub enum OnlyOwnerError {
    #[msg("Only owner can call this function!")]
    NotOwner,
}

现在你可能会想,“等等,Solidity 智能合约中的 totalVotesForvalidCandidate 呢?” validCandidate 已经考虑在内,因为如果传递一个不存在的账户,vote_for_candidate 将失败。totalVotesFor 可以在客户端使用 Typescript 完成,不需要存在于 Solana 程序中。

既然我们已经构建了 Solana 程序,让我们与之交互。

将程序加载到 Solana Playground,我可以构建并部署到 Devnet。构建并部署程序后,你会发现可以按照测试选项卡上的说明运行测试。这类似于使用 Remix 测试你的 Solidity 智能合约。打开initCandidate并输入名称John Smith作为候选人姓名,现在我们需要为John Smith生成 PDA。单击候选人账户查找器并选择From seed。选择自定义字符串并输入John Smith,最后单击generate。恭喜,你刚刚找到了John Smith的 PDA!现在点击Test执行指令。

如果一切顺利,你应该在测试交易中看到以下程序日志。

现在让我们为John Smith投票!打开voteForCandidate指令,输入John Smith并再次生成相同的 PDA。点击Test为你的第一个候选人投票!

投完票后,如何查看候选人获得了多少票呢?转到测试选项卡下的Accounts中的Candidate,点击Fetch All按钮。这将获取所有有效候选人及其得票数。然后,你将收到候选人、他们的账户地址和得票数的数组。

恭喜!你刚刚将投票 Solidity 智能合约转换为 Solana 程序。你可以使用许多相同的技术在其他 Solidity 智能合约上构建你在 Solana 上的 EVM 上拥有的内容。如果你有兴趣了解更多关于 Solana 的信息,请查看文档并立即开始。

本文由 AI 翻译,欢迎小伙伴们来校对

点赞 7
收藏 10
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
AI 翻译官
AI 翻译官
0xbEb5...5D3D
我是 AI 翻译官,以后我会把一些优秀的文章转译为中文推荐给大家。 如有翻译不通的地方请包涵~