Solana 交易数据结构深入研究

尽管我是一个区块链老手,但我不得不承认我对此感到好奇。我终于有时间深入了解Solana的工作原理,并决定研究一笔交易是最好的起点,所以我开始了这一过程并记录了我的思考和分析。

Solana 交易数据结构深入研究
一键发币: SUI | SOL | BNB | ETH | BASE | ARB | OP | POLYGON | AVAX | FTM | OK

自2015年以来,我一直在以太坊上进行开发和构建,并且主要专注于基于EVM的区块链。因此,我对以太坊从上到下的工作原理有了相当不错的理解。我还开发了一个硬件钱包(Lattice),旨在增强在智能合约平台上进行交易的用户体验。直到最近,基于EVM的链几乎占据了这个市场的全部份额。

Solana在今年早些时候开始被更多人使用,于是我查看了他们的文档,意识到它与以太坊有多么不同。这是一个完全不同的系统,具有完全不同的权衡。尽管我是一个区块链老手,但我不得不承认我对此感到好奇。我终于有时间深入了解Solana的工作原理,并决定研究一笔交易是最好的起点,所以我开始了这一过程并记录了我的思考和分析。

1、设置钱包并铸造代币

在做任何事情之前,先设置一个测试网上的临时Sollet钱包。实际上,我克隆了钱包仓库并在本地运行,这样我可以获得一些描述性的打印输出来了解发生了什么,如果你有兴趣深入研究,也可以这样做。

一旦你设置了钱包并切换到测试网,请求空投然后铸造测试代币。这笔铸造交易就是我将在这里讨论的内容。

2、Solana交易的结构

我将首先描述通用Solana交易的结构,根据官方文档 ,基本上,一笔交易由几个部分组成:

  1. 签名 — 这是一系列ed25519曲线消息哈希签名的列表,其中“消息”由元数据和“指令”组成。与以太坊不同,Solana交易可以包含多个签名者(这将在后面更有意义),并且此签名字段需要所有签名者。任何授权状态更新的账户都必须签署该交易,类似于比特币中所有UTXO必须由其所有者签名,但不同之处在于这种关系不是1:1的——一个账户在一个交易中多次更新只需要签署一次。
  2. 元数据 — 消息中包含一些元数据,这些元数据没有在我的打印输出中出现,我在这里简要提及一下,因为它们在文档中有说明。消息有一个头部,其中包括3个uint8,描述有多少账户将签署有效载荷、有多少不会签署以及有多少是只读的。元数据还包含此交易中将引用/使用的账户列表和一个“最近”的区块哈希。我不确定这个哈希的确切规则是什么,但它不必是最近的区块,只需是一个最近的区块。多近才算“足够近”?我不知道。
  3. 指令 — 这是消息的核心(也是本文的核心)。当你在Solana上进行交易时,你是在向运行时提供一系列“指令”,每个指令都会对全局Solana系统中的状态进行不同的更新。一个指令包含三个主要信息:a) 使用的账户集以及每个账户是否为签名者和/或可写,b) 引用调用代码位置的程序ID,c) 包含一些数据的缓冲区,这些数据充当calldata。

“账户”本质上是Solana系统中具有关联地址的内存区域。“程序”是一种特殊类型的账户,包含静态的、可执行的代码。

旁注: 实际上有两种类型的“地址”,如果你来自以太坊,这可能会让你感到困惑。Solana使用 ed25519 ,它有32字节的公钥——这是一种“地址”(即未经过哈希处理)。如果你想创建一个能够从中签名的账户,你的地址需要是 ed25519 曲线上的一个点(即有效的公钥)。然而,你还可以创建一个“程序派生地址”(PDA),它必须 不在 曲线上,这意味着没有对应的私钥。你不能直接创建一个带有PDA的账户——相反,程序拥有的账户必须使用程序功能调用 System Program 。PDA通常用于程序需要内部管理账户以实现更复杂功能的情况(例如DeFi),我们在代币铸造交易中不会使用它们。

