區塊鏈 程式開發

在不同鏈佈署相同合約到相同地址:Nick’s Method 介紹與比較

最近因緣際會下看到 ERC-6551 提到 Nick’s Factory,好奇地去了解一下這是什麼魔法,發現了古老(?)而有趣的 Nick’s Method。這是一個可以在無信任的條件下,在特定地址上執行特定交易的方法(即便你和所有人都不知道密鑰)。在這篇文章中,我想介紹 Nick’s Method 和其他方法的比較。

Nick’s Method

Nick’s Method 最早是 Nick Johnson 使用的技法,他是 ENS 的首席開發者,之前曾是 Ethereum Foundation 的人。這個技法被運用在 The DAO 攻擊事件後的代幣分發,目的是發代幣到 11440 個地址中。發代幣的錢包是個多簽錢包(所以要他們聚在一起簽 11440 個交易多少有點不實際)。你可能會好奇:所以這關多鏈地址什麼事?

Nick’s Method 使我們可以先決定交易內容,接著再決定執行交易的錢包。因此在代幣分發時,首先先建立分發交易(104 個 MultiSend 分發到 11440 個錢包)的集合,再透過 Nick’s Method 決定執行這 104 個交易的錢包,接著用多簽錢包執行一次 MultiSend 到這 104 個錢包,最後發出 104 個 MultiSend 交易。如此一來就避免需要信任分發者的問題。

你可能會好奇:所以這關多鏈地址什麼事?

在說明 Nick’s Method 為什麼可以用到多鏈地址前,先說明地址是如何決定的可能會更好理解。目前地址的決定有兩種:一、透過合約建立者地址與其 Nonce 計算得出。二、透過建立者地址、合約 Bytecode、以及自選的 Salt 計算得出。而第一種就是 Nick’s Method 能成立的原因。

而在 Nick’s Method 中,我們會「創造」一個建立者,因為沒有用過的地址的 Nonce 是 0,因此在不同鏈上所計算出來的地址就都會一樣。接著下個問題是:怎麼「創造」的?

我們習慣的流程通常是先產生一個私鑰(也就是錢包),接著放一些原生代幣到這個錢包,再用這個錢包簽署一個建立合約的交易,最後把這個交易放到鏈上完成佈署。

我們有沒有可能把產生私鑰這段過程變不見呢?

以太坊的數位簽章

為了說明流程,我們還要了解一下交易簽署與驗證的過程,為了避免密碼學的數學部分讓你失去方向,這裡會將太過數學的部分跳過。簡單而言,交易簽署與驗證是透過數位簽章技術達成的。這也是當你按下錢包介面中的送出後,錢包內部會做的事情。當這個簽章與交易一起被發出去之後,其他人便會檢查這個簽章和內容是否相符,以及簽署者是否正確。

而以太坊使用的數位簽章,ECDSA,會對於一段訊息使用私鑰(約略等於助記詞)進行簽章,這個簽章是三個數字,通常使用 r、s、v 作為符號。當驗證簽章時,我們使用原本的訊息與這三個數字去逆算出簽名者,再比對這個簽名者是否與我們預期的一樣。

而數位簽章確保了使用某金鑰進行簽章的話,反算出來的簽名者一定會是該金鑰對應的公鑰(約略等於地址)。因此可以確保該交易確實是被該地址核可的。

或許你注意到了,簽名者實際上是被逆算出來的,因此即便隨意的給一個簽章,幾乎都能找到對應的簽名者(但如果你想找到特定的簽名者,那機率是十分地渺茫了)。

延伸閱讀:Kimi 老師的 ECDSA 介紹

應用 Nick’s Method 到多鏈佈署

現在,你應該大概知道 Nick’s Method 是怎麼發生的了:我們創建一個交易,任意的給予一個簽章,然後看看這個簽名者是誰。現在我們來嘗試一下,我們使用 Remix 隨意地建立一個合約並取得 Bytecode:

如果你很懶,以下是圖中合約的結果:

6080604052348015600e575f80fd5b50603e80601a5f395ff3fe60806040525f80fdfea26469706673582212205dd80bbc56676990b6e0302cf7586b7d29c9a86e8e324ccbe505b5729a26ca2d64736f6c634300081a0033

