我们将在 ZKsync Era 网络上部署一个 ZK 红包项目,它将满足以下需求:
- 去中心化的红包创建以及领取体验
- 创建者可以设置白名单,名单之外的地址无法领取奖励,有效避免机器人抢红包等恶意行为
- 支持随机金额
- 使用 ZK snark 技术,支持密码红包功能(在不暴露密码明文的基础上,在链上验证密码是否正确)
ZK 红包项目已经在 DappLearning 官网上线,您可以在 dapplearning.org/reward 体验完整的红包!
接下来我们考虑如何设计红包合约,以满足上述需求。
- 红包创建者可以设置白名单。显然将白名单完整的保存于链上是不现实的,这将会带来大量的 gas 消耗,一种通用的方案就是使用 Merkle Tree。
- 链上只需要保存 Merkle Root,用户领取红包时,需要根据完整的白名单地址列表,生成 Merkle Proof
- 由于生成 proof 需要完整的地址列表,所以我们需要额外的为用户保存这些列表,例如结合后端数据库,或者使用 IPFS 去中心化存储
- 支持随机金额。实际上是需要链上生成随机数来随机划分金额。
- 可以简单的使用
block.timestamp
,msg.sender
,nonce
, 随机种子等字段进行打包 hash,然后转换为数字 - 或者使用 Chainlink 的链上随机数服务(有一定使用成本)
- 可以简单的使用
- 支持红包密码功能。如果我们希望实现类似口令红包的功能,用户只有在输入正确密码的情况下才能领取,那么在智能合约中实现最大的难点在于如何隐藏明文密码。
- 使用 ZK snark 技术,设计一个简单的电路,即将明文密码进行一次 hash 运算,用户在领取时,提供这个 hash 运算过程的 ZK proof 证明
- 由于明文密码已经经过 hash 运算,公开的 zk 电路输出只是一个 hash 值,不会暴露明文密码
- 而 ZK snark 技术保证了用户进行 hash 的原象是正确的密码
IPFS 存储白名单的方案,可以参考红包项目的开源代码版本 dapp-learning-app; 主要流程是根据先将完整的白名单上传 IPFS,然后将 cid 塞入创建红包的自定义信息中;而领取时,解析自定义信息中的 cid 部分,去 ipfs 获取完整的白名单列表;另外没有后端的辅助,红包列表需要完全依靠前端配合 subgraph 获取,这个过程耗时较长,可以放到
webworker
中进行.
我们继续梳理整个流程:
-
初始化红包合约
- 设置随机种子,用于随机红包的随机数生成
- 设置 ZK snark 的 verifier 合约地址(稍后会解释用途)
-
创建红包的准备阶段
- 在调用合约创建接口之前,创建者还需要在链下进行一些计算:
- 根据白名单地址列表生成 merkle tree,拿到 merkle root
- 如果将创建密码红包,需要计算密码的 hash 值,注意:这里必须使用和 zk 电路一致的 hash 方法,且需要
zk-friendly
, 比如Poseidon Hash
- 如果不设置密码,
lock
字段直接传零值 - 保存白名单列表,一种方法是传给后端数据库,一种方法是保存在去中心化存储中,例如 IPFS
- 如果红包奖励的是 ERC20 token,需要先进行 approve 授权操作
- 在调用合约创建接口之前,创建者还需要在链下进行一些计算:
-
创建红包
- 调用红包创建接口,传入相关参数
- 红包合约的 nonce + 1
- 根据
msg.sender
+message
(自定义消息,一般为时间戳) 进行 hash,作为新红包的 id - 合约进行条件检查
- 可领取人数检查,至少大于 0,且不能超过上限
- token_type 检查,0 为 ETH,1 为 ERC20 token,不允许其他值
- 保证每个领取者领取的金额有一个下限(比如 0.1),避免因随机生产的过于极端的金额,即
总金额 / 数量 >= 0.1
- 检查新红包 id 没有重复
- 转入 ETH 或者 ERC20 token ,检查合约余额变化,转账金额是否足够
- 保存红包数据
- 发送 event,便于链下跟踪相关数据
-
领取红包准备阶段
- 是否为密码红包
- 是,需要额外生成 ZK proof
- 否,跳过
- 获取完整的白名单地址列表,生成自己的 merkle proof
- 是否为密码红包
-
领取红包, 这里区分为两个接口,一个普通红包领取,不需要传入 zk proof,另一个密码红包领取,需要 zk proof
- 是否为密码红包
- 是,调用
claimPasswordRedpacket
, 大部分流程与普通红包一样,但是会先调用 zk 相关的验证方法Groth16Verifier.verifyProof()
- 否,调用
claimOrdinaryRedpacket
- 是,调用
- 条件检查
- 红包是否已过期,过期无法领取
- 已领取的人数小于总可领取人数,即可领取人数满后,不能领取
- 验证 merkle proof,即领取者是否在白名单列表中
- 领取者尚未领取该红包
- 是否为随机金额红包
- 是,计算随机数,进而得到随机金额,且不低于领取下限
- 否,直接均分金额
- 存储领取者的地址以及金额
- 转账
- 发送 event,便于链下跟踪相关数据
- 是否为密码红包
-
过期红包的金额赎回
- 条件检查
- 是否为创建者
- 该红包是否已经过期
- 红包仍有金额(没有领取完)
- 转账
- 发送 event,便于链下跟踪相关数据
- 条件检查
由于 zk-SNARK 原理非常复杂且难懂,很难快速向您解释其底层原理,这里将简单介绍两种工具的使用流程,以及每一条命令后做了什么事情。掌握这些步骤即可搭建简单的 zk 应用,当然还是强烈建议您深入了解其原理,领略 ZK 的魅力,推荐 ret 老师在我们开源大学分享的 zk-SNARK 系列课程。
circom
用于编写电路的文件格式,同时 circom 可执行文件提供了将电路文件编译为 二进制wasm
文件的方法,即从高级语言编译到低级语言snarkjs
遵循 zk-SNARK 协议,根据电路生成证明以及验证的工具库
上图所描述的过程:
- 编写电路(使用
.circom
文件) - 将电路编译成二进制文件
.wasm
- 生成 witness 文件
- witness 可以近似的理解为结合 input 输入 和 算数电路约束的一种数据形式
- 在 zk 红包的开发中,我们不会使用 witness 文件,而是借助
verification_key
,.zkey
文件,直接使用 js 脚本生成 proof
- 进行 trusted setup(信任仪式)生成
.ptau
文件,进而生成.zkey
文件;然后根据.zkey
以及.wasm
文件,以及 input 输入 生成 proof;.ptau
文件不要暴露,建议在完成 trusted setup 步骤后删除,如果暴露,则会面临被攻击的风险.zkey
文件包含电路的证明密钥(proving key)和验证密钥(verification key)
- 验证 zk proof (该图版本较老,命令有所差异)
我们的目的是让用户在不暴露密码的情况,证明密码正确。通常电路的 input 可以是 private
, output 是 public。所以我们的电路的 output 不能输出明文密码,那么一个单向的 hash 函数可以很好的满足这个需求。
也就是说,我们需要设计一个电路,约束一个 hash 运算,prover 可以隐藏自己的明文密码输入,但是输出是密码的 hash,这个 hash 可以公开,保存在链上,以便验证者核对检查。
password(private) -> hash function(circuit) -> hash(public)
由于 hash 函数的单向性质,攻击者很难从 hash 还原到原象(明文密码);同时我们使用电路约束了 hash 计算过程的正确性,那么当 prover 提供了一个的 proof,证明了计算过程合法,且输出的 hash 与链上保存一致,我们就能认为 prover 是知道正确密码的。
上述图片过程是使用命令行进行生成 proof 以及验证的过程,而我们开发的红包应用则需要将验证阶段放到链上,snarkjs 提供了一个非常好用的命令,帮助我们自动生成 verifier.sol
合约。
npx snarkjs zkey export solidityverifier circuit_final.zkey contracts/redpacket/verifier.sol
得到合约文件后,我们只需要将其部署到链上,调用其验证接口,就能实现链上 zk proof 验证。
另外我们还需要将 verfication_key 导出为 json 文件,方便脚本调用
# Export the verification key
npx snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
verifier 合约是一个非常复杂的合约,充斥着大量的"魔法数字"以及难以理解的 assembly
代码, 简单来说它的功能是使用 verification_key
来验证 zk proof。对比这些数字,不难发现合约中的 verfication key 和我们导出的 json 文件是一致的。
// verifier.sol
contract Groth16Verifier {
...
// Verification Key data
uint256 constant alphax = 19697311613983860786212999493703568908688055110191172310826512624877065310240;
uint256 constant alphay = 15269637490765963877960429824285449706379884451647116762156513512057908209004;
uint256 constant betax1 = 20296311323050859114464236566100939242060256604420178663056396770577093522106;
uint256 constant betax2 = 13493524922581187257154998994913263575067438443058928748339785154227743480315;
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) public view returns (bool) {
...
}
}
// verification_key.json
{
...
"vk_alpha_1": [
"19697311613983860786212999493703568908688055110191172310826512624877065310240",
"15269637490765963877960429824285449706379884451647116762156513512057908209004",
"1"
],
"vk_beta_2": [
[
"13493524922581187257154998994913263575067438443058928748339785154227743480315",
"20296311323050859114464236566100939242060256604420178663056396770577093522106"
],
[
"20523082812265072401230822370196284808250923800479258640050610593596972848609",
"14567830741370033739684300122126422233548108646784945873266980459071562511739"
],
...
}
在 js 脚本生成 proof 的过程中,我们可以直接使用 groth16.fullProve
函数来生成 proof,不用运行命令行命令。
import { groth16 } from "snarkjs";
...
export const calcProof = async (input: string) => {
const proveRes = await groth16.fullProve(
{ in: keccak256(toHex(input)) },
path.join(__dirname, "../datahash_js/datahash.wasm"),
path.join(__dirname, "../circuit_final.zkey")
);
...
}
为了实现 zk 验证密码,我们需要编写一个简单的 circom 电路。
circom 的基本语法比较简单,可以参考 circom 官方文档
这个电路约束了一个简单的 Poseidon hash 的运算过程,prover 将通过这个电路,证明他的 hash 结果是按照规则计算得出,而计算结果将与链上保存的 hash lock 值比较,如果一致,说明 prover 输入了正确的密码。
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/poseidon.circom";
template PoseidonHasher() {
signal input in;
signal output out;
component poseidon = Poseidon(1);
poseidon.inputs[0] <== in;
out <== poseidon.out;
}
component main = PoseidonHasher();
存储 Redpacket 数据的结构体,以及 redpacket_by_id
mapping.
字段说明:
packed
合并存储一些长度较短的数据,节省 gastotal_tokens
总金额expire_time
红包过期的时间戳token_addr
ERC20 token 的地址, ETH 则可以输入零地址numbers
可领取红包的最大人数token_type
0 为 ETH 红包,1 为 ERC20 token 红包ifrandom
true 为随机金额, false 为平均分配金额
claimed_list
: 已领取的地址列表merkleroot
: merkle rootlock
: hash lock 在密码红包时用到creator
: 创建者地址
struct RedPacket {
Packed packed;
mapping(address => uint256) claimed_list;
bytes32 merkleroot;
bytes32 lock;
address creator;
}
struct Packed {
uint256 packed1; // 0 (128) total_tokens (96) expire_time(32)
uint256 packed2; // 0 (64) token_addr (160) numbers(15) token_type(1) ifrandom(1)
}
mapping(bytes32 => RedPacket) public redpacket_by_id;
function create_red_packet(
bytes32 _merkleroot,
bytes32 _lock,
uint _number,
bool _ifrandom,
uint _duration,
string memory _message,
string memory _name,
uint _token_type,
address _token_addr,
uint _total_tokens
) public payable {
nonce++;
// check conditions...
// ETH / token transferFrom creator
if (_token_type == 0) {
require(
msg.value > 10 ** 15 * _number,
"At least 0.001 ETH for each user"
);
require(msg.value >= _total_tokens, "No enough ETH");
} else if (_token_type == 1) {
IERC20(_token_addr).safeTransferFrom(
msg.sender,
address(this),
_total_tokens
);
received_amount = balance_after_transfer - balance_before_transfer;
require(received_amount >= _total_tokens, "#received > #packets");
}
bytes32 _id = keccak256(abi.encodePacked(msg.sender, _message));
// save data to redpacket_by_id[_id]
}
claim 根据红包是否设置密码,分为两个接口,主要区别在于有密码需要额外传入 ZK proof,即三个二维数组 pa
, pb
, pc
, 且需要调用外部合约 Verifier 的验证方法,进行 ZK 验证。
function claimOrdinaryRedpacket(
bytes32 _id,
bytes32[] memory proof
) public returns (uint claimed) {
//check exist or not
RedPacket storage rp = redpacket_by_id[_id];
require(rp.lock == bytes32(0), "Not ordinary redpacket");
claimed = _claim(_id, proof);
}
function claimPasswordRedpacket(
bytes32 _id,
bytes32[] memory proof,
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC
) public returns (uint claimed) {
RedPacket storage rp = redpacket_by_id[_id];
require(rp.lock != bytes32(0), "Not password redpacket");
uint256[1] memory input;
input[0] = uint256(rp.lock);
require(
verifier.verifyProof(_pA, _pB, _pC, input),
"ZK Verification failed, wrong password"
);
claimed = _claim(_id, proof);
}
claim 内部函数的核心逻辑代码实现:
- 检查红包是否过期
- 检查 Merkle proof
- 根据是否为随机红包,来计算领取金额
- 是
- 如果仅剩一个未领取的红包,则直接将所剩余额都给予最后一位领取者
- 计算随机金额,需要扣除剩余的最低金额,再进行随机
- 否
- 如果仅剩一个未领取的红包,则直接将所剩余额都给予最后一位领取者
- 均分金额
- 是
- 检查用户是否已经领取过该红包
- 转账
- 发送 event
function _claim(
bytes32 _id,
bytes32[] memory proof
) internal returns (uint claimed) {
...
// check expired
require(unbox(packed.packed1, 224, 32) > block.timestamp, "Expired");
...
// check merkle proof
require(
MerkleProof.verify(proof, rp.merkleroot, _leaf(msg.sender)),
"Verification failed, forbidden"
);
...
if (ifrandom == 1) {
if (total_number - claimed_number == 1)
claimed_tokens = remaining_tokens;
else {
// reserve minium amount => (total_number - claimed_number) * 0.1
uint reserve_amount = (total_number - claimed_number) *
minium_value;
uint distribute_tokens = remaining_tokens - reserve_amount;
claimed_tokens =
random(seed, nonce) %
((distribute_tokens * 2) / (total_number - claimed_number));
// minium claimed_tokens for user is 0.1 ; and round the claimed_tokens to decimal 0.1
claimed_tokens = claimed_tokens < minium_value
? minium_value
: (claimed_tokens - (claimed_tokens % minium_value));
}
} else {
if (total_number - claimed_number == 1)
claimed_tokens = remaining_tokens;
else
claimed_tokens =
remaining_tokens /
(total_number - claimed_number);
}
// Penalize greedy attackers by placing duplication check at the very last
require(rp.claimed_list[msg.sender] == 0, "Already claimed");
// save data to redpacket_by_id[id]
// send event
}
如果红包已经过期,且金额还没领取完,创建者可以调用 refund
赎回未领取的金额。
- 检查是否为红包创建者
- 检查是否已过期
- 将剩余的 ETH / ERC20 转给创建者
- 发送 event
function refund(bytes32 id) public {
...
require(creator == msg.sender, "Creator Only");
require(
unbox(packed.packed1, 224, 32) <= block.timestamp,
"Not expired yet"
);
...
if (token_type == 0) {
(bool success, ) = payable(msg.sender).call{
value: remaining_tokens
}("");
require(success, "Refund failed.");
} else if (token_type == 1) {
transfer_token(token_address, msg.sender, remaining_tokens);
}
emit RefundSuccess(id, token_address, remaining_tokens, rp.lock);
}
- 部署红包合约
- 部署 Groth16Verifier 合约(该合约通过 snarkjs 结合电路文件生成,详细过程会在下面讲解)
- 红包合约初始化,将 Groth16Verifier 合约地址传入
- 保存合约地址到 json 文件
import { deployContract, saveDeployment } from "./utils";
export default async function () {
const redPacket = await deployContract("HappyRedPacket");
const redPacketAddress = await redPacket.getAddress();
const groth16Verifier = await deployContract("Groth16Verifier");
const groth16VerifierAddress = await groth16Verifier.getAddress();
console.log("Groth16Verifier address:", groth16VerifierAddress);
let initRecipt = await redPacket.initialize(groth16VerifierAddress);
await initRecipt.wait();
saveDeployment({
Redpacket: redPacketAddress,
Verifier: groth16VerifierAddress,
});
}
- 读取红包合约部署地址,初始化红包合约实例
- 初始化测试 token 实例
- approve 授权操作
- 根据白名单生成 Merkle Tree, 这里使用
merkletreejs
依赖.- 需要将 address 先进行 hash 再作为 Merkle Tree 的叶子节点
- hashlock 字段
- 如果没有设置密码,直接传
BYTES_ZERO
- 如果有设置密码,需要使用
Poseidon Hash
对明文密码进行 hash,作为 hash lock 值
- 如果没有设置密码,直接传
- 创建红包
- 领取红包
import MerkleTree from "merkletreejs";
...
export default async function () {
...
// Initialize contract instance for interaction
const redPacket = new ethers.Contract(
CONTRACT_ADDRESS,
CONTRACT_ABI,
wallet, // Interact with the contract on behalf of this wallet
);
const redPacketAddress = await redPacket.getAddress();
const testToken = new ethers.Contract(
ERC20_ADDRESS,
(await hre.artifacts.readArtifact("SimpleToken")).abi,
wallet,
);
const testTokenAddress = await testToken.getAddress();
// approve...
// white list
const claimerList = [
wallet.address,
"some other address",
];
// generate merkle tree
const merkleTree = new MerkleTree(
claimerList.map((user) => hashToken(user as `0x${string}`)),
keccak256,
{ sortPairs: true },
);
const merkleTreeRoot = merkleTree.getHexRoot();
console.log("merkleTree Root:", merkleTreeRoot);
let message = new Date().getTime().toString();
let lock = ZERO_BYTES32;
if (password) {
lock = await calculatePublicSignals(password);
}
let redpacketID = "";
await createRedpacket();
await cliamRedPacket(wallet);
}
创建红包, 发出创建红包的交易后,会监听交易回执,解析其中的 event 参数,将红包 id 打印出来
async function createRedpacket() {
// create_red_packet
let creationParams = {
_merkleroot: merkleTreeRoot,
_lock: lock,
_number: claimerList.length,
_ifrandom: true,
_duration: 259200,
_message: message,
_name: "cache",
_token_type: 1,
_token_addr: testTokenAddress,
_total_tokens: parseEther("1"),
};
let createRedPacketTx = await redPacket.create_red_packet(
...Object.values(creationParams),
);
const createRedPacketRes = await createRedPacketTx.wait();
const creationEvent = createRedPacketRes.logs.find(
(_log: EventLog) =>
typeof _log.fragment !== "undefined" &&
_log.fragment.name === "CreationSuccess",
);
if (creationEvent) {
const [
total,
id,
...
] = creationEvent.args;
redpacketID = id;
console.log(
`CreationSuccess Event, total: ${total.toString()}\tRedpacketId: ${id} `,
);
console.log(`lock: ${hash_lock}`);
} else {
throw "Can't parse CreationSuccess Event";
}
console.log("Create Red Packet successfully");
}
领取红包,根据是否有密码,调用不同的接口
- 有密码,先生成 zk proof,再调用
claimPasswordRedpacket
- 没有密码,调用
claimOrdinaryRedpacket
// claim
async function cliamRedPacket(user) {
let merkleProof = merkleTree.getHexProof(hashToken(user.address));
const balanceBefore = await testToken.balanceOf(user.address);
let claimTx: any;
if (password) {
const proofRes = await calcProof(password);
if (proofRes) {
const {
proof: { a, b, c },
publicSignals,
} = proofRes;
claimTx = await redPacket
.claimPasswordRedpacket(redpacketID, merkleProof, a, b, c)
.catch((err) => console.error(err));
}
} else {
claimTx = await redPacket.claimOrdinaryRedpacket(redpacketID, merkleProof);
}
const createRedPacketRecipt = await claimTx.wait();
console.log("createRedPacketRecipt", createRedPacketRecipt);
const balanceAfter = await testToken.balanceOf(user.address);
console.log(
`user ${user.address} has claimd ${balanceAfter - balanceBefore}`
);
}
calculatePublicSignals
对明文密码进行 poseidon hash,这里使用 circomlibjs
依赖
import { buildPoseidon } from "circomlibjs";
export const calculatePublicSignals = async (input: string) => {
const poseidon = await buildPoseidon();
const hash = poseidon.F.toString(poseidon([keccak256(toHex(input))]));
return toHex(BigInt(hash), { size: 32 });
};
calcProof
计算 zk proof
- 将密码作为电路的输入
in
字段,依赖 wasm 和 zkey 文件生成 proof - 验证生成的 proof 是否能通过
- 对proof结果进行重排(因为
groth16.exportSolidityCallData
函数输出的结果与vierifer的接口参数顺序不一样)
import { encodePacked, keccak256, parseEther, toHex } from "viem";
import { groth16 } from "snarkjs";
...
export const calcProof = async (input: string) => {
const proveRes = await groth16.fullProve(
{ in: keccak256(toHex(input)) },
path.join(__dirname, "../datahash_js/datahash.wasm"),
path.join(__dirname, "../circuit_final.zkey")
);
const res = await groth16.verify(
Vkey,
proveRes.publicSignals,
proveRes.proof
);
if (res) {
const proof = convertCallData(
await groth16.exportSolidityCallData(
proveRes.proof,
proveRes.publicSignals
)
);
return {
proof: proof,
publicSignals: proveRes.publicSignals,
};
} else {
console.error("calculateProof verify faild.");
return null;
}
};
# snarkjs
npm install -g snarkjs@latest
# circom
# 参考 circom 官网
# https://docs.circom.io/getting-started/installation
# 项目依赖
yarn install
- 生成 ptau 文件。注意:ptau 文件不要公开,如果泄露 ZK 电路会有被攻击的风险,可以在完成 setup 阶段后删除 ptau 文件.
# Start a new powers of tau ceremony
npx snarkjs powersoftau new bn128 14 pot14_0001.ptau -v
# Contribute to the ceremony
npx snarkjs powersoftau contribute pot14_0001.ptau pot14_0002.ptau --name="Second contribution" -v -e="some random text"
# Apply a random beacon
# 数字是 32 bytes 的随机数字
npx snarkjs powersoftau beacon pot14_0002.ptau pot14_beacon.ptau 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 10 -n="Final Beacon" -v
# Prepare phase 2 生成最终的 ptau 文件
# 这一步耗时较长
npx snarkjs powersoftau prepare phase2 pot14_beacon.ptau pot14_final.ptau -v
# 验证 ptau 文件是否有效
npx snarkjs powersoftau verify pot14_final.ptau
- 编写电路
circuits/datahash.circom
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/poseidon.circom";
template PoseidonHasher() {
signal input in;
signal output out;
component poseidon = Poseidon(1);
poseidon.inputs[0] <== in;
out <== poseidon.out;
}
component main = PoseidonHasher();
-
编译电路
- 这一步会在
datahash_js
文件夹下生成三个文件:datahash.wasm
,generate_witness.js
,witness_calculator.js
, 用于后续生成证明 - 以及
datahash.r1cs
和dahash.sym
文件,用于完成 trusted setup 的第二阶段
- 这一步会在
circom circuits/datahash.circom --r1cs --wasm --sym
- trusted setup 第二阶段,生成 zkey 文件
# Groth16 requires a trusted ceremony for each circuit.
npx snarkjs groth16 setup datahash.r1cs pot14_final.ptau circuit_0000.zkey
# Contribute to the phase 2 ceremony
npx snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="1st Contributor Name" -e="some random text" -v
# Verify the latest zkey
npx snarkjs zkey verify datahash.r1cs pot14_final.ptau circuit_final.zkey
# Export the verification key
npx snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
- 建议删除所有 .ptau 文件
trusted setup 完成后,我们需要利用 snarkjs 自动生成 verifier.sol
文件,辅助红包合约进行链上的 zksnark 验证。
npx snarkjs zkey export solidityverifier circuit_final.zkey contracts/redpacket/verifier.sol
npx hardhat compile
npx hardhat test --network hardhat
npx hardhat deploy-zksync --script deploy.ts --network zkSyncSepoliaTestnet
# output
Starting deployment process of "HappyRedPacket"...
Wallet address: 0x67a6Ba1b418911EB67AF4E2DfEF80aCBe2cCE0b6
Estimated deployment cost: 0.0042542753 ETH
"HappyRedPacket" was successfully deployed:
- Contract address: 0x1FB0E4Bd48a09C3569c570B2Cd70200dd84c096F
- Contract source: contracts/redpacket/HappyRedPacket.sol:HappyRedPacket
- Encoded constructor arguments: 0x
Requesting contract verification...
Your verification ID is: 26382
Contract successfully verified on ZKsync block explorer!
Starting deployment process of "Groth16Verifier"...
Wallet address: 0x67a6Ba1b418911EB67AF4E2DfEF80aCBe2cCE0b6
Estimated deployment cost: 0.00083167485 ETH
"Groth16Verifier" was successfully deployed:
- Contract address: 0x3e6b146aBb56c49D7e87f940112971546e761CeB
- Contract source: contracts/redpacket/verifier.sol:Groth16Verifier
- Encoded constructor arguments: 0x
Requesting contract verification...
Your verification ID is: 26383
Contract successfully verified on ZKsync block explorer!
Groth16Verifier address: 0x3e6b146aBb56c49D7e87f940112971546e761CeB
交互脚本
npx hardhat deploy-zksync --script interact.ts --network zkSyncSepoliaTestnet
# output
Running script to interact with contract 0x1FB0E4Bd48a09C3569c570B2Cd70200dd84c096F
Wallet address: 0x67a6Ba1b418911EB67AF4E2DfEF80aCBe2cCE0b6
allowance: 115792089237316195423570985008687907853269984665640564039457.584007913129639935
Redpacket Nonce: 3n
merkleTree Root: 0xa49ea2b86a72603f37d4eee7d52b4b0c85fd23e5605603ad82b7dc10d48fd2c5
CreationSuccess Event, total: 1000000000000000000 RedpacketId: 0x945f7018e48934dc180db1a25b1ef14b2ef2b33eb03f6d5806b7ac084648d8fe
lock: 0x22abfb84e37f8a8623a6486a1158311ed0a25038730b70d9312df8112c4a7e22
Create Red Packet successfully
createRedPacketRecipt ContractTransactionReceipt {
provider: Provider { _contractAddresses: {} },
to: '0x1FB0E4Bd48a09C3569c570B2Cd70200dd84c096F',
from: '0x67a6Ba1b418911EB67AF4E2DfEF80aCBe2cCE0b6',
contractAddress: null,
hash: '0x94a675bda6599a22e93b0125f6149870a63ce5f9be8dcbb909698bfbfc851c38',
index: 0,
blockHash: '0xa79b740ebe25d38b8560bf52e7afad5a523b0b41ae40502525155602253c7da8',
blockNumber: 3819025,
logsBloom: '0x00000000000400000000004000800000000000000000000040000000000000000000010000000000000000000000000000000000000008000000000000000000000000000000040000000008000000000000200000100000000000000000082000000000000000000000000000000000000000000000002000000010000000000000000000000000000004000000000000000000004000000000000000000010000000000000100000000000000000000000000000000000000000000000000400000002008000000000000000200000000000000000000000000000000000000000000000000000000000000000000040000000001000000000000000000000',
gasUsed: 8811086n,
blobGasUsed: undefined,
cumulativeGasUsed: 0n,
gasPrice: 25000000n,
blobGasPrice: undefined,
type: 113,
status: 1,
root: '0xa79b740ebe25d38b8560bf52e7afad5a523b0b41ae40502525155602253c7da8'
}
user 0x67a6Ba1b418911EB67AF4E2DfEF80aCBe2cCE0b6 has claimd 700000000000000000