mookim

mookim

mookim.eth

如何猜測 unichain 主網創世?

我們擁有什麼?#

我們想要達成什麼?#

一個可用的 unichain RPC 節點,在本文中,我們只專注於如何猜測正確的 genesis-l2.json 文件,使得區塊 0 具有正確的區塊哈希 =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 前綴,值包含代碼、存儲字典、餘額和隨機數。
我猜餘額和隨機數不會不同,因此查詢存儲和代碼,找到不匹配的並替換:

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. 找到缺失的合約#

在運行上述代碼後,我們發現兩個合約出現在測試網中,但主網沒有代碼或存儲,因此這 2 個鍵應該被刪除:

  • 0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f
  • 0x1f98431c8ad98523631ae4a59f267346ea31f984

搜索這些地址顯示它們分別是 UniV2Factory 和 UniV3Factory。

查看探索器驗證合約頁面( https://unichain.blockscout.com/verified-contracts ),我們發現一個具有唯一合約地址的 UniV2Factory 合約:https://unichain.blockscout.com/address/0x1F98400000000000000000000000000000000002?tab=contract

因此,我們受到啟發去檢查相鄰合約地址,發現這些合約也已預先部署:

0x1F98400000000000000000000000000000000002
0x1F98400000000000000000000000000000000003
0x1F98400000000000000000000000000000000004

後兩者尚未驗證,但從發出的事件中,我們可以猜測它們是 UniV3 和 UniV4 合約,因此讓我們將這 3 個地址添加到我們的 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 獲取它們並替換為正確的值。

在這些步驟之後,我們仍然獲得錯誤的區塊哈希。

4. eth_getProof 確認存儲正確性#

我們如何知道創世區塊的更多細節?debug_traceBlock RPC 方法確實支持創世區塊。

檢查所有可用的 RPC 方法,我們可以找到這個 eth_getProof,它可以輸出 accountProof、餘額、codeHash、隨機數、storageHash 和 storageProof。

通過迭代所有已知地址調用 eth_getProof,我們可以發現所有已知地址具有正確的餘額、codeHash、隨機數、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 個槽,我們成功找到前三個的正確存儲值,但最後一個仍然錯誤,即使它的前 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。

6. 猜測槽#

storageProof 是一個梅克爾樹,每個葉子節點是 sha3 (槽) 和 rlp 編碼的值,前綴由上述分支節點引用。因此,我們可以使用 eth_getProof 查詢一些前綴以獲取完整樹。證明的第一項總是根,解碼後我們可以發現它在 16 個可能的子項中有 4 個。例如,通過查詢槽 2 的 storageProof,我們可以獲得子前綴 4 的證明,因為 sha3(toarg(2))=405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace

let {toBytes,bytesToHex}=require("@ethereumjs/util")
const { utils } = require('ethers');
decodeNode=require("@ethereumjs/trie").decodeNode
nibblesToBytes=require("@ethereumjs/trie").nibblesToBytes


> decodeNode(toBytes("0xf891808080a0464322c988da3ad6becee03210d007812970a011da4ce89d63d7a8c7c46dd349a047213c020d0419fb3c2273dcddab7cb42ad2614cfd1042c358b5219d4d66d472808080a0937ec93a69abccbf4d7348457eb2e55ed59b568bc212aeb125abf3aee59cc897808080a0aa4ecc3c019aa37691f5cb2cf11822f456ce443b292d228219f1fb29502d104f80808080"))
BranchNode {
  _branches: [
    Uint8Array(0) [],
    Uint8Array(0) [],
    Uint8Array(0) [],
   3 Uint8Array(32) [ 0x464322c988da3ad6becee03210d007812970a011da4ce89d63d7a8c7c46dd349 => v="0xf7a030efa3127a31b746c4ade29f2dc3d63d735226f92c8b5c62f93844e730e5387595943fcbacd76037534d2aaeb9a17f4e631dd64fbe31"
  key = 0x30efa3127a31b746c4ade29f2dc3d63d735226f92c8b5c62f93844e730e53875
  value="0x3fcbacd76037534d2aaeb9a17f4e631dd64fbe31"
       70,  67,  34, 201, 136, 218,  58, 214,
      190, 206, 224,  50,  16, 208,   7, 129,
       41, 112, 160,  17, 218,  76, 232, 157,
       99, 215, 168, 199, 196,  109, 211,  73
    ],
   4 Uint8Array(32) [ 0x47213c020d0419fb3c2273dcddab7cb42ad2614cfd1042c358b5219d4d66d472 => v="0xe5a03d2849548fa0011234fd0669e617f5df1374a0d525b71bedec3d14dd02bf7f9b83821db0"
   key = 0x4d2849548fa0011234fd0669e617f5df1374a0d525b71bedec3d14dd02bf7f9b  bytesToHex(nibblestoBytes([4].concat(decodeNode(toBytes(v)).key())))
   value = 7600 utils.decodeRlp(bytesToHex(decodeNode(toBytes(v)).value()))
      71,  33,  60,   2,  13,   4,  25, 251,
      60,  34, 115, 220, 221, 171, 124, 180,
      42, 210,  97,  76, 253,  16,  66, 195,
      88, 181,  33, 157,  77, 102, 212, 114
    ],
    Uint8Array(0) [],
    Uint8Array(0) [],
    Uint8Array(0) [],
   8 Uint8Array(32) [ 0x937ec93a69abccbf4d7348457eb2e55ed59b568bc212aeb125abf3aee59cc897 => 0xf7a0328a4924e9ba234119f26396d33bbebdcb65d7af957d59f814558268b1c8e69a9594ae85bbb6c1c1807a64a88f1a1f978740c8a0dba0
Node key=0x828a4924e9ba234119f26396d33bbebdcb65d7af957d59f814558268b1c8e69a [8, 2,  8, 10,  4,  9,  2,  4, 14,  9, 11, 10,  2,3,  4,  1,  1,  9, 15,  2,  6,  3,  9,  6, 13,3,  3, 11, 11, 14, 11, 13, 12, 11,  6,  5, 13,7, 10, 15,  9,  5,  7, 13,  5,  9, 15,  8,  1,4,  5,  5,  8,  2,  6,  8, 11,  1, 12,  8, 14,6,  9, 10],
key = sha3(sha3(toarg(0x3fcbacd76037534d2aaeb9a17f4e631dd64fbe31)+toarg(3)))
value = > utils.decodeRlp(bytesToHex(decodeNode(toBytes("0xf7a0328a4924e9ba234119f26396d33bbebdcb65d7af957d59f814558268b1c8e69a9594ae85bbb6c1c1807a64a88f1a1f978740c8a0dba0")).value()))
'0xae85bbb6c1c1807a64a88f1a1f978740c8a0dba0'
      147, 126, 201,  58, 105, 171, 204,
      191,  77, 115,  72,  69, 126, 178,
      229,  94, 213, 155,  86, 139, 194,
       18, 174, 177,  37, 171, 243, 174,
      229, 156, 200, 151
    ],
    Uint8Array(0) [],
    Uint8Array(0) [],
    Uint8Array(0) [],
   12 Uint8Array(32) [ 0xaa4ecc3c019aa37691f5cb2cf11822f456ce443b292d228219f1fb29502d104f => v="0xe5a03112f27f422905a81edb239837dddce63ac33c22d7066f8b0d9d4e00afc6650d83820960"
   key = 0xc112f27f422905a81edb239837dddce63ac33c22d7066f8b0d9d4e00afc6650d bytesToHex(nibblestoBytes([12].concat(decodeNode(toBytes(v)).key())))
   value = 0x0960=2400 utils.decodeRlp(bytesToHex(decodeNode(toBytes(v)).value()))
      170,  78, 204, 60,   1, 154, 163, 118,
      145, 245, 203, 44, 241,  24,  34, 244,
       86, 206,  68, 59,  41,  45,  34, 130,
       25, 241, 251, 41,  80,  45,  16,  79
    ],
    Uint8Array(0) [],
    Uint8Array(0) [],
    Uint8Array(0) []
  ],
  _value: Uint8Array(0) []
}

最終,我們獲得了所有 4 個缺失的槽,涉及兩個地址:0xae85bbb6c1c1807a64a88f1a1f978740c8a0dba0 0x3fcbacd76037534d2aaeb9a17f4e631dd64fbe31,並存儲在映射槽 3 和 +1(結構)

  • sha3(toarg(0xae85bbb6c1c1807a64a88f1a1f978740c8a0dba0)+toarg(3)) = f42505e4cfab5287d35a1886da3b550274dafd60099332aaa2a1f2edb64366e0 => 828a4924e9ba234119f26396d33bbebdcb65d7af957d59f814558268b1c8e69a
  • sha3(toarg(0xae85bbb6c1c1807a64a88f1a1f978740c8a0dba0)+toarg(3))+1 = f42505e4cfab5287d35a1886da3b550274dafd60099332aaa2a1f2edb64366e1 => c112f27f422905a81edb239837dddce63ac33c22d7066f8b0d9d4e00afc6650d
  • sha3(toarg(0x3fcbacd76037534d2aaeb9a17f4e631dd64fbe31)+toarg(3)) = 6499618908e6c3bd1552bf01e251de0ec9ec985e74571d0b89e1b71c4e49dc7c => 30efa3127a31b746c4ade29f2dc3d63d735226f92c8b5c62f93844e730e53875
  • sha3(toarg(0x3fcbacd76037534d2aaeb9a17f4e631dd64fbe31)+toarg(3))+1 = 6499618908e6c3bd1552bf01e251de0ec9ec985e74571d0b89e1b71c4e49dc7d => 4d2849548fa0011234fd0669e617f5df1374a0d525b71bedec3d14dd02bf7f9b
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。