跳到主要内容

访问私有数据

当我们开始编写智能合约并接触到 public、private 等可见性修饰词时,我们可能会认为,如果你想让某些变量的值可以被公众读取,你需要声明它是公开的,而私有变量除了智能合约本身,任何人都不能读取。

但是,Ethereum 是一个公共区块链。那么,私有数据到底是什么意思?

在这一层次中,我们将看到你如何从任何智能合约中实际读取私有变量的值,同时澄清私有实际上代表什么--这绝对不是私有数据!

私有是什么意思?

函数(和变量)可见性修改器只影响函数的可见性--并不阻止对其值的访问。我们知道,公共函数是那些既可以由用户和智能合约在外部调用,也可以由智能合约本身调用的函数。

同样,内部函数是那些只能由智能合约本身调用的函数,外部用户和智能合约不能调用这些函数。外部函数则相反,它们只能由外部用户和智能合约调用,但不能由拥有该函数的智能合约本身调用。

私有的,类似的,只是影响到谁可以调用该函数。私有的和内部的行为基本类似,只是内部函数也可以被派生合约调用,而私有函数则不可以。

因此,举例来说,如果合约 A 有一个标记为内部的函数 f(),第二个继承合约 A 的合约 B 就会像这样

contract B is A {
...
}

仍然可以调用 f()。

然而,如果合约 A 有一个标记为私有的函数 g(),合约 B 就不能调用 g(),即使它继承了合约 A。

变量也是如此,因为变量基本上只是函数。私有变量只能由智能合约本身访问和修改,甚至派生合约也不能。然而,这并不意味着外部各方不能读取该值。

BUIDL

我们将建立一个简单的合约,以及一个 Hardhat 测试,来证明这一点。我们的合约将尝试将数据存储在私有变量中,希望没有人能够读取它的值。

该合约将在其构造函数中输入密码和用户名,并将它们存储在私有变量中。

用户将能够以某种方式访问这些私有变量。

概念

为了理解这一点,请回顾一下以太坊存储和执行层面,Solidity 中的变量被存储在 32 字节(256 位)的存储槽中,数据根据这些变量的声明顺序,按顺序存储在这些存储槽中。

存储也被优化了,如果一堆变量可以放在一个槽里,它们就会被放在同一个槽里。这被称为变量打包,我们将在后面进一步了解这个问题。

注意 所有这些命令都应该能顺利工作。 如果你在 windows 上,并面临无法读取 null 的属性(读取'pickAlgorithm')等错误,请尝试使用 npm cache clear --force 来清除 NPM 缓存。

新建项目

要建立一个 Hardhat 项目,打开终端并执行这些命令

npm init --yes
npm install --save-dev hardhat
# windows 电脑
# npm install --save-dev @nomicfoundation/hardhat-toolbox @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
npx hardhat

选择创建一个基本样本项目 对已经指定的 Hardhat 项目根按回车键 在是否要添加.gitignore 的问题上按回车键 在 "是否要用 npm 安装这个样本项目的依赖项(@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? 现在你已经有一个准备好的 hardhat 项目了!

创建合约 Login.sol 文件

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Login {

// Private variables
// Each bytes32 variable would occupy one slot
// because bytes32 variable has 256 bits(32*8)
// which is the size of one slot

// Slot 0
bytes32 private username;
// Slot 1
bytes32 private password;

constructor(bytes32 _username, bytes32 _password) {
username = _username;
password = _password;
}
}

由于这两个声明的变量都是 bytes32 变量,我们知道每个变量正好占用一个存储槽。由于顺序很重要,我们知道用户名将占用 0 号槽,密码将占用 1 号槽。

因此,与其试图通过调用合约来读取这些变量值,这是不可能的,我们可以直接访问存储槽。由于 Ethereum 是一个公共区块链,所有节点都可以访问所有的状态。

让我们写一个 Hardhat 测试来演示这个功能。

在测试文件夹内创建一个新文件 attack.js,并添加以下几行代码

const { ethers } = require("hardhat");
const { expect } = require("chai");

describe("Attack", function () {
it("Should be able to read the private variables password and username", async function () {
// Deploy the login contract
const loginFactory = await ethers.getContractFactory("Login");

// To save space, we would convert the string to bytes32 array
const usernameBytes = ethers.utils.formatBytes32String("test");
const passwordBytes = ethers.utils.formatBytes32String("password");

const loginContract = await loginFactory.deploy(
usernameBytes,
passwordBytes
);
await loginContract.deployed();

// Get the storage at storage slot 0,1
const slot0Bytes = await ethers.provider.getStorageAt(
loginContract.address,
0
);
const slot1Bytes = await ethers.provider.getStorageAt(
loginContract.address,
1
);

// We are able to extract the values of the private variables
expect(ethers.utils.parseBytes32String(slot0Bytes)).to.equal("test");
expect(ethers.utils.parseBytes32String(slot1Bytes)).to.equal("password");
});
});

在这个测试中,我们首先创建了 usernameBytes 和 passwordBytes,它们是一个短字符串的字节 32 版本,作为我们的用户名和密码。然后我们用这些值部署 Login 合约。

合约部署后,我们使用 provider.getStorageAt 直接读取 loginContract.address 的 0 和 1 槽的存储槽值,并从中提取字节值。

然后,我们可以比较检索到的值--slot0Bytes 与 usernameBytes,slot1Bytes 与 passwordBytes,以确保它们事实上是相等的。

如果测试通过,这意味着我们能够成功地直接读取私有变量的值,而根本不需要调用合约上的函数。

最后,让我们运行这个测试,看看它是否有效。在你的终端上输入。

npx hardhat test

测试应该通过。哇,我们真的可以读取密码了

预防

永远不要在公共区块链上存储私人信息。没有其他办法了。

参考

  • 017_PrivateData