接著,我們透過 Ethers.js 的 Playground 來建立一個交易。我們執行以下指令(撰文當下為 v6,未來改版可能會有所不同):

tx = new Transaction()
tx.gasPrice = 100_000_000_000
tx.gasLimit = 1_000_000
tx.to = ZeroAddress
tx.type = 0
tx.data = '0x' + '6080604052348015600e575f80fd5b50603e80601a5f395ff3fe60806040525f80fdfea26469706673582212205dd80bbc56676990b6e0302cf7586b7d29c9a86e8e324ccbe505b5729a26ca2d64736f6c634300081a0033'

這個交易是一個 Gas Price 為 100 gwei、Gas Limit 為 100 萬的合約創建交易。最後的 60.... 是剛剛產生的合約,如果有使用自己的合約的話請記得要更改。接著,我們隨意的產生簽章:

tx.signature = Signature.from({r: hexlify(randomBytes(32)), s: hexlify(randomBytes(31)), v: '0x1b'})

我們隨機的產生 32 bytes 給 r、31 bytes 給 s、並固定 v 為 27(0x1b)。s 是因為被要求要是 low s value,簡單而言第一個 bit 要是 0。v 為 27 的原因是 EIP-155 開始透過簽名的 v 來指定鏈,因此使用 EIP-155 前的值 27 或 28 來避免違反。(目前似乎有許多鏈拒絕這類交易,因此部分鏈可能無法使用。)

我們輸入 tx 來看看目前的交易,會發現 from 欄位被推算出來了。

如果我們真的傳送一點 ETH 到這個地址,再廣播出去的話,會發現該交易是有效的。你可以透過 tx.serialized 來得到 Raw Transaction,並在瀏覽器的 Console 發出交易。請注意替換 <INFURA-KEY><TX> ,前者可以在 Infura 申請免費帳號並取得,後者則是 tx.serialized 的內容。

fetch('https://sepolia.infura.io/v3/<INFURA-KEY>', {
    method: 'POST',
  headers: {
    'Content-Type': 'application.json',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    method: 'eth_sendRawTransaction',
    params: ['<TX>'],
    id: "1"
  })
}).then(e => e.json()).then(console.log)

查詢該交易,可以看到確實進到 Mempool 了(至於他要怎麼在平均 Gas Price 3.5k 的 Sepolia 鏈上飄到確認則是另一個故事了。)

Nick’s Method 小結

到這裡,我們嘗試在沒有密鑰的狀況下,創造一個有效交易出來。然而也可以發現以下問題:

  1. EIP-155 後,部分 RPC Node 或鏈不支援該類交易:如果你嘗試透過 Etherscan 廣播該交易,可能會告訴你該 RPC Node 不收這類交易。而有些鏈則(似乎)是從一開始就不打算收。
  2. Gas Price 與 Gas Limit 無法依需調整:由於 Gas Price 與 Gas Limit 在交易創建時就需要指定,因此對於如 Sepolia,100 gwei 太少,但對於其他鏈而言則可能太高。

延伸閱讀:Nick’s method - Ethereum Keyless Execution

Nick’s Factory 的佈署在 0x4e59b44847b379578588920cA78FbF26c0B4956C 。透過發送 Data 為 <SALT><BYTECODE> 的交易到此地址就可以建立合約。範例可以參考 ERC-6551

Foundry 中,如果佈署合約的腳本添加了 Salt,則預設使用此 Nick’s Factory 佈署。而 ERC-2470 也是利用相同方法,建立了 Create2 Factory。

然而,由於 Nick’s Factory 應用了 Nick’s Method,因此無法躲過 EIP-155 的影響,在更新的鏈可能需要鏈的項目方特別處理,因此不防過時。

其他多鏈佈署方案

如同最前面提到的,目前最通用的多鏈佈署方案大概非全新 EOA 莫屬,大概不會有幾個 EVM 鏈會去動到這部份的運算。然而,這也是最需要信任的,假如我們開發一個多鏈合約,該合約信任某多鏈合約。若此多鏈合約的創建者在某鏈佈署了惡意合約,由於地址相同,我們可能不會發現到差異,進而受到攻擊。

其他常見的多鏈佈署方案,主要有最一開始提到的 Nick’s Factory 以及 CreateX。

