访问控制模式
机密性不能替代访问控制。它是一个独立的层,把状态对未授权的链上读者隐藏——但在合约内部,你依然要决定哪些调用方能读、哪些能写、能读写什么。
好消息:你已经熟悉的标准 EVM 工具在 OXN 上工作方式完全一致。本页列出常用模式。
msg.sender 是可信的
改状态交易中的 msg.sender 是交易签名者,由链层验证。任何人都无法伪造它。这是以太坊的保证,OXN 保留它。
在通过签名 查询触发的 view 函数中,msg.sender 是签名这次查询的地址。同样不可伪造。见签名查询。
在通过明文或未认证 eth_call 触发的 view 函数中,msg.sender 是 0x0。不要用它做授权判断。
模式:仅 owner 可写
标准 Solidity。没有 OXN 特有的东西。
contract Vault {
address public owner = msg.sender;
mapping(address => uint256) private balances;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setBalance(address who, uint256 amount) external onlyOwner {
balances[who] = amount;
}
}
因为改状态交易必须签名、msg.sender 被校验,此模式在 OXN 上与在以太坊上完全一致。
模式:每用户私密读
用户只能读自己的状态。这是机密 ERC-20 余额、私密订单、私密投票等的旗舰模式。
contract PrivateToken {
mapping(address => uint256) private balances;
function myBalance() external view returns (uint256) {
return balances[msg.sender];
}
function balanceOf(address who) external view returns (uint256) {
require(msg.sender == who, "not authorized");
return balances[who];
}
}
调用者必须以签名查询的方式发起,msg.sender 才会被填充。见签名查询。
模式:委托读(授权)
合约 owner 授权特定读者也能看到某些状态。
mapping(address => uint256) private balances;
mapping(address => mapping(address => bool)) private readApprovals;
function approveReader(address reader) external {
readApprovals[msg.sender][reader] = true;
}
function readBalance(address owner) external view returns (uint256) {
require(msg.sender == owner || readApprovals[owner][msg.sender], "no approval");
return balances[owner];
}
两个分支——自读和授权读——都需要签名查询。
模式:基于角色的访问控制(OpenZeppelin)
OpenZeppelin 的 AccessControl 无需修改可用。结合 private 存储做机密的角色门控数据:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract AuditableSecret is AccessControl {
bytes32 public constant AUDITOR_ROLE = keccak256("AUDITOR_ROLE");
string private secretData;
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function readSecret() external view returns (string memory) {
require(hasRole(AUDITOR_ROLE, msg.sender), "not auditor");
return secretData;
}
}
审计员通过签名查询读取;他人无法解密结果。
模式:定时揭示
状态以加密形式存储,仅在截止时间后揭示。密封拍卖的典型场景。
struct Bid {
uint256 amount;
bool revealed;
}
mapping(address => Bid) private bids;
uint256 public revealDeadline;
function submitBid(uint256 amount) external {
require(block.timestamp < revealDeadline, "bidding closed");
bids[msg.sender] = Bid(amount, false);
}
function reveal() external view returns (uint256) {
require(block.timestamp >= revealDeadline, "not yet");
return bids[msg.sender].amount;
}
function winner() external view returns (address, uint256) {
require(block.timestamp >= revealDeadline, "not yet");
// 遍历 bids 返回赢家
// ...
}
revealDeadline 之前,submitBid 不返回自己的出价、reveal 会 revert,因此没人能拿到出价明文。截止后,调用者可以通过签名查询读自己的出价,合约也可以公开揭示赢家。
模式:加密的每用户机密
存储任意用户机密——会话 token、API key、密码学材料。
mapping(address => bytes) private secrets;
function storeSecret(bytes calldata secret) external {
secrets[msg.sender] = secret;
}
function getSecret() external view returns (bytes memory) {
return secrets[msg.sender];
}
storeSecret 调用必须是加密交易(secret 在 calldata 里)。getSecret 调用必须是签名查询。这样机密加密进存储、加密着待着、只加密回给它的 owner。
什么不能当作替代品
不要用 tx.origin 做授权。 原因与以太坊一致——容易被钓鱼合约利用。机密性不能修复这个。
不要用自动生成的 public getter 暴露私密状态。 public 关键字生成的是明文无认证 getter。安全替代见加密合约存储。
不要实现"任何签名都通过"的访问控制。 访问控制必须校验是哪个地址签的,而不是"存在某个签名"。