NEAR (Blockchain Platform)

Table of Contents

1. 简介

NEAR 是一个采用 Sharding 方案的 PoS 区块链。

1.1. 原生币单位

NEAR 中的原生币的最小单位被称为 yoctoNEAR,它和 NEAR 的关系为: 1NEAR=1000000000000000000000000yoctoNEAR=1024yoctoNEAR

2. 帐户模型

NEAR 中有两个类型的 Accounts

  1. Named Account,它是 human readable 的形式,比如主网上的帐户 alice.near,测试网上的帐户 example.testnet
  2. Implict Account,它是 ED25519 公钥的 16 进制编码,比如 98793cd91a3f870fb126f66285808c7e094afcfc4eda8a970f6648cdf0dbd6de

通过 near create-account 可以创建 Named accounts。但如果你在主网上操作,则需要提前准备另一个有 NEAR 币的帐户来创建这个新的 Named Account。我们可通过 near generate-key 创建 Implict Account,它不需要消耗 NEAR 币。

2.1. 两种类型的 Access Key

每个帐户可以绑定多个 Access Key,通过 RPC view_access_key_list 查看帐户所绑定的 Access Key,具体可参考节 4.3

NEAR 中有两种类型的 Access Key:

  1. Full-Access Key: 对帐户有完全的控制权,不能把它的私钥共享给别人;
  2. Function-Call Key: 可配置使用这个 Key 可以访问哪个合约的哪个方法。往往把它的私钥共享给别人,这样别人就可以代表用户调用指定合约的指定方法了。

注意: Function-Call Key 有个限制:在使用 Function-Call Key 调用合约方法时,不能附带任何 NEAR。 如果要求某个合约方法必须使用 Full-Access Key 签名后才能访问,则可以在合约方法要求用户附带一定数量的 NEAR,比如最少的 1 yoctoNEAR。参考节 6.2

2.1.1. Function-Call Key 场景

Function-Call Keys 允许应用程序代表用户签署交易,这可以提升用户体验。

考虑下面场景:您正在开发一款在智能合约上记录用户得分的游戏。

  1. 在其他区块链上,每次游戏需要更新分数时,您都必须请求用户进行交易签名,这是种用户体验很不好。
  2. 在 NEAR 链上,您可以请求用户为游戏合约生成 Function-Call Key 并与游戏共享。这样,游戏就可以以用户的名义签署交易,从而消除游戏中断。共享此密钥对于用户来说是安全的,因为即使有人窃取它,他们也只能调用更新分数的方法,而不能调用其他方法。

3. 交易

3.1. 交易结构

每个 Tx 由下面六个部分组成:

  1. signerId:签名帐户。
  2. signerPublicKey:签名所使用的公钥。一个帐户可以绑定多个公钥,所以需要指定使用帐户的哪一个对应私钥进行签名。
  3. nonceForPublicKey:公钥的 nonce 值,一个帐户可以绑定多个公钥,nonce 值和公钥绑定,而不是和帐户绑定。通过 RPC view_access_key_list 可以查询公钥 nonce 值。
  4. receiverId:接收帐户。
  5. blockHash:每个 Tx 都需要引用一个近期的区块 Hash,以验证这个 Tx 是最近创建的。
  6. actions:每个 Tx 可以包含一个或者多个 Action(s),参考节 3.1.1

参考:
https://docs.near.org/integrator/create-transactions
https://github.com/near/near-api-js/blob/f28796267327fc6905a8c6a7051ff37aaa7bbd06/packages/transactions/src/schema.ts#L224

3.1.1. Actions

Tx 的具体动作由 Action 指定,目前有 9 个 Actions,它们分别为 CreateAccount/DeployContract/FunctionCall/Transfer/Stake/AddKey/DeleteKey/DeleteAccount/Delegate。比如,对于普通的转帐来说使用的 Action 是 Transfer。

3.2. Receipts

NEAR 支持 Sharding,这给系统带来了一些复杂性。比如 alice.near 给 bob.near 转账时,他们的帐户信息可能不在一个 Sharding 中,这就涉及到跨 Sharding 之间的通信。

NEAR 引入了 Receipts 的概念,我们 可以简单地认为 Receipts 是在不同 Sharding 之间传递信息的内部交易。

You can think of the Receipt as an internal transaction that exists to pass information across shards.