要在Solana系统上“安装”一个程序,你需要创建一个新的账户(带有一对公私钥),并将一堆编译后的字节码作为calldata发送。代码将部署到与你提供的公钥匹配的地址。Solana中有一些系统级程序,其中之一是BPF Loader(BPF代表“伯克利包过滤器”)——这是你用来创建新程序的程序,它将成为你新程序的“拥有者”。

因此,你可以将“程序”视为Solana系统中的一个内存区域,其中包含不可变的、无状态的可执行代码。一旦程序代码被部署,你将创建一个新账户并将其标记为“拥有者”——从现在开始我将其称为“程序拥有的账户”。

请注意,如果你使用一对公私钥创建一个账户并将一个程序指定为拥有者,你无法直接改变该账户的状态——你必须使用链接程序的API来做到这一点,类似于以太坊中的代理合约模式。明确地说,这意味着你生成的用于创建程序拥有账户的私钥可以在签署创建账户的交易后丢弃——一旦账户创建完成,这个密钥就无法再做任何事情。

3、示例:铸造代币

好了,背景介绍够多了。让我们来看看我们的交易。这是我运行的那笔交易 ,这是我的本地Sollet钱包JSON输出:

{  "instructions": [// instruction 1/5  
    {  
       "keys": [  
        {  
          "pubkey": {  
            "_bn": "63e8ee43a8583013ba6fef5c09181f26720d0621debe8c57eaf4ed87e704843b"  
          },  
          "isSigner": tru e,  
          "isWritable": true  
        },  
        {  
          "pubkey": {  
            "_bn": "f7fa7a4d4e8dda19a3afbec13a5fb4a540289b869b6a5a46162b7c3e3d9161ab"  
           },  
          "isSigner": true,  
          "isWritable": true  
        }  
      ],  
      "programId": {  
        "_bn": "00"  
      },  
      "data": {  
        "type" : "Buffer",  
        "data": [  
          0,   0,   0,   0,  96,  77,  22,   0,   0,  0,   0,  
          0,  82,   0,   0,   0,   0,   0,   0,   0,  6, 221,  
          246, 225 , 215, 101, 161, 147, 217, 203, 225, 70, 206,  
          235, 121, 172,  28, 180, 133, 237,  95,  91, 55, 145,  
          58, 140, 245, 133, 126, 255,   0, 169  
        ]  
       }  
    }, // instruction 2/5  
    {  
      "keys": [  
        {  
          "pubkey": {  
            "_bn": "f7fa7a4d4e8dda19a3afbec13a5fb4a540289b869b6a5a46162b7c3e3d9161ab "  
          },  
          "isSigner": false,  
          "isWritable": true  
        },  
        {  
          "pubkey": {  
            "_bn": "06a7d517192c5c51218cc94c3d4af1 7f58daee089ba1fd44e3dbd98a00000000"  
          },  
          "isSigner": false,  
          "isWritable": false  
        }  
      ],  
      "programId": {  
        "_bn": "06 ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9"  
      },  
      "data": {  
        "type": "Buffer",  
        "data": [  
          0,  2,  99, 232, 238,  67,  168, 88,  48, 19, 186, 111,  
          239, 92,   9,  24,  31,  38, 114, 13,   6, 33, 222, 190,  
          140, 87, 234, 244, 237, 135, 231,  4, 132, 59,   0,   0,  
          0,   0,   0,   0,   0,   0,   0,  0,   0,  0,   0,   0,  
          0,  0,   0,   0,   0,   0,   0,  0,   0,  0,   0,   0,  
          0,  0,   0,   0,   0,   0,   0  
        ]  
       }  
    },     // instruction 3/5  
    {  
      "keys": [  
        {  
          "pubkey": {  
            "_bn": "63e8ee43a8583013ba6fef5c09181f26720d0621debe8c57eaf4ed87e70 4843b"  
          },  
          "isSigner": true,  
          "isWritable": true  
        },  
        {  
          "pubkey": {  
            "_bn": "f2560320922ee24d0086e5769e 8b3cdf261c646f4f17184532ec7380220b980c"  
          },  
          "isSigner": true,  
          "isWritable": true  
        }  
      ],  
      "programId": {  
        "_bn": " 00"  
      },  
      "data": {  
        "type": "Buffer",  
        "data": [  
          0,   0,   0,   0, 240,  29,  31,   0,   0,  0,   0,  
          0, 165,   0,   0,   0,    0,   0,   0,   0,  6, 221,  
          246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206,  
          235, 121, 172,  28, 180, 133, 237,  95,  91, 55, 145,  
          58, 140 , 245, 133, 126, 255,   0, 169  
        ]  
      }  
    }, // instruction 4/5  
    {  
      "keys": [  
        {  
          "pubkey": {  
            "_bn": "f2560320922ee24 d0086e5769e8b3cdf261c646f4f17184532ec7380220b980c"  
          },  
          "isSigner": false,  
          "isWritable": true  
        },  
        {  
          "pubkey": {  
             "_bn": "f7fa7a4d4e8dda19a3afbec13a5fb4a540289b869b6a5a46162b7c3e3d9161ab"  
          },  
          "isSigner": false,  
          "isWritable": false  
        },  
         {  
          "pubkey": {  
            "_bn": "63e8ee43a8583013ba6fef5c09181f26720d0621debe8c57eaf4ed87e704843b"  
          },  
          "isSigner": false,  
          "i sWritable": false  
        },  
        {  
          "pubkey": {  
            "_bn": "06a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a00000000"  
          },  
           "isSigner": false,  
          "isWritable": false  
        }  
      ],  
      "programId": {  
        "_bn": "06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a 9"  
      },  
      "data": {  
        "type": "Buffer",  
        "data": [  
          1  
        ]  
      }  
    }, // instruction 5/5  
    {  
      "keys": [  
         {  
          "pubkey": {  
            "_bn": "f7fa7a4d4e8dda19a3afbec13a5fb4a540289b869b6a5a46162b7c3e3d9161ab"  
          },  
          "isSigner": false,  
          "isWrita ble": true  
        },  
        {  
          "pubkey": {  
            "_bn": "f2560320922ee24d0086e5769e8b3cdf261c646f4f17184532ec7380220b980c"  
          },  
          "isSi gner": false,  
          "isWritable": true  
        },  
        {  
          "pubkey": {  
            "_bn": "63e8ee43a8583013ba6fef5c09181f26720d0621debe8c57eaf4ed87e704843b "  
          },  
          "isSigner": true,  
          "isWritable": false  
        }  
      ],  
      "programId": {  
        "_bn": "06ddf6e1d765a193d9cbe146ceeb79ac1cb48 5ed5f5b37913a8cf5857eff00a9"  
      },  
      "data": {  
        "type": "Buffer",  
        "data": [  
           7, 232, 3, 0, 0, 0, 0, 0, 0  
        ]  
      }  
    }  
   ],  "signatures": [  
    {  
      "signature": {  
        "type": "Buffer",  
        "data": [  
          20,  
          ... // 省略,因为签名数据不重要  
           5  
        ]  
      },  
      "publicKey": {  
        "_bn": "63e8ee43a8583013ba6fef5c09181f26720d0621debe8c57eaf4ed87e704843b"  
      }  
    },  
    {  
      "sign ature": {  
        "type": "Buffer",  
        "data": [  
          0,  
          ...  
          15  
        ]  
      },  
      "publicKey": {  
        "_bn": "f7fa7a4d4e8d da19a3afbec13a5fb4a540289b869b6a5a46162b7c3e3d9161ab"  
      }  
    },  
    {  
      "signature": {  
        "type": "Buffer",  
        "data": [  
          230,  
           ...  
          8  
        ]  
      },  
      "publicKey": {  
        "_bn": "f2560320922ee24d0086e5769e8b3cdf261c646f4f17184532ec7380220b980c"  
      }  
    }  
  ]  
}

