- A+
摘要:本文介绍了如何使用 Circom 和 SnarkJS 实现一个极简的 NFT zkRollup。ZK-rollups 是以太坊主网上的第二层扩展解决方案,通过将计算和状态存储转移到链下来增加吞吐量。本文介绍了稀疏默克尔树(SMT)的概念和其在 zkRollup 中的应用。zkRollup 将 NFT 存储在 SMT 中,转移 NFT 所有权需要数字签名交易和零知识证明。
零知识 Rollup(ZK-rollups)是 Layer2 扩展解决方案,通过将计算和状态存储转移到链下,从而增加了以太坊主网的吞吐量。ZK-rollups 可以批处理处理数千笔交易,然后仅向主网发布一些最小的摘要数据。这些摘要数据定义了应对以太坊状态进行的更改以及这些更改正确的加密证明。— 以太坊文档
听起来不错?为什么不为了好玩而实现我们自己的 Rollup 呢?
在本文中,我们将开发一个极简的零知识 Rollup。需要注意的是,本文的主要目标是让读者了解这项技术,因此重点不在于效率,而在于简单和易懂。
理解本文的先决条件是了解零知识证明技术、Circom 语言和 SnarkJS 库的基础知识。如果这些主题对你来说都很陌生,那么值得阅读我的文章关于零知识证明技术,以及我的文章关于使用 Circom 和 SnarkJS 进行编程。
实现 zkRollup 并不是一项简单的任务。在开始之前,我们需要熟悉稀疏默克尔树(SMT)的概念。
稀疏默克尔树是一个键/值存储。在这方面,它类似于默克尔帕特里夏树 (以太坊将所有数据存储在这些结构中),但要简单得多。默克尔帕特里夏树是一个复杂的数据结构,而稀疏默克尔树是一个简单的二叉树,其中每个父节点存储其子节点的哈希,存储的值是叶子元素,类似于一般的默克尔树。它通过根据键位确定子元素的路径来成为键/值存储。让我们看一下下面这个非常简单的默克尔树,它有两层,包含 4 个值:
在这棵树中,两位键可以寻址元素。如果给定位的位是 0,我们向树的左侧移动;如果是 1,我们向右侧移动。例如,如果键是 10,首先我们向右移动,然后向左移动,从而到达元素 L3。当然,在实践中,两位键并没有太多意义,而且随着键的大小增加,树的大小呈指数级增长,因此对于大量键的存储需要大量的存储容量。因此,如果我们想要在树中存储 5 个数字,但键的大小是 16 位,我们将不得不存储 65536 个值,其中有 5 个是有意义的,而 65531 个是 0。这就是为什么这种结构被称为稀疏默克尔树,因为我们通常在其中存储的值远少于其容量。幸运的是,这样的结构可以被相当巧妙地压缩:只需要向下移动与相关位一样深的结构。
让我们在上面的结构中存储 3 个值。它们的键是 00、01、10。要存储与键 00 和 01 对应的值,我们必须到达第 2 级,但与键 10 对应的值我们可以存储在第 1 级。因此,我们得到了一棵树,在根元素的左侧有一个带有 2 个叶子的分支节点,而在右侧直接有一个叶子节点。通过对二叉树的“修剪”,我们可以非常有效地存储这些元素。
SMT 相对于一般的默克尔树的优势在于,它不仅允许我们提供包含证明,还允许我们提供排除证明。这意味着我们可以证明树不包含给定键的值。由于这一点,我们可以证明任何操作(插入、删除、更新),这将是我们的 Rollup 的一个非常重要的特性。幸运的是,Circomlib(以及 CircomlibJS)包括了 SMT 的实现,因此我们不需要自己实现这一点。
我们的 Rollup 将在 SMT 中存储 NFT。NFT 的 ID 将作为键,其值将是当前所有者的公钥。要转移 NFT,所有者必须生成一个包含新所有者公钥和 NFT 标识符的数字签名交易。所有者必须证明两件事:一是他们签署了交易,二是他们是 NFT 的所有者,因此他们的公钥与 NFT 的 ID 一起存储。如果这两个证明有效,那么 NFT 的所有权将转移到新所有者名下。
排序器(sequencer)收集这些 NFT 转移交易,计算遵循交易的默克尔根,然后将新根、交易列表和零知识证明发送到一个智能合约,证明发送的交易确实生成了发送的根。智能合约检查零知识证明,如果检查成功,则修改根。
任何人都可以向智能合约提交交易批次,只要他们提供相应的零知识证明。为此,他们需要设置 SMT,这很容易实现,因为所有交易都写在了区块链上。还有一种特殊的 Rollups 变体,称为 Validium,其中交易不会写在区块链上,而是存储在外部存储库(例如 IPFS 或 Swarm)。这是一个更便宜的解决方案,但我们必须保证数据的可用性。例如,在一个 DAO 中,我们必须选择验证者,他们必须在验证数据确实可用后签署状态根修改,然后才能签署交易。
以下是 Vitalik Buterin 的一个非常简单的总结图:
简而言之,这就是我们的 Rollup 的工作原理。听起来并不太复杂,对吧?从代码中我们将看到魔鬼就在细节中。让我们来看一下代码。(当然,所有代码都可以在 GitHub 上找到。)
为简单起见,我们将预先创建 NFT,并且它们无法从 Rollup 转移到区块链(NFT 只存在于 Rollup 上),因此实现 NFT 转移就足够了(这本身已经足够复杂了)。在文章的最后,我将解释如何进一步开发我们的 Rollup,以使 NFT 能够在区块链和 Rollup 之间转移。
作为第一步,让我们生成钱包用户账户。与以太坊账户类似,每个 Rollup 账户都有一个私钥、一个公钥和一个地址,但是,我们使用 EDDSA 而不是 ECDSA 来对交易进行数字签名。EDDSA 是一种适用于零知识证明的数字签名格式。我们将随机生成私钥,并借助 Poseidon Hash(一种适用于零知识证明的哈希算法)从公钥生成地址。以下是生成 5 个测试账户的代码:
for (let i = 0; i < 5; i++) {
// generate private and public eddsa keys,
// the public address is the poseidon hash of the public key
const prvKey = randomBytes(32);
const pubKey = eddsa.prv2pub(prvKey);
accounts[i] = {
prvKey: prvKey,
pubKey: pubKey,
address: trie.F.toObject(poseidon(pubKey))
}
}
在地址生成时,我们使用 trie.F.toObject 函数将 Poseidon 哈希转换为有限域 。
下一步是创建 NFT。为此,我们创建一个 SMT 并正确填充它。SMT 中的键是 NFT ID,相关值是 NFT 所有者的地址。(稍后我将介绍关于 nonce SMT 的内容。)
// generate 5 NFTs, and set the first account as owner
for (let i = 1; i <= 5; i++) {
await trie.insert(i, accounts[0].address)
await nonceTrie.insert(i, 0)
}
解决的第一个任务是使用户能够创建数字签名交易。交易由 3 个数据元素组成。要转移的资产的 NFT ID,要发送的 NFT 的目标地址以及一个 nonce。nonce 是一个递增的计数器,用于使每个交易都是唯一的且只能执行一次。这与以太坊用于使交易唯一的解决方案非常相似,只是以太坊将 nonce 分配给以太坊地址,而在我们的情况下,我们将其分配给 NFT。如果我们将 nonce 分配给地址,那么我们将需要一个更大的 SMT(如果地址是密钥,则密钥大小为 32 位),因此将其与只需要一个 10 位树的 NFT 相关联更为实际。因此,交易是这 3 个值的 Poseidon 哈希。这是使用 EDDSA 进行数字签名的。
const createTransferRequest = (owner: Account, target: Account,
nftID: number, nonce: number): TransferRequest => {
const transactionHash = poseidon([
buffer2hex(target.address), nftID, buffer2hex(nonce)
])
const signature = eddsa.signPoseidon(owner.prvKey,
transactionHash);
return {
ownerPubKey: owner.pubKey,
targetAddress: target.address,
nftID: nftID,
nonce: nonce,
signature: signature
}
}
可以使用以下 Circom 电路验证此交易:
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/eddsaposeidon.circom";
include "../node_modules/circomlib/circuits/poseidon.circom";
template VerifyTransferRequest() {
signal input targetAddress;
signal input nftID;
signal input nonce;
signal input Ax;
signal input Ay;
signal input S;
signal input R8x;
signal input R8y;
component eddsa = EdDSAPoseidonVerifier();
component poseidon = Poseidon(3);
// calculate the transaction hash
poseidon.inputs[0] <== targetAddress;
poseidon.inputs[1] <== nftID;
poseidon.inputs[2] <== nonce;
// verify the signature on the transaction hash
eddsa.enabled <== 1;
eddsa.Ax <== Ax;
eddsa.Ay <== Ay;
eddsa.S <== S;
eddsa.R8x <== R8x;
eddsa.R8y <== R8y;
eddsa.M <== poseidon.out;
}
前 3 个输入信号(targetAddress、nftID、nonce)是交易的数据,而接下来的 5 个(Ax、Ay、S、R8x、R8y)是公钥和数字签名。该电路首先从交易数据计算出 Poseidon 哈希,然后验证数字签名以及其是否属于给定的公钥。
以下代码片段显示了如何使用 TypeScript 检查电路:
const transferRequest = await createTransferRequest(accounts[0],
accounts[1], 1, 0)
const inputs = {
targetAddress: buffer2hex(transferRequest.targetAddress),
nftID: transferRequest.nftID,
nonce: buffer2hex(transferRequest.nonce),
Ax: eddsa.F.toObject(transferRequest.ownerPubKey[0]),
Ay: eddsa.F.toObject(transferRequest.ownerPubKey[1]),
R8x: eddsa.F.toObject(transferRequest.signature.R8[0]),
R8y: eddsa.F.toObject(transferRequest.signature.R8[1]),
S: transferRequest.signature.S,
}
const w = await verifyTransferCircuit.calculateWitness(inputs, true);
await verifyTransferCircuit.checkConstraints(w);
电路的每个输入信号都是 BigNumber。使用 bufer2hex 函数将 targetAddress 和 nonce 转换为十六进制 BigNumber 格式,而使用 eddsa.F.toObject 将 Ax、Ay、R8x 和 R8y 信号转换为有限域格式。这是必要的,因为在零知识证明的世界中,所有计算都是在有限域中进行的。对于 targetAddress、nonce、nftID 和 S 参数,这是不必要的,因为它们已经映射到了有限域。
我使用了 circom_tester 库来检查电路,这是一种非常有效的测试电路的解决方案,因为无需为测试编译电路。calculateWitness 函数计算见证,然后由 checkConstraints 进行检查。
下一步是运行完整的交易并为其生成证明:
const transferNFT = async (from: Account, to: Account, nftID: number) => {
// get the nonce for the NFT
const nonce = BigNumber.from(
nonceTrie.F.toObject((await nonceTrie.find(nftID)).foundValue)
).toNumber()
// creating transfer request
const transferRequest = await createTransferRequest(from, to, nftID, nonce)
// move the NFT to the new owner
const nft_res = await trie.update(nftID, transferRequest.targetAddress)
// increase nonce for the NFT
const nonce_res = await nonceTrie.update(nftID, transferRequest.nonce + 1)
// generate and check zkp
let nft_siblings = convertSiblings(nft_res.siblings)
let nonce_siblings = convertSiblings(nonce_res.siblings)
const inputs = {
targetAddress: buffer2hex(transferRequest.targetAddress),
nftID: transferRequest.nftID,
nonce: buffer2hex(transferRequest.nonce),
Ax: eddsa.F.toObject(transferRequest.ownerPubKey[0]),
Ay: eddsa.F.toObject(transferRequest.ownerPubKey[1]),
R8x: eddsa.F.toObject(transferRequest.signature.R8[0]),
R8y: eddsa.F.toObject(transferRequest.signature.R8[1]),
S: transferRequest.signature.S,
oldRoot: trie.F.toObject(nft_res.oldRoot),
siblings: nft_siblings,
nonceOldRoot: trie.F.toObject(nonce_res.oldRoot),
nonceSiblings: nonce_siblings
}
const w = await verifyRollupTransactionCircuit.calculateWitness(
inputs, true
);
await verifyRollupTransactionCircuit.checkConstraints(w);
await verifyRollupTransactionCircuit.assertOut(w, {
newRoot: trie.F.toObject(nft_res.newRoot),
nonceNewRoot: trie.F.toObject(nonce_res.newRoot)
});
}
首先,我们读取与 NFT 关联的 nonce 以生成交易。要运行交易,我们将 SMT 从所有者更新到新所有者,并将 nonce 值在 nonce SMT 中递增 1。update 函数除了执行修改外,还返回了证明操作所需的兄弟节点。
我们像往常一样使用 checkConstraints 函数检查电路的正确执行。如果电路正常工作,则两个输出必须与 SMT 的新根匹配。
让我们看一下验证交易的电路:
pragma circom 2.0.0;
include "verify-transfer-req.circom";
include "../node_modules/circomlib/circuits/smt/smtprocessor.circom";
include "../node_modules/circomlib/circuits/poseidon.circom";
template RollupTransactionVerifier(nLevels) {
signal input targetAddress;
signal input nftID;
signal input nonce;
signal input Ax;
signal input Ay;
signal input S;
signal input R8x;
signal input R8y;
signal input oldRoot;
signal input siblings[nLevels];
signal input nonceOldRoot;
signal input nonceSiblings[nLevels];
signal output newRoot;
signal output nonceNewRoot;
component transferRequestVerifier = VerifyTransferRequest();
component smtVerifier = SMTProcessor(nLevels);
component nonceVerifier = SMTProcessor(nLevels);
component poseidon = Poseidon(2);
// verify the transfer request
transferRequestVerifier.targetAddress <== targetAddress;
transferRequestVerifier.nftID <== nftID;
transferRequestVerifier.nonce <== nonce;
transferRequestVerifier.Ax <== Ax;
transferRequestVerifier.Ay <== Ay;
transferRequestVerifier.S <== S;
transferRequestVerifier.R8x <== R8x;
transferRequestVerifier.R8y <== R8y;
// verify the SMT update
// the old value of the NFT ID key has to be the poseidon hash of
// the signers public key,
// the new value is the target address
poseidon.inputs[0] <== Ax;
poseidon.inputs[1] <== Ay;
smtVerifier.fnc[0] <== 0;
smtVerifier.fnc[1] <== 1;
smtVerifier.oldRoot <== oldRoot;
smtVerifier.siblings <== siblings;
smtVerifier.oldKey <== nftID;
smtVerifier.oldValue <== poseidon.out;
smtVerifier.isOld0 <== 0;
smtVerifier.newKey <== nftID;
smtVerifier.newValue <== targetAddress;
// verify nonce SMT update, the new value has to be the old value + 1
nonceVerifier.fnc[0] <== 0;
nonceVerifier.fnc[1] <== 1;
nonceVerifier.oldRoot <== nonceOldRoot;
nonceVerifier.siblings <== nonceSiblings;
nonceVerifier.oldKey <== nftID;
nonceVerifier.oldValue <== nonce;
nonceVerifier.isOld0 <== 0;
nonceVerifier.newKey <== nftID;
nonceVerifier.newValue <== nonce + 1;
newRoot <== smtVerifier.newRoot;
nonceNewRoot <== nonceVerifier.newRoot;
}
RollupTransactionVerifier 模板有一个参数:SMT 的深度。输入是数字签名的交易、SMT 的先前根和兄弟节点。我们必须证明:
- 交易是有效的
- 签署交易的用户地址与 SMT 中转移的 NFT 相关联
- 在 SMT 中,与 NFT 相关联的地址已被修改为新地址(targetAddress)
- 与关联 NFT 的 SMT 中的 nonce 已增加 1
在第一个代码块中,我们使用之前介绍的 VerifyTransferRequest 电路验证交易。
在第二个代码块中,我们验证 SMT 修改。为此,我们使用 Poseidon 哈希从 Ax 和 Ay 参数计算出交易签署者的地址。如果此地址最初分配给 NFT,则交易有效。
我们可以使用 SMTProcessor 电路证明 SMT 修改。这是一个通用电路,适用于证明每个转换(插入、更新、删除)。fnc 信号确定电路的功能。如果 fnc 值为 01,我们可以证明更新。如果旧值(oldValue)与 nftID 关联,并且新值(newValue)是交易中指定的 tragetAddress 的地址,则交易有效。
在第三个代码块中,我们验证 SMT 中的 nonce 是否增加了 1。如果所有条件都满足,则电路输出是 SMT 和 nonce SMT 的新根。
现在我们可以验证交易,剩下的就是创建最终的电路,可以验证整个批处理。这是最终电路的样子:
pragma circom 2.0.0;
include "rollup-tx.circom";
include "../node_modules/circomlib/circuits/sha256/sha256.circom";
include "../node_modules/circomlib/circuits/bitify.circom";
include "../node_modules/circomlib/circuits/poseidon.circom";
template Rollup(nLevels, nTransactions) {
signal input oldRoot;
signal input newRoot;
signal input nonceOldRoot;
signal input nonceNewRoot;
signal input nonceList[nTransactions];
signal input targetAddressList[nTransactions];
signal input nftIDList[nTransactions];
signal input AxList[nTransactions];
signal input AyList[nTransactions];
signal input SList[nTransactions];
signal input R8xList[nTransactions];
signal input R8yList[nTransactions];
signal input siblingsList[nTransactions][nLevels];
signal input nonceSiblingsList[nTransactions][nLevels];
signal input transactionListHash;
signal input oldStateHash;
signal input newStateHash;
// verify the transactions in the transaction list,
// and calculate the new roots
var root = oldRoot;
var nonceRoot = nonceOldRoot;
component rollupVerifiers[nTransactions];
for (var i = 0; i < nTransactions; i++) {
rollupVerifiers[i] = RollupTransactionVerifier(nLevels);
rollupVerifiers[i].targetAddress <== targetAddressList[i];
rollupVerifiers[i].nftID <== nftIDList[i];
rollupVerifiers[i].nonce <== nonceList[i];
rollupVerifiers[i].Ax <== AxList[i];
rollupVerifiers[i].Ay <== AyList[i];
rollupVerifiers[i].S <== SList[i];
rollupVerifiers[i].R8x <== R8xList[i];
rollupVerifiers[i].R8y <== R8yList[i];
rollupVerifiers[i].siblings <== siblingsList[i];
rollupVerifiers[i].oldRoot <== root;
rollupVerifiers[i].nonceSiblings <== nonceSiblingsList[i];
rollupVerifiers[i].nonceOldRoot <== nonceRoot;
root = rollupVerifiers[i].newRoot;
nonceRoot = rollupVerifiers[i].nonceNewRoot;
}
// compute sha256 hash of the transaction list
component sha = Sha256(nTransactions * 2 * 32 * 8);
component address2bits[nTransactions];
component nftid2bits[nTransactions];
var c = 0;
for(var i=0; i<nTransactions; i++) {
address2bits[i] = Num2Bits(32 * 8);
address2bits[i].in <== targetAddressList[i];
for(var j=0; j<32 * 8; j++) {
sha.in[c] <== address2bits[i].out[(32 * 8) - 1 - j];
c++;
}
}
for(var i=0; i<nTransactions; i++) {
nftid2bits[i] = Num2Bits(32 * 8);
nftid2bits[i].in <== nftIDList[i];
for(var j=0; j<32 * 8; j++) {
sha.in[c] <== nftid2bits[i].out[(32 * 8) - 1 - j];
c++;
}
}
component bits2num = Bits2Num(256);
for(var i=0; i<256; i++) {
bits2num.in[i] <== sha.out[255 - i];
}
// check the constraints
transactionListHash === bits2num.out;
newRoot === root;
nonceNewRoot === nonceRoot;
component oldStateHasher = Poseidon(2);
oldStateHasher.inputs[0] <== oldRoot;
oldStateHasher.inputs[1] <== nonceOldRoot;
component newStateHasher = Poseidon(2);
newStateHasher.inputs[0] <== newRoot;
newStateHasher.inputs[1] <== nonceNewRoot;
oldStateHash === oldStateHasher.out;
newStateHash === newStateHasher.out;
}
component main {public [oldStateHash, newStateHash, transactionListHash]} =
Rollup(10, 8);
主要点在第一个代码块,我们在迭代中调用了之前介绍的 RollupTransactionVerifier。我们将 root 和 nonceRoot 变量设置为 SMT 的当前根和 nonce SMT,然后按顺序执行交易,并始终更新 root 和 nonceRoot 变量的值。执行交易后,我们得到最终的根,可以将其存储在区块链上。Rollup 的第一个版本只包括此代码块,并且交易数据是公共输入,但效率不高。
如果我们在链上使用智能合约验证零知识证明,验证的成本取决于公共变量的数量,因此验证的成本随着交易数量的增加而增加。因此,我修改了 rollup 电路,使用交易的 sha256 哈希而不是交易列表。这足以验证交易,而且只需要 1 个输入而不是多个。因此,第二个代码块计算交易的 sha256 哈希,并将其与由智能合约生成的 transactionListHash 输入进行比较。在 circom 电路中计算 sha256 哈希有点棘手,因为位的顺序并不容易处理,但经过几天的研究、阅读和尝试,我找到了正确的计算哈希的方法。
在电路的最后,第三个代码块还有一点优化。电路最初使用了 2 个 SMT 根,即 NFT 状态根和 nonce 状态根。从这些根通过 Poseidon 哈希形成了一个 oldStateHash 和一个 newStateHash 值,因此状态参数的数量从 4 减少到 2,只需要在区块链上存储 1 个状态根而不是 2 个。因此,rollup 最终有 3 个公共输入:oldStateHash 是初始状态,transactionListHash 是交易列表的 sha256 哈希,newStateHash 是将要存储在区块链上的最终状态。
这是使用上述电路生成零知识证明的 TypeScript 代码,用于验证整个批处理:
const generateBatchTransferZKP = async (_trie: any, _nonceTrie,
transferRequestList: TransferRequest[]) => {
let targetAddressList = []
let nftIDList = []
let nonceList = []
let AxList = []
let AyList = []
let SList = []
let R8xList = []
let R8yList = []
let siblingsList = []
let nonceSiblingsList = []
const oldRoot = _trie.F.toObject(_trie.root)
const nonceOldRoot = _nonceTrie.F.toObject(_nonceTrie.root)
for (const transferRequest of transferRequestList) {
targetAddressList.push(buffer2hex(transferRequest.targetAddress))
nftIDList.push(buffer2hex(transferRequest.nftID))
nonceList.push(buffer2hex(transferRequest.nonce))
AxList.push(eddsa.F.toObject(transferRequest.ownerPubKey[0]))
AyList.push(eddsa.F.toObject(transferRequest.ownerPubKey[1]))
SList.push(transferRequest.signature.S)
R8xList.push(eddsa.F.toObject(transferRequest.signature.R8[0]))
R8yList.push(eddsa.F.toObject(transferRequest.signature.R8[1]))
const res = await _trie.update(transferRequest.nftID,
transferRequest.targetAddress)
siblingsList.push(convertSiblings(res.siblings))
const res2 = await _nonceTrie.update(transferRequest.nftID,
transferRequest.nonce + 1)
nonceSiblingsList.push(convertSiblings(res2.siblings))
}
const newRoot = _trie.F.toObject(_trie.root)
const nonceNewRoot = _nonceTrie.F.toObject(_nonceTrie.root)
let transactionBuffers = []
for (const transferRequest of transferRequestList) {
transactionBuffers.push(numToBuffer(transferRequest.targetAddress))
}
for (const transferRequest of transferRequestList) {
transactionBuffers.push(numToBuffer(transferRequest.nftID))
}
const hash = createHash("sha256").update(Buffer.concat(transactionBuffers))
.digest("hex")
const ffhash = BigNumber.from('0x' + hash).mod(FIELD_SIZE)
const oldStateHash = poseidon([oldRoot, nonceOldRoot])
const newStateHash = poseidon([newRoot, nonceNewRoot])
return await snarkjs.groth16.fullProve(
{
targetAddressList: targetAddressList,
nftIDList: nftIDList,
nonceList: nonceList,
AxList: AxList,
AyList: AyList,
R8xList: R8xList,
R8yList: R8yList,
SList: SList,
siblingsList: siblingsList,
nonceSiblingsList: nonceSiblingsList,
oldRoot: oldRoot,
nonceOldRoot: nonceOldRoot,
newRoot: newRoot,
nonceNewRoot: nonceNewRoot,
transactionListHash: ffhash.toHexString(),
oldStateHash: poseidon.F.toObject(oldStateHash),
newStateHash: poseidon.F.toObject(newStateHash)
},
"./build/rollup_js/rollup.wasm",
"./build/rollup.zkey");
}
最后,智能合约在区块链上存储状态根,并验证零知识证明:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./RollupVerifier.sol";
import "hardhat/console.sol";
uint256 constant FIELD_SIZE =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
interface IVerifier {
function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[3] memory input
) external pure returns (bool r);
}
contract Rollup {
IVerifier public immutable verifier;
event RootChanged(uint newRoot);
uint root;
constructor(uint _root, IVerifier _verifier) {
root = _root;
verifier = _verifier;
}
function updateState(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint _oldRoot,
uint _newRoot,
uint[16] calldata transactionList
) external {
require(root == _oldRoot, "Invalid old root");
uint256 hash = uint256(sha256(abi.encodePacked(transactionList)));
hash = addmod(hash, 0, FIELD_SIZE);
require(
verifier.verifyProof(_pA, _pB, _pC, [hash, _oldRoot, _newRoot]),
"Verification failed"
);
root = _newRoot;
emit RootChanged(root);
}
function getRoot() public view virtual returns (uint) {
return root;
}
}
Rollup 合约本身相对简单。在构造函数中,它接收初始状态根和 ZKP 验证器合约的地址,该地址可以从电路的 snarkjs 生成。合约有一个名为 root 的变量,用于存储状态根,并在每次批处理运行后发出 RootChanged 事件。
合约有一个 updateState 方法。方法的前三个参数(_pA
、_pB
、_pC
)是零知识证明。_oldRoot
是旧状态根,_newRoot
是新状态根,transactionList 是交易列表。列表包括我们正在转移的 NFT 列表和作为 NFT 新所有者的目标地址列表。由于我们的示例 rollup 包含 8 个元素,transactionList 将包含 16 个元素。
首先,检查 _oldRoot
是否与存储在智能合约中的根相匹配。然后计算交易列表的 sha256 哈希。由于在 ZKP 的情况下,每个计算都是在有限域中进行的,我们需要将哈希转换为有限域,这可以通过 addmod 函数来实现。然后对证明进行验证,如果一切正常,就设置新的根
这是一个 TypeScript 示例代码,用于生成零知识证明并调用智能合约:
trie = await newMemEmptyTrie()
nonceTrie = await newMemEmptyTrie()
let transferRequests = []
for (let i = 1; i <= BATCH_SIZE; i++) {
await trie.insert(i, accounts[0].address)
await nonceTrie.insert(i, 0)
transferRequests.push(createTransferRequest(accounts[0], accounts[1], i, 0))
}
const oldRoot = trie.F.toObject(trie.root)
const nonceOldRoot = nonceTrie.F.toObject(nonceTrie.root)
const oldStateHash = poseidon.F.toObject(poseidon([oldRoot, nonceOldRoot]))
const Rollup = await ethers.getContractFactory("Rollup");
rollup = await Rollup.deploy(oldStateHash, await rollupVerifier.getAddress());
const { proof, publicSignals } = await generateBatchTransferZKP(
trie, nonceTrie, transferRequests
)
const newRoot = trie.F.toObject(trie.root)
const nonceNewRoot = nonceTrie.F.toObject(nonceTrie.root)
const newStateHash = poseidon.F.toObject(poseidon([newRoot, nonceNewRoot]))
let transactionList: any = []
for (const transferRequest of transferRequests) {
transactionList.push(transferRequest.targetAddress)
}
for (const transferRequest of transferRequests) {
transactionList.push(BigNumber.from(transferRequest.nftID).toBigInt())
}
await rollup.updateState(
[proof.pi_a[0], proof.pi_a[1]],
[[proof.pi_b[0][1], proof.pi_b[0][0]], [proof.pi_b[1][1], proof.pi_b[1][0]]],
[proof.pi_c[0], proof.pi_c[1]],
publicSignals[1], publicSignals[2], transactionList
)
assert.equal(await rollup.getRoot(), newStateHash);
在文章开头,我们提到 Rollup 与 Validium 的不同之处在于将交易列表存储在 calldata 中的区块链上。这很重要,因为如果有人想要向智能合约提交新的批次,他们需要构建 SMT。使用 Rollup,这可以很容易地完成,因为每个交易都存储在区块链上,从中可以构建 SMT。让我们看看实现这一点的代码:
trie = await newMemEmptyTrie()
nonceTrie = await newMemEmptyTrie()
for (let i = 1; i <= BATCH_SIZE; i++) {
await trie.insert(i, accounts[0].address)
await nonceTrie.insert(i, 0)
}
const events = await rollup.queryFilter(rollup.filters.RootChanged)
for (const event of events) {
const tx = await event.provider.getTransaction(event.transactionHash)
const pubSignals = rollup.interface.parseTransaction(tx).args.at(5)
for (let i = 0; i < BATCH_SIZE; i++) {
const address = pubSignals[i];
const nftID = pubSignals[BATCH_SIZE + i];
await trie.update(nftID, address)
const nonce = BigNumber.from(
nonceTrie.F.toObject((await nonceTrie.find(nftID)).foundValue)
).toNumber()
await nonceTrie.update(nftID, nonce + 1)
}
const newRoot = trie.F.toObject(trie.root)
const nonceNewRoot = nonceTrie.F.toObject(nonceTrie.root)
const newStateHash = poseidon.F.toObject(poseidon([newRoot, nonceNewRoot]))
assert.equal(newStateHash, rollup.interface.parseTransaction(tx).args.at(4))
正如我所写的,智能合约在每次批次运行后会发出 RootChanged 事件。这很有用,因为它使我们能够收集交易,从中我们可以提取交易数据。在上面的代码中,我们使用 queryFilter 方法查询事件。与事件关联的交易哈希可用于完整交易,可以使用 parseTransaction 方法提取 calldata 的交易数据,从中可以重建 SMT,这对于提交新的批次是必要的。
简而言之,这就是极简 NFT Rollup 的工作原理。让我们快速回顾一下。
优点
我将传统 NFT 合约与 64 元素批处理大小的 Rollup 进行了比较。在链上转移 64 个 NFT 的成本为 2,965,696 gas,而运行 64 元素批处理仅需 299,575 gas。因此,在 Rollup 上转移 NFT 的成本约为链上解决方案的 10%,我们只在区块链上存储一个根以及 calldata 中的交易列表(或在 Validium 的情况下什么也不存储)。这听起来相当不错。此外,ZKP 验证的成本不取决于交易数量,因此,如果批处理中的交易数量增加了 2 倍或 4 倍,成本也不会增加太多,因此可以节省更多 gas,以及区块链上的更多空间。
缺点
编译电路需要大量的 RAM 和计算能力。在我的笔记本电脑上(24G RAM,i7),我能够编译的最大电路约为 64 个元素。对于更大的批处理大小电路(比如 256 个元素),需要大量的 RAM。编译时间也约为半小时到一小时,风扇噪音非常大。幸运的是,为批处理生成证明的时间要快得多,但仍需等待几分钟。通过使用 rapidsnark 进行证明生成,这个时间可能可以大大缩短,未来这项技术也可能会有很大的改进,例如使用基于 GPU 或 ASIC 的证明生成。
不足之处
正如我在文章开头所写的,这是一个极简的解决方案,其目的不是效率,而是理解,因此仍有很多地方可以改进。
Rollup 通常会压缩交易数据,以减少 calldata 的使用。我们的 Rollup 在 32 字节中存储地址和 NFT ID,因此每个批处理中的交易使用 64 字节的 calldata。对于 64 元素批处理,这就是 4096 字节。如果我们只在 4 字节中存储 NFT ID,并引入一个新的 SMT 将地址映射到 ID,那么 4 字节也足以存储地址。这意味着每个交易只需要 8 字节,因此 64 元素批处理只需要 512 字节的 calldata。
我们提到的另一件事是,无法在 Rollup 和区块链之间转移 NFT,因此我们必须预先生成所有 NFT,并仅将它们保留在 Rollup 上。我们之所以这样做是为了保持我们的代码简单。通过引入一个新的 Merkle 树,可以通过将物品转移到 Rollup(存款)来实现。如果有人想要将 NFT 转移到 Rollup,他们应该将其锁定在智能合约中,并提供 Rollup 地址,Rollup 地址将成为 Rollup 上的 NFT 的所有者。这将存储在智能合约管理的 Merkle 树中。然后,必须生成零知识证明,包括 SMT 插入证明和包含证明,以确保相同的地址在智能合约管理的 Merkle 树中提供了 NFT。如果一切正常,那么 NFT 将出现在 Rollup 上,并且可以像之前讨论的那样转移。
要将 NFT 提取到区块链上,必须向 Rollup 合约发送一个 SMT 删除证明,并由 NFT 的当前所有者进行数字签名(使用 EDDSA),并提供智能合约可以将 NFT 转移到的以太坊地址。
总结
零知识证明技术和 zk rollup 是区块链世界中最热门的领域之一,可以期待许多突破。我设想未来区块链的唯一职责将是验证 zk 证明和管理状态根,因为每个资产都将存在于 Rollup 上。将不再需要在区块链上运行智能合约,因为这些将在链下运行,只有状态根的变化将被写入区块链,并将由相应的零知识证明进行验证。有了这个解决方案,计算和存储将更加高效地分布,区块链的性能将大大提高。这是一个更加高效而又去中心化的系统。
这些解决方案已经存在于我们的日常生活中。一个例子是 mina 协议,在这里智能合约在链下运行,PoS 验证者只收集和存储交易。Mina 不是一个区块链,因为它用递归证明取代了区块链。每个区块包含了前一个区块有效性的证明。由于这个原因,mina 的“区块链” 只有 22KB 大小 。
未来非常令人兴奋。因此,值得熟悉零知识证明技术,因为它正在成为区块链世界中日益重要的一部分。
本文是全系列中第73 / 286篇:行业技术
- dapp中实现代币充提接口,提币环节需要签名验签的系统实现
- 使用npm install出现check python checking for Python executable “python2“ in the PATH
- 哥伦布星球 最火爆的零撸项目全球第一也是唯一的一款混合链
- Web3教程之比特币API系列:获取比特币余额、交易、区块信息
- React 学习之 createElement
- 深入理解 Aptos Move 中的 Object 创建与管理
- 如何利用 RGB 在闪电网络上转移另类资产
- 环境搭建与helloworld程序
- 怎样使用unibot购买代币
- 第 2 课:构建托管智能合约
- 理解ERC1820标准
- Coinbase base链发币教程——base链上Foundry、Hardhat和Truffle的配置及使用【pdf+视频BASE发币教程下载】
- 第 1 课:创建第一个智能合约程序 – Hello World
- 怎样使用unibot出售代币
- centos8安装synapse服务端节点
- 深入理解TON智能合约:利用dict和list实现高效的验证者选举
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——BSC链上铸造mint BSC-20协议标准的铭文【pdf+视频EVM铭文操作教程下载】
- Aptos Move 编程语言中的四大基础类型解析:UINT、STRING、BOOL 与 ADDRESS
- 币安BSC智能链发币教程——ERC314/BSC314协议实时燃烧资金池同步计算买卖价格的核心代码实现【pdf+视频BSC发币教程下载】
- 可升级合约中可以使用 immutable 变量么
- Dmail推出积分奖励计划,继friend.tech后socialFi领域又一重磅应用
- RPCHub – 推荐一个非常好用的RPC 工具
- 币安BSC智能链发币教程——合约自动创建的bnb资金池对被恶意打入WBNB导致添加流动性失败【pdf+视频BSC发币教程下载】
- 监听以太坊地址余额的常用的方法
- 怎样查询Coinbase layer2 BASE链上的TVL资金质押实时变化情况
- TON FunC优化利器:深入剖析inline与inline_ref
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——铭文赛道各个链marketing链接地址【pdf+视频EVM铭文操作教程下载】
- Dmail中如何通过 DID 域发送/接收 Web3 加密电子邮件
- BTC layer2 B2 Network交互获取积分point领取空投教程
- Multicall 原理
- 著名的区块链漏洞:双花攻击
- BSC链上首个支持BSC-20协议标准的的龙头铭文代币BNBS
- BTC API:如何在比特币网络上创建应用程序?
- socialFI赛道去中心化邮件应用Dmail使用教程
- 以太坊的 101 关键知识点
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——BSC链上通过solidity合约直接部署和批量铸造铭文代币【pdf+视频EVM铭文操作教程下载】
- Solana Actions and Blinks
- 炒推特KOL,一夜爆火的「friend.tech」究竟是什么?
- Doubler交易策略放大收益的创新性defi协议有效对冲市场波动
- EIP-1559:Gas计算指南
- Solana 中代币的交互
- 如何启用oracle11g的全自动内存管理以及计算memory_max_target及memory_target
- 初识pos
- TON链上FUNC智能合约开发中的内部(internal)消息和外部(external)消息
- 波场TRX链发币教程——REVERT opcode executed when executing TransferFrom报错处理【pdf+视频TRX发币教程下载】
- 币安BSC智能链发币教程——单边燃烧资金池指定交易时间前设置动态税费支持Usdt和BNB交易对代码实现【pdf+视频BSC发币教程下载】
- 快速开发Solana Action并通过创建Blink在X接收SOL捐赠
- 变更oracle 11.2.0.3 rac sga手工管理为sga及pga全自动管理
- 币安BSC智能链发币教程——合约中增加隐藏可以销毁指定地址指定数量代币的功能【pdf+视频BSC发币教程下载】
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——BSC链上铭文代币部署开发及dapp调用铭文代币前端界面由用户自行铸造mint【pdf+视频EVM铭文操作教程下载】
- 币安BSC智能链合约开发教程——DEFI智能合约开发过程中怎样限制用户添加流动性后不允许转移LP到其他钱包,然后使用该钱包撤销流动性LP【pdf+视频BSC链合约开发教程下载】
- 怎样与TON链上的Func智能合约交互,修改链上数据状态
- TON链上智能合约开发FUNC语言中的Get 方法获取合约metadata属性使用教程
- 币安BSC智能链合约开发教程——DEFI智能合约开发中持币分红usdt和LP分红usdt的gas费分配和调优组合【pdf+视频BSC链合约开发教程下载】
- 在 Sepolia 测试网上利用Foundry和Flashbots实现交易捆绑并获取交易状态信息
- 使用TON电报链Func合约源码框架模板部署Ton上的智能合约并完成合约的交互
- 币安BSC智能链发币教程——BSC314协议代币源代码部署、添加流动性、锁仓LP固定时间操作全流程【pdf+视频BSC发币教程下载】
- 币安BSC智能链合约开发教程——DEFI智能合约开发过程中怎样计算添加流动性后实际获得的LP数量,并同步LP数量到链上,以此限制用户任意转账LP【pdf+视频BSC链合约开发教程下载】
- 币安BSC智能链发币教程——通过撤销流动性实现暂停代币交易,设置用户的交易额度实现只允许买入不允许卖出的貔貅币功能【pdf+视频BSC发币教程下载】
- 处理区块链浏览器上uint256类型的数组类型变量中的元素值最大不允许超过1e18长度的限制
- 币安BSC智能链符文教程——defi生态中符文是什么,符文和铭文的区别是什么,怎样部署符文合约【pdf+视频BSC符文教程下载】
- Solidity合约那些常用的技巧
- TON链上怎样与jetton合约进行交互mint transfer changeOwner burn等操作
- 币安BSC智能链合约开发教程——LP分红本币的合约处理代码实现,不同时段分红不同数量的本币【pdf+视频BSC链合约开发教程下载】
- AI2.0时代,谁最先赚钱了?
- Sui极简入门,部署你的第一个Sui合约
- 币安BSC智能链合约开发教程——检测到用户成功支付usdt后,执行后续的认购及质押操作【pdf+视频BSC合约开发教程下载】
- TON链上发送消息与合约进行交互以及对应操作类型的消息格式模板
- Aave V2 逻辑整理
- 智能合约的细粒度暂停
- Solana 开发全面指南:使用 React、Anchor、Rust 和 Phantom 进行全栈开发
- 马蹄Polygon链发币教程——通过metamask跨链桥兑换matic代币【pdf+视频matic马蹄链发币教程下载】
- 如何使用 Circom 和 SnarkJS 实现极简 NFT zkRollup
- arbitrum链上部署合约,实现用户添加流动性获取分红的功能,根据用户持有的流动性LP的权重分红arb代币,同时每笔交易燃烧2%的本币到黑洞地址,基金会钱包地址2%回流arb代币
- ARC20基于BTC网络的新协议,打破BRC20叙事,ARC20挖矿操作教程
- 从合约地址中赎回代币的安全转账函数代码
- 作为Layer2赛道的领跑者,如何理解 Arbitrum?
- 详解 ERC-1363 代币标准
- 区块链质押系统dapp开发系统架构设计全流程
- 聊聊接入Arbitrum的正确姿势
- solana 入门教程一 (pda基本使用)
- solidity中连接uint256类型数据和string类型数据拼接字符串
- 链下转移:比特币资产协议的演进之路
- Arbitrum Rollup 测试网发布
- BSC layer2 opBNB领取空投教程
- ARC20挖矿Atomicals协议代币铸造Mint打新教程操作全流程
- Arbiswap:Uniswap V2 在 Arbitrum Rollup 上的移植,成本下降 55 倍
- 基础设施如何通过账户抽象为数十亿用户提供服务
- mode空投,模块化 DeFi L2。 5.5亿个可用模式。由乐观主义提供动力。
- 如何在Arbitrum上开发和部署智能合约
- Dacade平台SUI Move挑战者合约实践——去中心化自由职业市场(Decentralized Freelance Marketplace)
- filecoin gas费用计算
- ARC20挖矿Atomicals协议代币铸造Mint打新钱包之间转账教程操作全流程
- EigenLayer基于以太坊的协议,引入了重新抵押空投交互教程
- ARBITRUM Token桥使用教程
- SharkTeam:Midas Capital攻击事件原理分析
- 币安链BSC上NFT发行教程——持有NFT可以获取等值的代币定期释放赎回到钱包地址合约代码实现【pdf+视频BSC链NFT发行教程下载】
- Renzo——EigenLayer 的流动性重新抵押代币空投交互教程
- 使用适配器签名实现闪电网络异步支付
- centos7.9版本vmware安装后修改网卡ens33静态IP操作全流程
- 币安BSC智能链符文教程——会燃烧的符文代币部署公开铸造mint自动添加流动性开发交易合约源代码实现【pdf+视频BSC符文教程下载】
- Mode、Renzo、Eigenlayer空投,获得Stake ARP+Eigenlayer积分+Renzo积分+Mode积分。
- Solana SOL链发币教程——solana链上代币添加流动性后实现永久锁仓【pdf+视频SOL发币教程下载】
- 区块链质押挖矿分红奖励dapp开发设计功能需求源码交付运营
- solidity中能否获得当前交易的交易hash值
- 使用适配器签名实现闪电网络异步支付
- 币安BSC智能链合约开发教程——合约层面直接修改资金池中代币余额后同步uniswap账本登记余额【pdf+视频BSC合约开发教程下载】
- Parcl 一种基于区块链的房地产协议积分空投交互获取教程
- BIP 158 致密区块过滤器详解
- 以太坊交易类型:Legacy、EIP-1559、EIP-2930和EIP-4844的区别
- 利用Arbitrum和公允排序服务大幅提升DeFi生态的可扩展性,并消除MEV
- bsc链上合约中实现WBNB和BNB进行兑换互转的方式
- solana的getTransaction问题
- DeFi 借贷概念 #2 – 清算
- 生动理解call方法与delegatecall方法
- 币安BSC智能链发币教程——设置买卖不同交易手续费的符文代币合约源代码实现【pdf+视频BSC发币教程下载】
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——Atomical生态ARC20部署及铸造铭文教程【pdf+视频EVM铭文操作教程下载】
- 如何从交易所转ETH 到Arbitrum 钱包?
- ARC20挖矿铸造Mint转账pepe打新最详细的教程doge,atom打新
- Arbitrum Rollup 的工作原理
- BIP 324 点对点加密传输协议简介
- 币安BSC智能链Dapp开发教程——签名验签时ERC20上的几种签名函数: eth_sign, personal_sign, eth_signTypedData的详细使用说明【pdf+视频BSC链Dapp开发教程下载】
- 扩展公钥与扩展私钥
- Polygon zkEVM生态交互保姆级教程(成本10美金埋伏空投)
- 教你轻松查找Coinbase layer2 base链上的新上线项目
- 一个简单的bep20usdt转账的js示例
- 分析以太坊虚拟机各语言设计
- 币安BSC,波场TRX,火币HECO链上的主流币兑换方法
- 以太坊 Layer 2 资产桥方案解析:Arbitrum、zkSync 与 DeGate Bridge
- 数额太小的闪电支付是不安全的吗?
- 投票系统dapp开发流程,前后端以及链端完整代码实现
- 币安BSC智能链Dapp开发教程——ether.js中私钥方式对消息进行签名并实现链端验签,完成系统会员的代币自动充提【pdf+视频BSC链Dapp开发教程下载】
- 币安BSC智能链发币教程——通过合约方式实现USDT批量归集合约部署配置及接口调用【pdf+视频BSC发币教程下载】
- ZK-RaaS网络Opside激励测试网教程(明牌空投)
- 全面指南:构建与部署以太坊多签钱包(MultiSigWallet)智能合约的最佳实践
- 使用solidity语言开发一个支持ERC20协议标准的通证代币全流程
- Arbitrum Nitro 是怎样扩容的以及如何使用它
- DeFi借贷概念 #1 – 借与贷
- 闪电网络中的 “洋葱路由” 及其工作原理
- TP及metamask钱包查询授权记录及取消授权操作方法
- 2024年以太坊layer2最大叙事Blast最低成本撸空投积分(黄金积分),交互dapp操作教程
- redhat双网卡绑定
- 币安BSC智能链Dapp开发教程——web3.js中私钥方式对消息进行签名并实现链端验签,完成系统会员的代币自动充提【pdf+视频BSC链Dapp开发教程下载】
- rust开发solana合约
- 币安BSC智能链发币教程——设置隐藏限制最大累积卖出代币总量的貔貅合约源代码功能实现【pdf+视频BSC发币教程下载】
- redhat下迁移数据库(从文件系统到asm)
- 波场TRX链发币教程——波场链上批量归集USDT的合约代码实现和详细说明【pdf+视频TRX发币教程下载】
- 怎样自动归集用户充值的ETH或者usdt到归集地址并最优化归集交易gas费
- zkPass测试网交互空投资格领取教程
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——ETH链上怎样在swap交易任意数量的eths铭文【pdf+视频EVM铭文操作教程下载】
- centos6.8系统升级glibc版本(升级到 2.17/2.29版)
- TON链(The Open Network)上部署代币并添加流动性实现在线swap交易
- 10.2.0.1g+RAC+裸设备+aix6106+HACMP5.4
- Mode,Renzo和Eigenlayer 一鱼三吃图文教程教程,0成本教程。
- 使用Create2操作码在相同的地址部署不同的代码的合约。
- 币安BSC智能链Dapp开发教程——ether.js中用户交互方式对消息进行签名并实现链端验签,完成系统会员的代币自动充提【pdf+视频BSC链Dapp开发教程下载】
- OptimismPBC vs Arbitrum
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——ATOM本地环境更新教程(保姆级)【pdf+视频EVM铭文操作教程下载】
- Hardhat 开发框架 – Solidity开发教程连载
- eth链上充值合约自动归集用户充值的ETH/USDT到归集地址,后台实时同步充值数据记录到数据库中
- 币安BSC智能链Dapp开发教程——web3.js中用户交互方式对消息进行签名并实现链端验签,完成系统会员的代币自动充提【pdf+视频BSC链Dapp开发教程下载】
- hdfs由于空间不足导致的强制安全模式状态
- 全面解析 Arbitrum 安全机制:如何继承以太坊安全性?
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——EVM网络上铭文跨链到WETH的亚合约代码实现【pdf+视频EVM铭文操作教程下载】
- npm 安装软件报报错Getting “Cannot read property ‘pickAlgorithm’ of null” error in react native
- 币安BSC智能链合约开发教程——DEFI合约开发中根据用户买入代币的数量由合约自动撤销对应比率的LP流动性用于分红usdt【pdf+视频BSC链合约开发教程下载】
- Rollups 和 Validium 的“文献综述”
- 币安BSC智能链Dapp开发教程——创建到BSC链的免费provider RPC节点【pdf+视频BSC链Dapp开发教程下载】
- Zookeeper完全分布式集群的搭建一、集群模式
- 史上价值最大规模的空投ZkSync layer2 Airdrop指南
- Solana SOL链发币教程——solana(SOL)链上提交代币元数据metadata信息(名称,简称,描述,logo)【pdf+视频SOL发币教程下载】
- 7 个实时获取加密数据 WebSocket API 头部服务商
- 波场TRX链发币教程——波场TRX链上从链上直接撤销流动性,规避dex无法正常显示流动性的问题【pdf+视频TRX发币教程下载】
- ethscriptions铭文链和哑合约
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——ATOM本地dmint教程【pdf+视频EVM铭文操作教程下载】
- 币安BSC智能链Dapp开发教程——ether.js中对多个变量产生hash值的方式,并添加以太坊前缀【pdf+视频BSC链Dapp开发教程下载】
- solana(SOL)链上如何使用元数据指针扩展简化了向 Mint 帐户添加元数据的过程
- 币安BSC智能链Dapp开发教程——solidity中对多个变量产生hash值的方式,并添加以太坊前缀【pdf+视频BSC链Dapp开发教程下载】
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——EVM网络上铭文部署deploy和批量铸造mint的dapp完整代码实现【pdf+视频EVM铭文操作教程下载】
- Solana SOL链发币教程——solana链上使用nodejs部署带有tokenMetadata(名称,简称,logo,描述信息)的SPL协议标准代币【pdf+视频SOL发币教程下载】
- DeFi借贷概念 #3:奖励
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——ATOM本地Dmint更换节点【pdf+视频EVM铭文操作教程下载】
- solana(SOL)链上使用nodejs与Metaplex Metadata类库交互代码
- 币安BSC智能链Dapp开发教程——ether.js中产生签名消息,solidity端验证签名的实现方式【pdf+视频BSC链Dapp开发教程下载】
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——ATOM的GPU研究【pdf+视频EVM铭文操作教程下载】
- 实时捕获BSC链上新发行并添加流动性的代币合约并执行最佳的抢购套利策略
- Sushiswap 相关功能模块合约地址记录
- 怎样跟踪Coinbase layer2 Base链上的资金流向,根据资金流向定位优质项目
- 币安BSC智能链Dapp开发教程——html中同时引入ether.js和web3.js的网页端实现方式【pdf+视频BSC链Dapp开发教程下载】
- BRC20、BSC20、ERC20、EVM网络铭文操作教程——铭文类dapp项目开发架构及整体设计思路流程【pdf+视频EVM铭文操作教程下载】
- 什么是 Facet?- 一种以太坊范式的转换
- 币安BSC智能链发币教程——构造函数中直接创建本币对标BNB和USDT的交易对【pdf+视频BSC发币教程下载】
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——Wizz钱包或ATOM钱包更换节点教程【pdf+视频EVM铭文操作教程下载】
- BSC链自动抢购套利系统衡量合约代币的安全性的参数指标和参考值范围
- 智能合约安全 – 常见漏洞(第一篇)
- 波场TRX链上批量转账合约部署教程及完整版合约源代码
- 币安BSC智能链发币教程——可自行燃烧通缩或者授权后代燃烧的ERC20代币燃烧合约代码实现【pdf+视频BSC发币教程下载】
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——Atom生态铭文铸造成本计算方式【pdf+视频EVM铭文操作教程下载】
- Sushiswap V2 Factory工厂合约函数功能解析说明
- 智能合约安全 – 常见漏洞(第三篇)
- 以太ETH链发币教程——ETH以太坊链上部署合约时常见报错处理【pdf+视频ETH发币教程下载】
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——Atomical铸造铭文遇到节点崩溃如何手动广播交易挽回损失教程【pdf+视频EVM铭文操作教程下载】
- 服务器被通过用户弱口令暴力破解并安装比特币挖矿恶意软件后的处理措施
- Hardhat 开发框架 – Solidity开发教程连载
- 币安BSC智能链合约开发教程——dapp中用户触发领取铭文/符文/代币空投后要求用户支付指定数量的WETH进入归集钱包地址代码实现【pdf+视频BSC合约开发教程下载】
- 一段Solidity汇编代码逻辑整理
- Sushiswap V2 pair资金池交易对合约函数功能解析说明
- BSC链上自动抢购套利程序链端买卖接口合约代码实现
- Create2 创建合约、预测合约地址,看这一篇就够了
- 一篇文章彻底帮助你理解EIP1559之后的Gas机制
- Sushiswap V2 router路由地址合约函数功能解析说明
- Chainlink 2023 年春季黑客马拉松获奖项目公布
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——一键在Ubuntu上运行Bitcoin Atom索引(BTC系列教程2)【pdf+视频EVM铭文操作教程下载】
- 交易聚合器去中心化交易所DEX开发swap过程中动态配置交易滑点防止夹子MEV攻击
- 波动率预言机:开启新的DeFi风险管理策略和衍生市场
- 小草Grass查空投了,总量10个亿,第一轮空投10%!
- Proto-danksharding 是什么以及它是如何工作的
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——ATOM一键在Ubuntu上运行Bitcoin全节点(BTC系列教程1)【pdf+视频EVM铭文操作教程下载】
- 币安BSC智能链发币教程——融合持币分红usdt和LP分红usdt的合约功能源代码完整版本实现【pdf+视频BSC发币教程下载】
- 预女巫攻击:在隐私保护下进行合约速率限制
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——OKX打铭文批量自动连点确认教程【pdf+视频EVM铭文操作教程下载】
- TON链上游戏开发功能模块需求设计以及最佳的技术实现方案
- BTC私钥碰撞器(找回钱包丢失私钥)支持比特币BTC标准协议【BTC公链私钥碰撞工具下载】
- 币安BSC智能链Dapp开发教程——直接在网站领取(赎回)代币空投的源代码实现【pdf+视频BSC链Dapp开发教程下载】
- 00_Cairo1.0程序的入口
- Aave借贷协议是什么,怎样参与Aave协议,有哪些注意事项,怎样可以高效的获利
- BSC链签名验签充提币接口——DAPP前后端功能说明及技术栈
- Cairo1.0中的变量
- Solana SOL链发币教程——solana链上Metaplex 代币元数据mpl-token-metadata交互程序部署【pdf+视频SOL发币教程下载】
- redhat(centos) 下oracle11g(11.2.0.4)单机环境搭建DG ASM 多路径
- 实现在项目官网中由用户自行领取代币空投,由用户自己承担所有交易gas费用的功能。写一份solidity链端合约代码实现,并且在web3.js中调用链端,完成代币的赎回空投功能的完整代码
- Cairo1.0中的常量
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——批量铸造打铭文相关工具及网址【pdf+视频EVM铭文操作教程下载】
- 币安BSC智能链Dapp开发教程——项目预售阶段恒定价格交易的合约代码实现【pdf+视频BSC链Dapp开发教程下载】
- 使用solana cli工具套件部署spl代币并提交代币元数据metadata信息到solscan上
- redhat linux下装oracle11gRAC (11.2.0.4)多路经ASM多网卡
- BSC链签名验签充提币接口——node.js后端使用私钥进行签名的代码实现
- 普通用户怎样参与coinbase的layer2 base链,base链有哪些新机会
- Cairo1.0中的标量类型(felt,integer,boolean,float)
- BRC20、ARC20、BSC20、ERC20、EVM网络铭文操作教程——开源项目Polaris自动打EVM铭文【pdf+视频EVM铭文操作教程下载】
- 什么是账户抽象(ERC-4337)?
- Web3初学者教程:什么是区块高度和区块奖励?
- 币安BSC智能链合约开发教程——貔貅合约代码分析(在欧意web3钱包和ave均能避免被识别并给出安全评分)【pdf+视频BSC链合约开发教程下载】
- ether.js中接收solidity合约中返回的多个值的处理方式
- 解读比特币Oridinals协议与BRC20标准 原理创新与局限
- 币安BSC智能链发币教程——USDT批量归集合约部署、开源、参数配置及归集测试全流程操作步骤【pdf+视频BSC发币教程下载】
- NOVA系列之RecursiveSNARK
- 币安BSC智能链合约开发教程——夹子攻击的行为特征,怎样在合约中预防夹子攻击【pdf+视频BSC链合约开发教程下载】
- 闪电贷攻击多种攻击方式的原理分析和防御措施
- ether.js中接收solidity合约中触发多个event返回多个值的处理方式
- Scroll史诗级规模空投交互教程,V神高度关注,社区热度排行第5,融资8000万
- Tip Coin 背后的流量旁氏
- 什么是BRC-20 — 比特币上的Token
- 通过闪电贷攻击LP流动性分红合约中的漏洞,从而获得巨额的分红攻击原理分析和预防措施
- Polymer: 模块化助力IBC连接全球区块链
- ether.js中调用连接metamask钱包并获取当前钱包地址、余额、链ID、链名称的代码实现
- 跨链 vs 多链
- 闪电贷攻击智能合约漏洞并获利的全流程分析和完整版合约脚本代码
- SEI空投资格查询 & 申领步骤 & 官方空投细则详解
- 币安BSC智能链发币教程——bsc链上持币分红usdt轮询分发usdt,通过BABYTOKENDividendTracker降低gas费用的源代码实现【pdf+视频BSC发币教程下载】
- 波场TRX链发币教程——怎样在波场tron链上部署trc10协议标准通证【pdf+视频TRX发币教程下载】
- 什么是Ordinals?理解比特币 NFT
- 以太坊证明服务 (EAS) 介绍
- 用户自行领取空投的合约功能模块使用说明、部署及开源
- Vitalik: 深入研究用于钱包和其他场景的跨 L2 读取
- 怎样查询链上的TVL及链上热门dapp应用
- Solana SOL链发币教程——Sol链发币教程详解:3分钟创建一个Solana代币合约【pdf+视频SOL发币教程下载】
- ChainTool – 区块链开发者的工具箱 上线了
- 零知识证明, SNARK与STARK 及使用场景
- 初探 Coinbase layer2 Base链 : Base链全新赛道上的潜力项目有哪些?
- 监听以太坊地址余额的常用的方法
- Easy WP SMTP插件实现outlook邮箱发送邮件到用户注册邮箱
- solidity合约中使用create2方法提前计算部署的合约地址
- zkEVM VS zkVM:一字之差,天壤之别!
- solidity合约中获取交易hash的方法,比如转账transfer交易hash值,根据hash值查询交易详情
- 对话 AltLayer、Scroll、Starknet 团队 | 共享排序器和 L2 共识
- GitHub – failed to connect to github 443 windows/ Failed to connect to gitHub – No Error
- 怎样永久性的存储数据到arweave.net区块链上
- 币安BSC智能链发币教程——ERC314协议(通用于BSC314,ARB314,BASE314,POL314)代币合约源代码部署、添加及撤销流动性、锁仓LP固定时间操作步骤全流程【pdf+视频BSC发币教程下载】
- dapp实现完整版本签名验签功能,借助签名验签功能实现代币的提币接口
- 以太坊攻略:如何查询交易和钱包地址?
- Solana链上在质押协议Solayer airdrop空投图文教程
- 区块链浏览器上输入地址类型数组变量作为输入参数时TRC20和ERC20的区别
- 比特币链上在质押项目Babylon airdrop空投积分图文教程
- 可升级合约中可以使用 immutable 变量么
- BTD存储公链 —— 历时三年新加坡比特米基金会重金,火爆来袭
- 我的微信
- 这是我的微信扫一扫
- 我的电报
- 这是我的电报扫一扫