1 是 Receipt 示意图。

参考:https://docs.near.org/concepts/data-flow/token-transfer-flow

3.3. 手续费

在每笔交易中,NEAR 网络都会收取少量的 Gas 费用。NEAR 的 Gas 机制除了用于防止攻击者发送无用交易之外,还有其它特点:

  1. 智能合约开发者会获得用户调用合约时所消耗 Gas 的 30%,从而激励开发者;
  2. 每个交易限制 300 Tgas(大约 300ms 的计算时间)。

手续费计算公式:

Fee = Gas Units * Gas Price

其中 Gas Units 是 Gas 数量,而 Gas Price 就是每个 Gas Unit 的 yoctoNEAR 价格。

NEAR Tx 的手续费,并没有体现在 Tx 中,而是由节点直接扣除,所以不需要用户指定手续费。目前,用户也无法指定额外小费来加速 Tx 的上链过程。

参考:https://docs.near.org/concepts/protocol/gas

3.3.1. Gas Units

大部分 NEAR 中的 Action 会消耗固定数量的 Gas Units,比如 Action CreateAccount/Transfer 等都是固定的 Gas Units。 具体消耗的 Gas Units 值是在 NEAR 节点的配置文件中设置的,参考:https://github.com/near/nearcore/blob/master/core/parameters/res/runtime_configs/parameters.yaml

当然对于 Action DeployContract 来说,它的 Gas Units 会和合约的大小有关系;对于 Action FunctionCall 来说,它的 Gas Units 和具体调用的合约有关系。要估计 Action FunctionCall 的 Gas Units,可以参考:https://docs.near.org/concepts/protocol/gas#estimating-costs-for-a-call

Gas Units 一般使用 Tgas 表示,它是换算关系为:1 Tgas = 1000000000000 Gas Units = 1012 Gas Units

3.3.2. Gas Price

Gas Price 就是每个 Gas Unit 的 yoctoNEAR 价格。通过 RPC gas-price 可以查询指定区块,或者最新区块的 Gas Price。目前,每个 Gas Unit 消耗 100000000 yoctoNEAR。

4. 帐户基本操作(命令行演示)

4.1. 安装 NEAR CLI

下面演示一下如何通过 NEAR CLI 进行帐户的基本操作,如创建、转账、查看余额等。

下面是安装 NEAR CLI 的命令:

$ npm install -g near-cli                                               # 安装 NEAR CLI

4.2. 创建帐户

使用 near create-account <your-account-name> 可以创建你指定的帐户。比如在测试网中创建帐户 my-first.testnet(测试网中的帐户一般以 .testnet 结尾,主网中的帐户一般以 .near 结尾):

$ near create-account my-first.testnet --useFaucet --networkId testnet  # 创建测试网的帐户 my-first.testnet,并从内置 faucet 中领取测试币
Storing credentials for account: my-first.testnet (network: testnet)
Saving key to '~/.near-credentials/testnet/my-first.testnet.json'

我们可以看看密钥文件的具体内容:

$ cat ~/.near-credentials/testnet/my-first.testnet.json                 # 查看保存的密钥(ed25519 公钥和私钥采用 base58 编码)
{"account_id":"my-first.testnet","public_key":"ed25519:7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1","private_key":"ed25519:4ZHyGAY8XEr3Z3JxBTZsp17b8HHhjnC5S3T82t3Kgr2da3UCg58CsovZk6g5iiiVp2gyP8xDs9WX5TFkf2kJtQfb"}

可见,密钥文件是个 JSON 文件,保存了三个信息:account_id/public_key/private_key。其中 public_key 是 ed25519 公钥(32 字节)的 base58 编码;而 private_key 是 ed25519 私钥(32 字节)和公钥(32 字节)concatenate 后组成的数据的 base58 编码。

4.3. 查看帐户绑定的公钥

在 NEAR 中一个帐户可以绑定多个 Key,使用 near list-keys <your-account-name> 可以查看帐户所绑定的 Key(即绑定哪个 ed25519 公钥),比如:

$ near list-keys my-first.testnet --networkId testnet
Keys for account my-first.testnet
[
  {
    access_key: { nonce: <BN: 91d00e4aacc0>, permission: 'FullAccess' },
    public_key: 'ed25519:7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1'
  }
]

