{"content":{"title":"Solidity 中的私有变量不私有","body":"## 0x01 看下面被极度简化过的合约代码\r\n```\r\n// SPDX-License-Identifier: MIT\r\n\r\npragma solidity =0.8.19;\r\n\r\ncontract Auth {\r\n\r\n    string private secret;\r\n\r\n    constructor(string memory secret_) {\r\n        secret = secret_;\r\n    }\r\n}\r\n```\r\n这个代码里声明了一个私有状态变量 secret，部署合约的时候我往里面传了一个值，这个变量的值是可以被读到的么？\r\n\r\n## 0x02 玻璃罩子\r\n有时候感觉智能合约就像是放在区块链这个公开透明的玻璃罩子里面，对这个玻璃罩子外面的人来说，里面是没有任何隐私可言的，不管智能合约的状态变量是 private 的还是 public 的，我们都可以很轻松的读取里面的值。\r\n\r\n这里的 private 只对同在这个罩子里的其它智能合约起作用，也就是说，如果一个状态变量声明为 private, 其它智能合约是不能读取这个值的，目前的 SLOAD 指令只能读取当前智能合约的值。\r\n\r\n## 0x03 如何读取\r\n其实我们有不止一种办法来读取到私有状态变量 secret。\r\n最常见的，我们可以使用 getStorageAt，我已经把这个简单合约部署到 BSC 测试网上，地址为 0x4332401C3Ea3aeebF9813dFA3Fe3Ee581ef8572d\r\n下面是我的操作步骤：\r\n1. 连上节点\r\n```\r\n> geth attach https://rpc.ankr.com/bsc_testnet_chapel\r\nWelcome to the Geth JavaScript console!\r\n\r\ninstance: erigon/2.39.0/linux-amd64/go1.19.3\r\nat block: 28080565 (Thu Mar 16 2023 09:19:09 GMT+0800 (CST))\r\n modules: debug:1.0 erigon:1.0 eth:1.0 net:1.0 rpc:1.0 trace:1.0 txpool:1.0 web3:1.0\r\n\r\nTo exit, press ctrl-d\r\n```\r\n2. 调用 eth.getStorageAt\r\n```\r\n> eth.getStorageAt(\"0x4332401C3Ea3aeebF9813dFA3Fe3Ee581ef8572d\",0)\r\n\"0x2431303000000000000000000000000000000000000000000000000000000008\"\r\n```\r\n\"0x2431303000000000000000000000000000000000000000000000000000000008\" 就是私有状态变量 secret 的值，因为这个变量是字符串类型，可以使用工具 https://codebeautify.org/hex-string-converter 进行数据转换，把16进制数字转换为一个字符串。\r\n\r\n细心一点儿的话，可以会注意到我们获取的数据最后面多了一个数字 8，这是因为字符串本质上是变长数组，需要一些特别处理，这里有更详细的说明：https://docs.soliditylang.org/en/v0.8.10/internals/layout_in_storage.html#mappings-and-dynamic-arrays\r\n\r\ngetStorageAt 需要传入两个参数，第一个参数是合约地址，第二个参数是要读取的状态变量的存储位置，只要我们知道的变量的位置，就能读取到所存储的值。不过很多时候计算存储位置也是要费点儿功夫的。\r\n\r\n其实有时候可能另一种办法更简单，就是直接从设置数据的交易里来读取数据，比如 secret 这个变量是通过构造函数还设置的，那我去看部署合约的交易就好了。连上节点后，通过调用 eth.getTransaction，可以获取到交易的 input 值。\r\n```\r\n> eth.getTransaction(\"0xfa73be19403e5361c97d86dc64f25b2e45af330780e301b213107bdbc611c4a2\")\r\n......\r\ninput: \"0x60806040523480156100......05050565b603f806105076000396000f3fe6080604052600080fdfea2646970667358221220b8f76d5e70f478ae42c73d84dfdbe6c705c91a0db013260e4a6d0b5cdd4b620a64736f6c63430008130033000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000042431303000000000000000000000000000000000000000000000000000000000\"\r\n......\r\n```\r\n合约部署交易 input 值的最后一段 “42431303000000000000000000000000000000000000000000000000000000000” 就是要设置的 secret 相关内容，“24313030” 是 secret 参数的值，前面多出来的 \"4\" 代表的是参数的长度 4 个字节。不过这个本质上是读取参数的值，而不是变量的值，参数的值和变量的值有时候是相同，有时候是不相同的。\r\n\r\n## 0x04  如何在合约中保存秘密\r\n很多时候这需要比较严密的设计，比如我提前给你准备了一个大礼包，这个礼包里放了一大笔 ETH 资产，但只有我把密码告诉你之后你才能使用密码把这个大礼包打开。这个时候，我们把密码直接放合约肯定不行，那么把密码的哈希值放合约里行么？其实也不行，如果不对地址进行校验，当我把密码告诉你之后，很多人都有可能跑在你前面把钱取走，那么多抢跑机器人都在那里蹲着呢。\r\n下面是应对这种场景的简单代码，同时对密码和账户地址进行了校验。\r\n```\r\n// SPDX-License-Identifier: MIT\r\n\r\npragma solidity =0.8.19;\r\n\r\ncontract Gift {\r\n\r\n    bytes32 private secretHash;\r\n    address private owner;\r\n\r\n    constructor(bytes32 secretHash_, address owner_) {\r\n        secretHash = secretHash_;\r\n        owner = owner_;\r\n    }\r\n\r\n    function withdraw(string calldata secret) external {\r\n        require(keccak256(abi.encodePacked(secret)) == secretHash);\r\n        require(msg.sender == owner);\r\n        payable(owner).transfer(address(this).balance);\r\n    }\r\n\r\n    receive() external payable{}\r\n}\r\n```\r\n## 0x05 启发\r\n其实还有很多其它场景，比如口令红包啥的，都有通过智能合约对某个秘密进行校验的需求，要时刻谨记，智能合约里没有秘密。"},"author":{"user":"https://learnblockchain.cn/people/29","address":null},"history":null,"timestamp":1678932642,"version":1}