这并不是对上述交易结构的完美映射,但它包含了我们需要的所有信息。需要注意的一件重要事情是,所有的 _bn 条目都是地址的十六进制字符串表示形式。地址通常在像Solscan这样的东西中编码/显示为base58字符串,从现在开始我将使用base58。还有一个 recentBlockhash,但我已经省略了它,因为它与这笔交易无关,而且我已经讨论过这个概念。

现在让我们逐一查看每个部分。

— 指令 #1/5 —

第一条指令创建了一个由 Token Program 拥有的程序派生账户。该指令包含两个 keys

  • 7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp: 这是一个签名者,并且需要是可写的,以便从我的余额中扣除SOL。
  • Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL: 签名者和可写。我们还不知道这个账户的故事。
旁注: SOL的原子单位被称为“lamports”,这让我非常懊恼,因为基本上每个加密项目都将原子单位命名为随机密码学家的名字,而不是,你知道的,基础单位的转换。我们没有毫SOL或纳SOL。享受你的lamports吧。

programId 设置为 00,这意味着我们正在调用 System Program,这是唯一能够在系统中创建新账户的Solana程序。

data 是这个缓冲区:

[  
    0,   0,   0,   0,  96,  77,  22,    0,   0,  0,   0,  
    0,  82,   0,   0,   0,   0,   0,   0,   0,  6, 221,  
  246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206,  
  235, 121, 172,  28, 180, 133, 237,  95,  91 , 55, 145,  
   58, 140, 245, 133, 126, 255,   0, 169  
]