我们也可以使用 RPC view_access_key_list 查看帐户所绑定的 Key,比如:

$ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0",
  "id": "dontcare",
  "method": "query",
  "params": {
    "request_type": "view_access_key_list",
    "finality": "final",
    "account_id": "my-first.testnet"
  }
}' https://rpc.testnet.near.org                                 # 使用 RPC view_access_key_list 查看帐户所绑定的 Key
{
    "jsonrpc": "2.0",
    "result": {
        "block_hash": "EXTyRjyPrKH1rgPUk3G9pYHiAN1iaGnrZHwrxtt4vX2Z",
        "block_height": 160327217,
        "keys": [
            {
                "access_key": {
                    "nonce": 160322779000000,
                    "permission": "FullAccess"
                },
                "public_key": "ed25519:7fkqm12NvLXSJzF4nxmyPYTmstQ6xNuZ2Pi5UpRAooS1"
            }
        ]
    },
    "id": "dontcare"
}

4.4. 测试转帐

在测试转账前,我们先创建另一个帐户,以作为接收帐户。这个接收帐户我们将使用 Implicit 帐户(帐户名就是 ed25519 公钥对应的 hex string):

$ near generate-key --saveImplicit --networkId testnet                  # 创建 Implicit 帐户(帐户名就是 ed25519 公钥对应的 hex string)
Seed phrase: bring crime prevent mention guess capable image box flight ill elite judge
Key pair: {"publicKey":"ed25519:DPbWgbUVxRogEekpPkEJLmkjjgQ1KZYMFP4H4GU1BfCZ","secretKey":"ed25519:5RhWFMGJnxupgWZTXqPPt9wJmwpJ5FMUWPecR2ePYJg4r74xjNXVFr4YjAAtNkoD1CrMmztSW81t1d23nnoz2zVK"}
Implicit account: b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6
Storing credentials for account: b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 (network: testnet)
Saving key to '~/.near-credentials/testnet/b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6.json'
$ cat ~/.near-credentials/testnet/b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6.json
{"account_id":"b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6","public_key":"ed25519:DPbWgbUVxRogEekpPkEJLmkjjgQ1KZYMFP4H4GU1BfCZ","private_key":"ed25519:5RhWFMGJnxupgWZTXqPPt9wJmwpJ5FMUWPecR2ePYJg4r74xjNXVFr4YjAAtNkoD1CrMmztSW81t1d23nnoz2zVK"}

使用 near send-near 可以往另一个地址转账,如从 my-first.testnet 往 b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 转移 1.234 NEAR:

$ near send-near my-first.testnet b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 1.234
Sending 1.234 NEAR to b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6 from my-first.testnet
Transaction Id H1xq6hTFdCKYjkUsrH4fp9yyejdxARj2sxMoF5sQ1AFh
Open the explorer for more info: https://testnet.nearblocks.io/txns/H1xq6hTFdCKYjkUsrH4fp9yyejdxARj2sxMoF5sQ1AFh

4.5. 查看余额

使用 near state 可以查看某帐户的余额,如:

$ near state b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6
Account b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6
{
  amount: '1234000000000000000000000',
  block_hash: 'BJXB9H5oxKptvie5dZccUbcAYXjP5cGPgWF6qH2KNJK2',
  block_height: 160329036,
  code_hash: '11111111111111111111111111111111',
  locked: '0',
  storage_paid_at: 0,
  storage_usage: 182,
  formattedAmount: '1.234'
}

我们也可以使用 RPC view_account 查看帐户的余额,比如:

$ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0",
  "id": "dontcare",
  "method": "query",
  "params": {
    "request_type": "view_account",
    "finality": "final",
    "account_id": "b8160b22ba63df2115da0a7c26679069a34c66329c33ed73ec0eb7240f4706a6"
  }
}' https://rpc.testnet.near.org                                 # 使用 RPC view_account 查看帐户余额
{
    "jsonrpc": "2.0",
    "result": {
        "amount": "1234000000000000000000000",
        "block_hash": "8LxbGanAuhHv53YRvxMyQBTEEDAD7wSBpYv3Sn9zeejU",
        "block_height": 160329066,
        "code_hash": "11111111111111111111111111111111",
        "locked": "0",
        "storage_paid_at": 0,
        "storage_usage": 182
    },
    "id": "dontcare"
}