Nick’s Factory

Nick’s Factory 指的是透過 Nick’s Method 佈署的 Create2 Factory 來達到多鏈同地址。Create2 是 EVM 的一個 Opcode,同樣也會佈署合約,但地址的運算不依賴於佈署者的 Nonce,而是合約的 Init Code 與添加的 Salt。也就是說:

  1. 透過 Nick’s Method 佈署一個 Create2 Factory 到某地址
  2. 透過該地址,給定固定的 Init Code 與 Salt,在相同的地址佈署相同合約

Nick’s Factory 的佈署在 0x4e59b44847b379578588920cA78FbF26c0B4956C 。透過發送 Data 為 <SALT><BYTECODE> 的交易到此地址就可以建立合約。範例可以參考 ERC-6551

Foundry 中,如果佈署合約的腳本添加了 Salt,則預設使用此 Nick’s Factory 佈署。而 ERC-2470 也是利用相同方法,建立了 Create2 Factory。

然而,由於 Nick’s Factory 應用了 Nick’s Method,因此無法躲過 EIP-155 的影響,在更新的鏈可能需要鏈的項目方特別處理,因此不防過時。

EOA + Create2 Factory

用與 Nick’s Factory 類似的思路,我們可以使用新的 EOA 先建立一個 Create2 Factory,再透過此 Create2 Factory 佈署新的合約。由於該 Create2 的地址會是由該 EOA 的 Nonce 算出(各鏈中都相同),因此若透過此 Create2 Factory 使用相同 Bytecode 與 Salt 佈署合約,則會取得相同的合約地址。如此一來也可在不同鏈上取得相同地址。

CreateX

CreateX 是基於 Create3 概念所建立的專案。Create3 Factory 指的是結合 Create 與 Create2,目的在於消除 Init Code 的硬性要求。當在建立多鏈合約時,我們可能希望不同鏈有不同的 Constructor 參數,但這會影響到 Init Code。Create3 使用與 Create2 Factory 相同的概念,但會先產生一個合約,並在該合約使用 Create Opcode 建立最後希望的合約。具體而言:

  1. 透過 Create2 佈署一個 Deployer Contract
  2. 透過 Deployer Contract 建立合約(基於 Nonce 計算地址)

由於該 Deployer Contract 在給定相同 Salt 下,不同鏈中會是相同地址,且 Nonce 均為 1。因此佈署新的合約時,無論合約內容都會得到同一個地址。

延伸閱讀:CREATE3 多鏈部署合約於相同地址

Hardhat 使用的即是 CreateX

然而 CreateX 仰賴開發者的私鑰,因此不完全防過時(但相對於 Nick’s Method 要好)。

比較

到目前,我們看了五個多鏈佈署的方案,以下做個比較:

  • EOA(有 Private Key):需信任、成本- 最低、防過時、中心化
  • EOA(無 Private Key):不需信任、成本難控制,不防過時、去中心化
  • EOA + Create2:不需信任、成本可控、防過時、中心化
  • Nick’s Factory:不需要信任,成本可控,不防過時、去中心化
  • CreateX:需要信任,成本可控、防過時、中心化

註:信任指其他人是否可信任某多鏈合約的內容在不同鏈上均相同。中心化指是否會依賴特定私鑰。

對於一般多鏈專案而言,使用專用 Private Key 可能還是更好的選項,而當建立基於多鏈專案應用上,還是免不了要逐鏈審查合約內容(畢竟還是得看看合約是用什麼方法佈署的,但可能比看 Bytecode 或 Source Code 要快一點點……嗎),可能沒有方法輕易的繞過這部份的工作。

寫在最後

最近因緣際會(?)下看了 ERC-6551,而發現了陌生的詞「Nick’s Method」,起初還查不太到是什麼,但後來看到那篇介紹後才意外發現這個古老的手藝(但可能也快被迫失傳了)。個人感到最有趣的點是沒想過可以反過來用簽章來決定地址(雖然之前一直摸 ECDSA),從而創建一個沒有私鑰的交易。

不過之前聽了海帶老師介紹 EOFv1,似乎 Create 和 Create2 也快要被淘汰了,所以可能這些都不防過時了。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *