跳到主要内容

机密性陷阱

OXN 加密你合约状态与调用的内容。它不会自动让你 dApp 的一切都私密。本页列出真实应用即便加密仍可能泄漏信息的方式。

Gas 侧信道

如果合约 Gas 使用量随秘密的值变化,这种变化是可观察的,会泄漏这个值。

示例。 按转账金额分档收费的机密 ERC-20:

function transfer(address to, uint256 amount) external {
// 错:费用计算的 gas 依赖 amount
uint256 fee;
if (amount > 1_000_000) { fee = amount / 100; } // ~2000 gas
else if (amount > 10_000) { fee = amount / 200; } // ~2200 gas
else { fee = amount / 500; } // ~1900 gas
// ...
}

观察者测 gasUsed 推断走了哪个分支,从而暴露 amount 的大致范围。

缓解。 每个分支都算,最后选:

uint256 feeA = amount / 100;
uint256 feeB = amount / 200;
uint256 feeC = amount / 500;
uint256 fee = amount > 1_000_000 ? feeA : (amount > 10_000 ? feeB : feeC);

三目运算符编译成 MSTORE + 选择,是常数时间。不完美(SLOAD 计数仍会变),但接近多了。

存储布局泄漏

每个槽地址都是公开的。 你合约写和读的是哪些槽,任何 dump 存储 trie 的人都能看到。

示例。 私密的用户标志 mapping:

mapping(address => bool) private hasClaimed;

function claim() external {
require(!hasClaimed[msg.sender], "already claimed");
hasClaimed[msg.sender] = true;
// ... 给用户奖励
}

hasClaimed[user] 的槽在 keccak256(user . position)。claim 之后它出现在存储 trie 中(不管加密值是什么)就暴露了 user 已经 claim 了。能访问原始存储 dump 的观察者只需检查哪些槽存在就能枚举出所有已 claim 的用户。

缓解。 要么接受"谁调用"公开(tx.from 反正也泄漏),要么用 oblivious 存储模式(每次调用写每一个用户的槽)。Oblivious 模式贵;只在泄漏真的要紧时用。

事件数量泄漏

交易 emit 了多少事件是公开的。 事件数据加密,但数量不加密——数 receipt 里的 logs

如果合约逻辑在路径 A emit N 个事件、路径 B emit M 个事件,数量就暴露路径。

缓解。 每条路径 emit 同样数量的事件——必要时用无操作事件填充。

元数据分析

谁在什么时候调用了什么合约是公开的。 即便内容全加密,交易图显示:

  • 发送方与接收方地址。
  • 调用了哪个合约。
  • 区块与时间戳。
  • 付了多少 gas。

如果只有你在某个特定时刻调用某个特定合约,那个模式与加密无关地暴露上下文。

缓解。 对交易图隐私敏感的场景,在 OXN 之上叠加混币器或中继。基础机密性不包含这个。

时间侧信道

交易入块时间公开。 一个基于调用时间行为不同的合约(例如"截止后拍卖揭示更严格")通过时间泄漏调用者的意图。

缓解。 同上——时间是链上执行的内在。若敏感,要么别把敏感逻辑放链上,要么接受时间会泄漏粗粒度信息。

编译与代码作为预言机

合约字节码公开。 任何人都可以读你的 Solidity(验证之后)或反编译字节码。你的控制流、存储布局、常量值全部可见。

缓解。 不要把安全关键逻辑藏在字节码里。假设字节码公开,据此设计。

部署历史

合约部署交易公开。 部署时传的构造参数永久可见于部署交易的 calldata。永远不要把秘密作为构造参数。

用部署后的加密初始化调用存机密:

// 构造函数:只放公开数据
constructor(address _owner) { owner = _owner; }

// 部署后:机密通过加密调用
function setSecret(bytes32 _secret) external {
require(msg.sender == owner);
secret = _secret; // calldata 加密、存储加密
}

跨合约模式

机密合约用明文参数调另一合约时,那些参数变公开。例如:

function pay(address recipient, uint256 amount) external {
// 如果 amount 本应私密,这里就错了
token.transfer(recipient, amount); // amount 在子调用中可见
}

子调用参数是否加密取决于你调用的目标合约与调用方式(机密合约会加密,公开合约不加密)。

缓解。 弄清楚你调用的合约是不是机密的。参数敏感时把调用路由通过机密合约。

对这一切该做什么

  • 给你的应用做威胁建模。 到底什么需要私密?余额?参与者?金额?投票选择?
  • 围绕泄漏面设计。 对每个想藏的东西问"加密够不够?还是存储 / gas / 时间 / 元数据会泄漏它?"
  • 偏向机密友好模式。 基于 approvals 的读(见访问控制模式)比公开 getter 安全。
  • 审计。 懂 TEE 机密性的安全审计值这个钱。

下一步