5. 智能合约

NEAR 支持使用 Javascript 和 Rust 开发智能合约,它们会被编译为 WebAssembly 代码在 NEAR 平台上执行。这里不详细介绍。

6. 代币(NEP141)

NEAR 中 Fungible Token 的标准为 NEP141,类似于 Ethereum 中的 ERC20 标准。

NEP141 的主要接口有:

  1. ft_transfer(receiver_id: string, amount: string, memo: string | null) 转移代币给别人;
  2. ft_transfer_call(receiver_id: string, amount: string, memo: string | null, msg: string) 转移代币给某合约(如 A),并调用合约中的代码;
  3. ft_total_supply() 返回代币总发行量;
  4. ft_balance_of(account_id: string) 返回帐户的代币余额;
  5. ft_on_transfer(sender_id: string, amount: string, msg: string) 这并不是 NEP141 合约需要实现的方法;而是 NEP141 的接收合约需要实现的方法。某帐户通过 Token1 的 ft_transfer_call 方法转移代币 Token1 给某合约帐户 A 时,合约帐户 A 中的 ft_on_transfer 函数会被触发。相当于 Token1 合约会提醒接收合约帐户 A,有人存入了一定数量的 Token1。

使用 near view 可以调用合约的只读方法,下面是查询帐户 24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b 的 USDC 测试币(合约帐户为 3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af)余额的例子:

$ near view 3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af ft_balance_of '{"account_id": "24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b"}' --networkId testnet     # 调用合约的 ft_balance_of 方法
View call: 3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af.ft_balance_of({"account_id": "24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b"})
'110000000'

我们也可以通过 RPC call_function 查询帐户 24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b 的 USDC 测试币余额:

$ curl -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": "dontcare",
  "method": "query",
  "params": {
    "request_type": "call_function",
    "finality": "final",
    "account_id": "3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af",
    "method_name": "ft_balance_of",
    "args_base64": "eyJhY2NvdW50X2lkIjogIjI0YWQ3OGQyNGNjNmIwMjUyNzdjYWYyYTk2ZjAwZjhiNmQzNjU4ZTdmOTZiNGI0MzJkY2RiYmFjZDIwNmRkMWIifQ=="
  }
}' https://rpc.testnet.near.org           # 使用 RPC call_function 查看某帐户的 USDC(3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af)余额
{
  "jsonrpc": "2.0",
  "result": {
    "block_hash": "Abqa7ZxyFjW5h1hgCbbPi7cB4oF5Vxdr91fFWpruHLsn",
    "block_height": 161198517,
    "logs": [],
    "result": [
      34,
      49,
      49,
      48,
      48,
      48,
      48,
      48,
      48,
      48,
      34
    ]
  },
  "id": "dontcare"
}

其中参数 args_base64{"account_id": "24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b"} 的 base64 编码;返回数据 [34, 49, 49, 48, 48, 48, 48, 48, 48, 48, 34] 表示 "110000000" 的 ASCII 编码。

NEP141 有一个特别的地方: 帐户要先要在代币合约中进行注册(本质是往代币合约中充入一定数量的 NEAR),才能接收别人转来的代币。 详情可参考下一节。

6.1. Storage Management(NEP145)

NEAR 使用 Storage Staking 机制,也就是说 合约必须 Stake 足够的 NEAR 余额来支撑它占用的链上存储大小。