前四个字节都是 0。程序函数通过枚举进行索引,因此每条指令的calldata的第一个参数引用给定程序API中调用的函数。由于第一个 u320,这意味着我们正在调用 System Programfunction 0,即 CreateAccount

旁注: Solana系统级程序使用 u32作为枚举索引类型,但此参数的大小取决于程序作者。我们将要看的 Token Program 使用 u8 枚举索引类型,所以我们只需要检查第一个字节就可以确定我们正在调用哪个函数。

lamports

我们的calldata中的第一个字段是一个 u64 类型,描述要转移到新账户的lamports数量:

[96,  77,  22,   0,   0,  0,   0,   0]

哇,这是个巨大的数字……还是不是呢?哈哈,结果发现Solana使用小端数字编码。在用C语言实现了以太坊的东西之后,我不得不说我对Solana选择使用LE感到非常高兴。如果你懂,你就懂。

无论如何,在小端中,这个数字是:

0x0000000000164d60  
-> 146 1600 lamports

这是什么?这是支付“租金”以保持程序在Solana系统中存活。我不确定具体的机制,但 这里 是关于租金的官方文档。关键似乎是租金成本与storage大小成正比。你也可以提前支付两年的租金,这样你的程序将永远存活下去,这就是这里发生的事情。我不知道他们是如何做出这个决定的,但好吧。

space

下一个字段是另一个 u64,指示为此程序分配多少字节的内存:

[82,   0,   0,   0,   0,   0,   0,   0]

所以…… 82 字节?看起来很小。这是因为我们实际分配的内存只是指向 Token Program 的指针、一个小数和可能的另一件事。重要的是,这个程序将不会持有用户的余额——相反,用户会创建自己的程序来与此程序交互。这很令人困惑。我们稍后会讲到。

owner

这次 System Program Create Account 调用的最后一个字段是“拥有者”,在这种情况下将是 Token Program。这是一个非常常见的程序,部署到每个Solana网络(即主网和测试网),因为每个人都想引用它来创建自己的代币。人们喜欢代币。

这里是那个字段:

[  
    6, 221, 246, 225, 215, 101, 161,  
  147, 217, 203, 225,  70, 206, 235,  
  121, 172,  28, 18 0, 133, 237,  95,  
   91,  55, 145,  58, 140, 245, 133,  
  126, 255,   0, 169  
]

用base58编码后是:

TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

这是我在使用的测试网上 Token Program 的地址(链接)。

旁注: 我想向那些花时间挖掘这些Solana地址的人致敬。我喜欢他们写了人类可读的前缀,比如 Token BPFLoader。有趣的东西。这也提出了Solana和以太坊之间的一个重要区别。在以太坊中,你会得到一个基于你的地址和nonce在部署时确定的确定性合约地址——而在Solana中,你可以不断创建新的密钥对,直到得到你喜欢的公钥,因为在Solana系统中实例化时你会引用账户地址(公钥)。这就是“挖掘”地址(公钥)的意思。

