我们有什么?#
- 测试网创世 https://github.com/Uniswap/unichain-node/raw/main/chainconfig/sepolia/genesis-l2.json
- 一个可用的 L2 RPC https://mainnet-readonly.unichain.org
- blockscout 浏览器 https://unichain.blockscout.com
我们想要实现什么?#
一个可用的 unichain RPC 节点,在本文中,我们只关注如何猜测正确的 genesis-l2.json 文件,使得区块 0 具有正确的 blockhash=0x3425162ddf41a0a1f0106d67b71828c9a9577e6ddeb94e4f33d2cde1fdc3befe
步骤#
1. 修复区块数据#
首先,让我们获取区块并检查差异
下载测试网创世,将其命名为 genesis-l2.json.bak
import os
os.environ["RPC"] = "https://mainnet-readonly.unichain.org"
from simplebase import *
b=eth_getBlockByNumber(RPC, 0, True)
g=json.load(open("genesis-l2.json.bak"))
>>> pprint({i:j for i,j in b.items() if i in g and b[i]!=g[i]})
{'nonce': '0x0000000000000000', 'timestamp': '0x67291fc7'}
修复上述两个差异,并注意 config.chainId
也应更改为 130(主网 chainId)。
2. 修复已知合约代码和数据#
现在让我们修复 alloc
,观察 alloc 结构:
"420000000000000000000000000000000000002f": {
"code": "0x60806040526004...",
"storage": {
"0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x0000000000000000000000004200000000000000000000000000000000000018"
},
"balance": "0x0"
},
关键是合约地址没有 0x 前缀,值包含代码、存储字典、余额和 nonce。
我猜测余额和 nonce 不会不同,因此查询存储和代码,找到不匹配的并替换:
import os
os.environ["RPC"] = "https://mainnet-readonly.unichain.org"
from simplebase import *
def cached_simple_rpccall(rpc, method, params, cacheprefix=""):
cachekey = hashlib.sha256(f"{rpc}_{method}_{json.dumps(params)}".encode()).hexdigest()
cachefile = f"__pycache__/cached_simple_rpccall{cacheprefix}_{cachekey}"
if os.path.isfile(cachefile):
return json.load(open(cachefile))
res = simple_rpccall(rpc, method, params)
open(cachefile, "w").write(json.dumps(res))
return res
if 1:
g = json.load(open("genesis-l2.json.bak"))
known = set()
for addr,v in g["alloc"].items():
addr = "0x"+addr
cache = "-".join([v[i] for i in sorted(v.keys()) if i!="storage"])
if "storage" in v:
cache += json.dumps(v["storage"])
if cache in known:
continue
known.add(cache)
#print(v.keys()) code,balance,storage,nonce
if "code" in v:
#realcode = eth_getCode(RPC, addr)
realcode = cached_simple_rpccall(RPC, "eth_getCode", [addr, "0x0"])
if v["code"]==realcode:
print("ok code", addr)
else:
print("ERROR code:", addr, len(v["code"]), "=>", len(realcode))
g["alloc"][addr[2:]]["code"] = realcode
if cache in known:
known.remove(cache)
if "storage" in v:
for s_slot, s_value in v["storage"].items():
#realvalue = eth_getStorageAt(RPC, addr, s_slot, height=0)
realvalue = int(cached_simple_rpccall(RPC, "eth_getStorageAt", [addr, s_slot, "0x0"]), 16)
if realvalue==int(s_value, 16):
print("ok storage", addr, s_slot)
else:
print("ERROR storage:", addr, s_slot, int(s_value, 16), "=>", realvalue)
g["alloc"][addr[2:]]["storage"][s_slot] = "%064x"%(realvalue)
if cache in known:
known.remove(cache)
print(len(known))
open("genesis-l2.json", "w").write(json.dumps(g))
由于测试网和主网都使用 opstack,因此大多数合约应该具有相同的地址、代码和存储。因此我们采用缓存机制,如果许多合约在测试网创世中具有完全相同的值,它们在主网中也可能具有相同的值。
3. 查找缺失的合约#
在运行上述代码后,我们发现两个合约出现在测试网中,但主网没有代码或存储,因此这两个键应该被移除:
- 0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f
- 0x1f98431c8ad98523631ae4a59f267346ea31f984
搜索这些地址显示它们分别是 UniV2Factory 和 UniV3Factory。
查看浏览器验证合约页面( https://unichain.blockscout.com/verified-contracts ),我们发现一个具有唯一合约地址的 UniV2Factory 合约:https://unichain.blockscout.com/address/0x1F98400000000000000000000000000000000002?tab=contract
因此,我们受到启发检查邻近合约地址,发现这些合约也已预部署:
0x1F98400000000000000000000000000000000002
0x1F98400000000000000000000000000000000003
0x1F98400000000000000000000000000000000004
后两个尚未验证,但从发出的事件中,我们可以猜测它是 UniV3 和 UniV4 合约,因此让我们将这三个地址添加到我们的 genesis-l2.json.bak 中。
存储槽编号可以通过合约源代码分析、主网部署的调试跟踪,或简单地通过 eth_getStorageAt 调用槽 0~10 来猜测。
"1f98400000000000000000000000000000000002": {
"code": "0x",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000009b64f6e1d60032f5515bd167346cfcd2162ee73a"
},
"balance": "0x0"
},
"1f98400000000000000000000000000000000003": {
"code": "0x",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000009b64f6e1d60032f5515bd167346cfcd2162ee73a",
"0x083fc81be30b6287dea23aa60f8ffaf268f507cdeac82ed9644e506b59c54ff0": "0x0000000000000000000000000000000000000000000000000000000000000001",
"0x72dffa9b822156d9cf4b0090fa0b656bcb9cc2b2c60eb6acfc20a34f54b31743": "0x000000000000000000000000000000000000000000000000000000000000003c",
"0x8cc740d51daa94ff54f33bd779c2d20149f524c340519b49181be5a08615f829": "0x00000000000000000000000000000000000000000000000000000000000000c8",
"0xfb8cf1d12598d1a039dd1d106665851a96aadf67d0d9ed76fceea282119208b7": "0x000000000000000000000000000000000000000000000000000000000000000a"
},
"balance": "0x0"
},
"1f98400000000000000000000000000000000004": {
"code": "0x",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000009b64f6e1d60032f5515bd167346cfcd2162ee73a"
},
"balance": "0x0"
},
详细值并不重要,上述代码将从 RPC 获取它们并替换为正确的值。
经过这些步骤后,我们仍然得到错误的 blockhash。
4. eth_getProof 确认存储正确性#
我们如何知道创世区块的更多细节?debug_traceBlock
RPC 方法确实支持创世区块。
检查所有可用的 RPC 方法,我们可以找到这个 eth_getProof,它可以输出 accountProof、余额、codeHash、nonce、storageHash 和 storageProof。
通过迭代所有已知地址调用 eth_getProof,我们可以发现所有已知地址具有正确的余额、codeHash、nonce、storageHash,但错误的 accountProof,这意味着创世中仍然缺少合约。
5. 继续搜索缺失的合约#
检查 blockscout 提供的所有 API,我们可以找到这个 获取合约列表,它可以返回所有合约地址:
https://unichain.blockscout.com/api?module=contract&action=listcontracts&offset=10000
通过调用 eth_getCode 使用 0 区块号过滤此输出,我们发现 4 个缺失的地址:
- 0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30001
- 0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30002
- 0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30003
- 0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30004
使用简单的 eth_getStorageAt 查找前 200 个槽,我们成功找到了前 3 个的正确存储值,但最后一个仍然错误,即使它的前 200 个槽都是零。
g = json.load(open("api?module=contract&action=listcontracts&offset=10000"))
for addr in ["0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30001", "0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30002", "0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30003", "0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30004"]:
if not addr.startswith("0x"):
addr = "0x"+addr
addr = addr.lower()
truth = cached_simple_rpccall(RPC, "eth_getProof", [addr, [], "0x0"])
our = cached_simple_rpccall(OUR_PRC, "eth_getProof", [addr, [], "0x0"], cacheprefix="test2")
#[i==our['accountProof'][idx] for idx,i in enumerate(truth['accountProof'])]
#if not all([truth['balance']==our['balance'], truth['codeHash']==our['codeHash'], truth['nonce']==our['nonce'], truth['storageHash']==our['storageHash']]):
if 1:
print(truth['balance'], truth['codeHash'], truth["nonce"], truth["storageHash"])
print(addr, truth['balance']==our['balance'], truth['codeHash']==our['codeHash'], truth['nonce']==our['nonce'], truth['storageHash']==our['storageHash'], )
现在我们使用 eth_getProof 指定槽,并发现该合约确实有存储,因为空合约不会返回 storageProof。
6. 反向工程合约#
使用 dedaub 反编译 0x4300c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30004
,并手动格式化:
// 由 library.dedaub.com 反编译
// 2024.11.17 14:07 UTC
// 使用 Solidity 编译器版本 0.8.26 编译
// 从存储指令的使用中推断的数据结构和变量
uint256 _receive; // STORAGE[0x0]
mapping (address => uint256) storage1; // STORAGE[0x1]
mapping (address => uint256) _earnedFees; // STORAGE[0x2]
mapping (address => struct_313) _balanceOf; // STORAGE[0x3]
struct struct_313 { address addr; uint256 amount; };
// 事件
Withdrawn(address, address, uint256);
function 0x396cb597(address varg0) public nonPayable {
require(msg.data.length - 4 >= 32);
return _balanceOf[varg0].addr;
}
function balanceOf(address account) public nonPayable {
require(msg.data.length - 4 >= 32);
return _balanceOf[account].amount;
}
function 0xc13886a4(address varg0, address varg1) public nonPayable {
require(_balanceOf[varg0].addr == msg.sender, Unauthorized());
_balanceOf[varg0].addr = varg1;
emit 0xf0476b1621059e9b7a94b31f4699ab07974f87c8e31db4cb9ad1f9892eb9b169(varg0, _balanceOf[varg0].addr, varg1);
}
function 0xc6743555(address contract1, address contract2, address owner2, uint256 amt) public nonPayable {
require(!_balanceOf[contract2].addr);
_balanceOf[contract2].addr = owner2;
_balanceOf[contract2].amount = 0;
emit 0xf0476b1621059e9b7a94b31f4699ab07974f87c8e31db4cb9ad1f9892eb9b169(contract2, 0, owner2);
0x6de(amt, contract2, contract1);
//require(msg.sender == _balanceOf[contract1].addr, Unauthorized());
}
function recipients(address varg0) public nonPayable {
return _balanceOf[varg0].addr, _balanceOf[varg0].amount;
}
function earnedFees(address addr) public nonPayable {
return _earnedFees[addr] + (_balanceOf[addr].amount * (_receive - storage1[addr])) /1e30;
}
function receive() public payable {
_receive += msg.value * 1e26;
}
function 0x6de(uint256 varg0, uint256 varg1, uint256 varg2) private {
require(msg.sender == _balanceOf[address(varg2)].addr, Unauthorized());
require(address(varg1));
require(0 - varg0);
require(_balanceOf[address(varg2)].amount >= varg0, InsufficientAllocation());
updateState(varg2);
updateState(varg1);
v0 = _SafeSub(_balanceOf[address(varg2)].amount, varg0);
_balanceOf[address(varg2)].amount = v0;
v1 = _SafeAdd(_balanceOf[address(varg1)].amount, varg0);
_balanceOf[address(varg1)].amount = v1;
emit 0x4c6e7131fb69f3c2cc88b05b76a7aa4809429fefa284dda9cf14884d25e3742b(msg.sender, address(varg2), address(varg1), varg0);
return ;
}
function updateState(addr) private {
_earnedFees[addr] += (_receive - storage1[addr])* _balanceOf[addr].amount /1e30 ;
storage1[addr] = _receive;
}
function 0x976(address varg0) private {
v0 = _SafeSub(_receive, storage1[varg0]);
v1 = _SafeMul(_balanceOf[varg0].amount, v0);
v2 = _SafeDiv(v1, 10 ** 30);
return v2;
}
function transferAllocation(address varg0, address varg1, uint256 amt) public nonPayable {
require(_balanceOf[varg1].addr);
//0x6de(uint256 varg0, uint256 varg1, uint256 varg2) 0x6de(varg2, varg1, varg0);
require(msg.sender == _balanceOf[varg0].addr, Unauthorized());
require(_balanceOf[varg0].amount >= amt, InsufficientAllocation());
updateState(varg0);
updateState(varg1);
_balanceOf[varg0].amount -= amt;
_balanceOf[address(varg1)].amount += amt;
emit 0x4c6e7131fb69f3c2cc88b05b76a7aa4809429fefa284dda9cf14884d25e3742b(msg.sender, varg0, address(varg1), amt);
}
function withdrawFees(address account_) public nonPayable {
updateState(msg.sender);
if (_earnedFees[msg.sender]) {
_earnedFees[msg.sender] = 0;
v0, /* uint256 */ v1 = account_.call().value(_earnedFees[msg.sender]).gas(msg.gas);
require(v0, WithdrawalFailed());
}
emit Withdrawn(msg.sender, account_, _earnedFees[msg.sender]);
return _earnedFees[msg.sender];
}
// 注意:原始 Solidity 代码中未包含函数选择器。
// 但是,为了完整性,我们显示它。
function __function_selector__() private {
MEM[64] = 128;
if (msg.data.length < 4) {
require(!msg.data.length);
receive();
} else if (0xc13886a4 > msg.data[0] >> 224) {
if (0x24e2c06 == msg.data[0] >> 224) {
transferAllocation(address,address,uint256);
} else if (0x164e68de == msg.data[0] >> 224) {
withdrawFees(address);
} else if (0x396cb597 == msg.data[0] >> 224) {
0x396cb597();
} else {
require(0x70a08231 == msg.data[0] >> 224);
balanceOf(address);
}
} else if (0xc13886a4 == msg.data[0] >> 224) {
0xc13886a4();
} else if (0xc6743555 == msg.data[0] >> 224) {
0xc6743555();
} else if (0xeb820312 == msg.data[0] >> 224) {
recipients(address);
} else {
require(0xfeb7219d == msg.data[0] >> 224);
earnedFees(address);
}
}
我们可以猜测其功能是收入分配,存在合约地址 => 所有者地址的映射,所有者可以转移权重,总权重为 10000。
7. 猜测槽#
存储证明是一个梅克尔树,每个叶子节点是 sha3 (槽) 和 rlp 编码的值,前缀由上述分支节点引用。因此,我们可以使用 eth_getProof 查询一些前缀以获取完整树。证明的第一个项目始终是根,解码后我们可以发现它在 16 个可能的子节点中有 4 个。例如,通过查询槽 2 的 storageProof,我们可以获得子前缀 4 的证明,因为 sha3(toarg(2))=405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
。