测试机密行为
本地模拟器无法测试 OXN 的机密性特性——加密 calldata、加密存储、签名查询、机密预编译都要 OXN 运行时。本页覆盖如何构建真正走过那些路径的集成测试。
测试目标
把集成测试指向 OXN 测试网:
RPC URL: https://rpc.bout.network
Chain ID: 186
对——这意味着测试用真实网络调用、真实 gas、水龙头领的真实(测试网)代币。这正是重点——整个机密性栈只在真实网络上存在。
基本集成测试结构
test/confidential.integration.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const CHAIN_ID = 186;
describe("MyToken 机密行为 [integration]", function () {
let token;
let signer;
let rawProvider; // 未 wrap,用于读取原始链上数据
this.timeout(120_000); // 网络调用比本地慢
before(async function () {
// Hardhat 插件自动 wrap 这个 signer 做加密
[signer] = await ethers.getSigners();
const chainId = (await ethers.provider.getNetwork()).chainId;
expect(chainId).to.equal(CHAIN_ID); // 防守跑在本地网络
// 部署
const Token = await ethers.getContractFactory("MyToken");
token = await Token.deploy(ethers.parseUnits("1000000", 18), { gasLimit: 3_000_000n });
await token.waitForDeployment();
// 用于读取未加密链上数据的 raw provider
rawProvider = new ethers.JsonRpcProvider("https://rpc.bout.network");
});
it("transfer 的 calldata 在链上是加密的", async function () {
const recipient = "0x000000000000000000000000000000000000dEaD";
const tx = await token.transfer(recipient, ethers.parseUnits("100", 18),
{ gasLimit: 500_000n });
await tx.wait();
// 从链上取原始交易
const onchain = await rawProvider.getTransaction(tx.hash);
// 加密:tx.data 以 0xa? 开头(CBOR map)。明文:0xa9059cbb(transfer)。
const firstByte = parseInt(onchain.data.slice(2, 4), 16);
expect(firstByte & 0xe0).to.equal(0xa0);
expect(onchain.data.startsWith("0xa9059cbb")).to.equal(false);
});
});
两个要点:
firstByte & 0xe0 === 0xa0断言 是确认加密的可靠方法——检查 tx.data 是不是 CBOR 信封。rawProvider是未 wrap 的 ethers provider——你需要它才能看到原始链上数据。用ethers.provider(wrapped 的),拿到的是解密后的数据,断言就没意义了。
测试签名查询
签名查询在 view 函数中填充 msg.sender。验证模式:
it("myBalance() 通过签名查询返回调用者余额", async function () {
const [caller, other] = await ethers.getSigners();
// 部署一个把余额分配给特定账户的代币
const token = /* ... */;
// 用调用者的 signer wrap 合约——这会触发签名查询
const asCaller = token.connect(caller);
const balance = await asCaller.myBalance();
expect(balance).to.equal(expectedCallerBalance);
// 同一个调用,不同 signer
const asOther = token.connect(other);
const otherBalance = await asOther.myBalance();
expect(otherBalance).to.equal(0); // other 没有代币
});
关键动作是 token.connect(signer)——传 signer(不是 provider)告诉 SDK 发签名查询。
测试日志只有调用者能解
it("emit 的事件只有调用者能解密", async function () {
const [caller, other] = await ethers.getSigners();
const tx = await token.connect(caller).transfer(other.address, 100, { gasLimit: 500_000n });
const receipt = await tx.wait();
// 调用者的 signer 能解密这次调用产生的事件
const decodedByCaller = receipt.logs
.map((log) => { try { return token.interface.parseLog(log); } catch { return null; } })
.filter(Boolean);
expect(decodedByCaller.length).to.be.greaterThan(0);
// Raw provider 读同一笔交易看到的是密文
const rawReceipt = await rawProvider.getTransactionReceipt(tx.hash);
for (const log of rawReceipt.logs) {
// topic 与 data 是密文字节,不是标准 Transfer 签名
expect(log.topics[0]).to.not.equal(ethers.id("Transfer(address,address,uint256)"));
}
});
CI 里的测试网账户管理
测试网集成测试需要有余额的账户。选项:
- 一次性资助一组账户,重用。 私钥作为 CI 加密 secret 存储。
- 资助一个"管理"账户,按需给测试账户打钱。 管理账户持有余额;每次测试从中给新账户少量转账。
- CI 里用水龙头。 水龙头不是为程序化设计的;如果有合理 CI 需求通过支持渠道联系团队。
不要提交私钥,即便是测试网账户——账户可能随时间跨环境重用,或误用到主网。
测试 gas 成本
因为 OXN 上 gas 是显式设置的,你不能像以太坊那样通过估算在测试里断言精确 gas 值。用宽松的固定 gasLimit 测试并断言交易成功;单独用 receipt.gasUsed 记录观察到的成本做回归追踪,但不要因为小的波动挂 build。
常见集成测试失败
"测试挂住然后超时。" RPC 端点或水龙头可能负载高。重试,或查状态页。
"用了 wrapped signer 但 tx.data 还是明文。" 检查 Hardhat 配置里 require("@oasisprotocol/sapphire-hardhat") 在 require("@nomicfoundation/hardhat-toolbox") 之后。顺序重要。
"myBalance() 对所有账户返回 0。" 你发的是明文 eth_call,不是签名查询。确保 Contract 是用 signer 构造的,不是 provider。