指令 #1 总结:

我们通过 System Program 调用了 Create Account,使用了我的钱包账户 (7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp) —— 这创建了一个新的程序拥有账户,地址为 Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL,现在持有 1461600 lamports 并且由 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA(即此测试网上的 Token Program)拥有。该账户被标记为签名者,因为它需要签署创建自己的内存区域,稍后将用于存储状态。不过,在此交易之后,私钥可以丢弃,因为该账户由 Token Program 拥有,因此无法修改自己的状态。

— 指令 #2/5 —

在此指令中,我们直接调用 Token Program

programId = 06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9base58 -> Tok enkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

我们的两个 keys 是:

Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL (!signer, writable)  
SysvarRent11111111111111111111111111111111 1  (!signer, !writable)

看看 InitializeMint 中的注释:

/// Accounts expected by this instruction:      
///   0. `[writable]` The mint to initia lize.      
///   1. `[]` Rent sysvar

第一个地址是我们将“铸造”代币的账户——这是我们上一条指令中创建的账户。这个概念将在后面变得更加清晰,但基本上这个账户是(某种意义上)Token Program 的一个实例——它描述了你正在创建的代币。它将用于“铸造”代币,这些代币将被发送到另一个账户(很快就会讲到)。这个铸币账户在此指令中需要是可写的,因为它的状态需要更新。

第二个地址是“租金sysvar”——这似乎是为了Solana运行时中的某些低级事情而必需的,我不知道为什么它会出现在这里,也许是为了允许支付租金,但这似乎并不重要,所以我们继续。

我们可以查看 Token Program API来弄清楚calldata是如何构造的。Solscan条目表明这是在调用 InitializeMint,正如第一个字节所示,它是 0,对应于 Token Program 指令API枚举中的第0个函数索引(如前所述,这是一个 u8 而不是 u32,因为这是 Token Program 作者想要的)。

Calldata:

[  
    0,  2,  99, 232, 238,  67, 168, 88,  48, 19, 186, 111,  
  239, 92,   9,  24,  31,  38, 114, 13,   6, 33, 222, 190,  
  140, 87, 234, 244 , 237, 135, 231,  4, 132, 59,   0,   0,  
    0,  0,   0,   0,   0,   0,   0,  0,   0,  0,   0,   0,  
    0,  0,   0,   0,   0,   0,   0,  0,   0,  0,   0,   0,  
    0,  0,   0,    0,   0,   0,   0  
]

第一个 0 是程序API枚举集中函数的索引。在这种情况下,它是 InitializeMint(看它是如何成为第一个枚举索引的:这里)。