在 NEP141 中,用户的代币余额保存在合约中的一个 Map 结构中,合约中每增加一个新用户都会在这个 Map 中添加一条记录,这会导致合约对应的链上存储变大,从而合约中的 NEAR 余额也必须变大(因为 Storage Staking 要求合约占用的存储越大,合约的 NEAR 余额就要越大)。 NEP145 中,提出了一种统一的 Storage Management 机制来把 Stake NEAR 的行为转移给用户。具体来说,每个合约(如 NEP141)中为每个用户设置了 storage 帐户,每个合约实现了下面方法来管理这个帐户:

  1. storage_balance_bounds 查询 storage 帐户余额的要求,即返回最小值和最大值。对于 NEP141 来说,合约中没有复杂逻辑,用户对合约存储的占用是固定的,所以最小值和最大值是相同的;
  2. storage_balance_of 查询用户在该合约中的 storage 余额。返回数据中有 total 和 available(可提取的数量)两个字段;对于 NEP141 来说,如果最小值和最大值往往相同,则 total 总是最小值,而 available 总是 0;
  3. storage_deposit 用户往 storage 帐户中充值 NEAR(相当于注册),充值数量不会超过 storage_balance_bounds 返回的最大值,超过部分会返还给用户。storage_deposit 有个 registration_only 参数,当它为 true 时表示仅用于注册,也就是说如果注册时指定的 NEAR 多于 storage_balance_bounds 返回的最小值,则会返还多余部分给用户。当它为 false 时,表示可以接收额外的 NEAR,但也不会超过 storage_balance_bounds 返回的最大值,超过部分会返还给用户。对于 NEP141 来说, storage_balance_bounds 返回的最小值和最大值往往相同,所以指定 registration_only 指定为 true 或者 false 都可以;
  4. storage_withdraw 用户从 storage 帐户中提取 NEAR。可提取数量是 storage_balance_of 返回的 available 值;
  5. storage_unregister 关闭帐户,取走里面的 NEAR。默认需要清空代币后,才能调用这个方法关闭帐户,除非参数指定 {"force": true} 进行强制关闭帐户。

当用户调用 NEP141 的 ft_transfer 方法往另一帐户 A 转移代币时,在 ft_transfer 函数内部会检测帐户 A 是否以前调用过 storage_deposit 注册帐户,如果以前没有注册过则报错(细节参考合约函数 internal_unwrap_balance_of),具体逻辑如下:

ft_transfer
 -> internal_transfer
  -> internal_deposit
   -> internal_unwrap_balance_of 检测是否在合约中注册过,没有注册过就报错

storage_deposit
 -> internal_register_account 在合约中注册帐户

函数 ft_transfer 的实现细节可参考:https://github.com/near/near-sdk-rs/blob/2f45c1693c7dff93932a4f118c3e4663e59d52fb/near-contract-standards/src/fungible_token/core_impl.rs#L122

6.2. 附带 1 yoctoNEAR

在调用 ft_transferstorage_withdraw 等需要减少用户代币的方法时,调用者必须精确地(不能多不能少)附带 1 yoctoNEAR。其目的是确保调用者不是使用 Function-Call Key(这个 Key 很可能会共享给别人)访问合约方法,而是使用 Full-Access Key 访问合约方法。因为 Function-Call Key 不能附带任何 NEAR,这里能成功附带了 1 yoctoNEAR 就说明一定经过了用户的 Full-Access Key 签名,参考节 2.1

6.3. NEP141 转账流程

Bob 想给 Alice 转移 NEP141 代币,可以进行下面操作:

  1. Bob 调用 NEP141 的 storage_balance_of 查询 Alice 是否注册过(有余额就是注册过)。
  2. 如果 Alice 注册过,对 Bob 调用 ft_transfer 给 Alice 转移代币;
  3. 如果 Alice 没有注册过,则 Bob 帮助 Alice 进行注册(当然也可以拒绝转账,通知 Alice 自己注册后,Bob 再转账)。Bob 先通过 storage_balance_bounds 查询最小值(即注册时最少需要的 NEAR 数量),然后 Bob 在 Tx 中指定两个 Actions,一是通过 storage_deposit 帮助 Alice 注册;二是通过 ft_transfer 给 Alice 转移代币。

Tx AvnkT5PjG5Vy5e78D3rpV8Ri1KLWQ73pAaPsgYNk9nHd 是某帐户第 1 次给 24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b 转移 USDC 测试币的例子;Tx 7b3vLnLtVAgGKDcTeS9WMK5Shxca3BTozeSeSN2H2k9Q 是某帐户第 2 次给 24ad78d24cc6b025277caf2a96f00f8b6d3658e7f96b4b432dcdbbacd206dd1b 转移 USDC 测试币的例子。在它们的交易详情中我们可以看到第 1 次给帐户转移 USDC 测试币时,同时指定了两个 Actions,即 storage_depositft_transfer ;而第 2 次给帐户转移 USDC 测试币时,只指定了一个 Action,即 ft_transfer

7. 参考

Author: cig01

Created: <2024-03-30 Sat>

Last updated: <2024-04-12 Fri>

Creator: Emacs 27.1 (Org mode 9.4)