第二个字节是 2 ,这是一个 u8,表示我们代币的小数位数。请注意,小数精度必须有一个相当小的限制,因为[代币转账金额](https://github.com/sbnair/Solana_Token/blob/main/token/program/src/ins truction.rs#L107) 是 u64 类型,但我不知道这个限制是多少或如何强制执行——这些都是特定于程序的东西。

接下来的32字节是 mint_authority ,这只是创建此初始铸币的账户。这里是我的钱包账户 7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp

最后32字节为空,对应于一个可选的 freeze_authority ,我假设这是可以冻结此代币转账的账户。但我们代币没有!我们的代币是无权限的

指令 #2 总结:

我们从 Token Program 调用了 InitializeMint,让它知道我们将从哪里铸造代币。我们在本指令的 keys 中引用了外部程序:我们的铸币账户(在指令 #1 中创建)和 SysvarRent。再次强调,我不知道后者在这里做什么。

我们让 Token Program 知道我们的代币将有 2 个小数位,并且代币铸造的接收账户将由我的“authority”账户拥有,也就是我的SOL钱包账户。我们还让它知道没有账户可以冻结此代币实例。

— 指令 #3/5 —

继续……我们现在将使用 System Program 创建另一个账户。看看我们的 keys,我们看到第一个和之前一样(我的 7j1N.. 钱包账户),而第二个是新的:

HJyjV883iDC3C3vBkqSXiRqCrmFWsk9ER3MFc3KqgF8f

快速浏览一下calldata:

前四个字节再次为 0。我们已经讨论过这个了。我们在调用 CreateAccount(函数索引 0)。

  • lamports2039280
  • space165 字节。
  • owner 再次是 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

好的,所以我们创建了另一个由 Token Program 拥有的账户,拥有165字节的存储空间而不是82字节。如果你在想为什么这两个账户的 lamport/space 比率不同 (2039280 / 165 = 12359, 1461600 / 82 = 17824)……抱歉,我无法给你答案。我想它们大致是正确的?

指令 #3 总结

基本上这与指令 #1 相同。我们在 HJyjV883iDC3C3vBkqSXiRqCrmFWsk9ER3MFc3KqgF8f 创建了另一个账户,具有更多的存储空间和相同的拥有者。这展示了不同的程序拥有账户如何与程序代码的不同部分交互(并更新状态),所以在Solana中,程序:程序拥有账户 的关系是 1:多

— 指令 #4/5 —

我们现在要对 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 进行另一个指令调用。这次我们的calldata要短得多:

[ 1 ]

这意味着我们正在调用索引为1的函数,该函数必须没有参数。这个函数被称为 InitializeAccount (参考)。

由于没有真正的calldata,这条指令的本质只是传递了哪些账户。让我们来看看这条指令的 keys

HJyjV883iDC3C3vBkqSXiRqCrmFWsk9ER3MFc3KqgF8f (!signer, writable)  
Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL (!signer, !writable)  
7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp (!signer, !writable)  
SysvarRent111111111111111111111111111111111 (!signer, !writable)

我会直接引用 Token Program 文档:

///   0. `[writable]`  The account to initialize.    
///   1. `[]` The mint this account will be associated with.    
///   2. `[]` The new account's owner/multisignature.    
///   3. `[]` Rent sysvar

我们需要将一些新术语与这些账户关联起来:

  • HJyjV883iDC3C3vBkqSXiRqCrmFWsk9ER3MFc3KqgF8f 是我们创建的第二个账户(指令3)。这是我们将初始化其状态的代币账户。它需要是可写的才能对其状态数据进行操作。这是将接收铸造代币的账户。
  • Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL 是我们创建的第一个账户(指令1)。这是“代币铸币”账户,持有有关此代币实例的信息。我们已经在指令2中设置了该账户的状态。
  • 7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp 是该持有账户的所有者。现在最好的理解方式是将其视为我的SOL账户。它可以持有SOL并进行签名,但不能持有代币。我需要一个单独的代币持有账户,这就是我们在这里初始化的账户。
  • 租金 sysvar 账户再次出现。我仍然不知道为什么,但我想答案在于Solana系统的内部机制,我现在不会担心这个问题。

指令 #4 总结

我们基本上只是使用 Token Program 来定义我们将铸造代币到的账户,并引用了铸币账户,这是一个具有不同状态数据结构的不同账户。此时,我们已经准备好铸造!

— 指令 #5/5 —

在最后一条指令中,我们将最后一次调用 Token Program。我们的calldata是:

[7, 232, 3, 0, 0, 0, 0, 0, 0]

我们正在调用第7个函数,即 MintTo (参考)。这里有一个 u64 参数叫做 amount——非常直观。我们将铸造1000个代币单位,由于有2位小数,我们实际上铸造了10个完整的代币。

参考代码注释:

///   * 单一授权      
///   0. `[writable]` 铸币账户。       
///   1. `[writable]` 将铸造代币到的账户。      
///   2. `[signer]` 铸币的铸造授权。

指令 keys

Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL (!signer, writable)  
HJyjV883iDC3C3vBkqSXiRqCrmFWsk9ER3MFc3KqgF8f (!signer, writable)  
7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp (signer, !writable)

我们正在从所有者账户 (7j1N..) 签署此转账。我们从“代币铸币”账户 Hh1E... 铸造代币,并将这些代币发送到我们的新“代币持有”账户 HJyj...。这两者都需要在此指令中标记为可写,因为它们的状态数据正在被修改。

指令 #5 总结

我们终于做了一件事!我们从铸币账户铸造了1000个代币单位,并将它们转移到了我们的代币持有账户。此时,我们可以使用我们的SOL账户作为签名者,因为我们已经在程序空间中将其定义为该持有账户的所有者(指令#4),从而花费这些代币。

旁注: 尽管特定程序代码超出了本练习的范围,但需要注意的是,铸币所有者账户还需要在单独的指令/交易中移除自身作为授权,否则它可以继续铸造代币 (credit)。

— 签名者 —

我不会详细讨论这一点,但基本上此时你会获取这组指令、元数据、签名密钥等,并序列化为一个“消息”。然后你会对该消息进行哈希处理,并从所有必要的账户进行签名(记住,当你创建任何账户时,你都拥有其私钥)。这组签名密钥可以通过找到至少在一个指令中标记为 isSigner=true 的所有唯一账户来确定。对于这个例子,资助者(我的SOL账户)、代币铸币和代币持有账户都是签名者。

上述JSON有效载荷中的 signatures 部分包含与三个不同地址相关的三个签名:

63e8ee43a8583013ba6fef5c09181f26720d0621debe8c57eaf4ed87e704843b  
f7fa7a4d4e8dda19a3afbec13a5fb4a540289b869b6a5a46162b7c3e3d9161ab  
f2560320922ee24d0086e5769e8b3cdf261c646f4f17184532ec7380220b980c

以base58编码,分别是:

7j1NJDkeg35gtRt4nco9fN4wak6M6rD16okMYgvLJeHp  
Hh1E4LwRxAiJdLV4gzKX3BpdbNXJ2AYVdv8UDrfTZAbL  
HJyjV883iDC3C3vBkqSXiRqCrmFWsk9ER3MFc3KqgF8f

你现在应该认出所有这些了。如果你遍历JSON对象,你应该确信这是在至少一个指令中标记为 isSigner=true 的完整账户集。此外,如果你打开 Solscan记录,你会看到相同的地址。 ?

4、结束语

如果你看到了这里,我希望这意味着你发现这有些用处,并且比之前更了解Solana。太棒了,我也是。

以下是我从这次练习中学到的一些无结构的学习要点:

  • “钱包”账户实际上不能直接做太多事情;它们可以持有SOL,与系统级程序(例如 System Program)交互,并充当程序拥有账户的授权(在程序空间中链接)。在这个例子中,我们创建了一个代币持有账户,Token Program 认为它是由我的SOL账户“拥有”的(指令#4)——这就是我所说的“授权”,这种链接只存在于程序空间中,即不在Solana系统级别。
  • 你必须支付租金来填充账户的数据,尽管确切的过程在交易级别并未真正暴露。租金与账户占用的 storage 数量成比例。如果你提前支付任意金额(两年的租金),你的程序将永远存在。否则,当它无法再支付租金时就会消失。
  • 指令可以打包在一个原子交易中,并且不需要相互关联。如果我们愿意,我们也可以加入一个指令来将SOL Transfer 到一个不相关的账户。
  • 程序代码功能通过枚举索引。每个指令都有自己的calldata缓冲区,其第一个参数始终是一个对应于要调用的功能索引的无符号整数。奇怪的是,这个整数可以是程序编写者想要的任何大小(u8-u64)。
  • Calldata 不需要像以太坊那样进行ABI编码,Solana不使用256位字,因此有效载荷通常会相当短。
  • Solana的执行环境可能比EVM更高效,但在我看来,真正的性能优势来自于你不需要在交易中引用最新的交易哈希——你只需要一个最近的哈希。这意味着你可以并行化不影响相同状态的交易,结果似乎很常见的是将状态拆分到多个账户中,这些账户都由同一个程序拥有。

总体而言,Solana的架构与以太坊有很大不同。它确实更令人困惑,但我可以看到它的优势。我很高兴完成了这次练习,我认为这是一个巧妙的系统。我希望有机会了解更多关于它的内容!


原文链接:Solana Transactions in Depth

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。
通过 NowPayments 打赏