解讀Starknet智能合約模型與原生AA:特立獨行的技術巨匠

中級Mar 18, 2024
Starknet支持原生級別的AA賬戶抽象,允許高度定製的交易處理方案,併採取多項反製措施保障安全。這些特性爲Starknet創造了必要的條件,支持存儲分層、檢測垃圾合約等功能,盡管某些功能尚未落地,但爲AA生態的探索提供了重要基礎。
解讀Starknet智能合約模型與原生AA:特立獨行的技術巨匠

摘要:·Starknet最主要的幾大技術特性,包括利於ZK證明生成的Cairo語言、原生級別的AA、業務邏輯與狀態存儲相獨立的智能合約模型。·Cairo是一種通用的ZK語言,既可以在Starknet上實現智能合約,也可以用於開髮偏傳統的應用,其編譯流程中引入Sierra作爲中間語言,使得Cairo可以頻繁迭代,但又不必變更最底層的字節碼,隻需要把變化傳導至中間語言身上;在Cairo的標準庫內,還納入了賬戶抽象所需要的許多基本數據結構。·Starknet智能合約將業務邏輯與狀態數據分開來存儲,不衕於EVM鏈,Cairo合約部署包含“編譯、聲明、部署”三階段,業務邏輯被聲明在Contract class中,包含狀態數據的Contract實例可以與class建立關聯,併調用後者所包含的代碼;


·Starknet的上述智能合約模型利於代碼覆用、合約狀態覆用、存儲分層、檢測垃圾合約,也利於存儲租賃製和交易併行化的實現。雖然後兩者目前暫未落地,但Cairo智能合約的架構,還是爲其創造了“必要條件”。·Starknet鏈上隻有智能合約賬戶,沒有EOA賬戶,從一開始就支持原生級別的AA賬戶抽象。其AA方案一定程度吸收了ERC-4337的思路,允許用戶選擇高度定製化的交易處理方案。爲了防止潛在的攻擊場景,Starknet做出了諸多反製措施,爲AA生態做出了重要的探索。


正文:繼Starknet髮行代幣之後,STRK逐漸成爲以太坊觀察者眼中不可或缺的要素之一。這個曏來以“特立獨行”“不重視用戶體驗”而聞名的以太坊Layer2明星,就像一個與世無爭的隱士,在EVM兼容大行其道的Layer2生態裡默默的開辟自己的一畝三分地。由於太過忽視用戶,甚至公開在Discord開設“電子乞丐”頻道,Starknet一度遭到擼毛黨的抨擊,在遭噴“不近人情”的衕時,技術上的深厚造詣瞬間變得“一文不值”,似乎隻有UX和造富效應才是一切。《金閣寺》中那句“不被人理解成了我唯一的自豪”,簡直就是Starknet的自我寫照。但拋開這些江湖瑣事,單純從代碼極客們的“技術品味”出髮,作爲ZK Rollup先驅之一的Starknet和StarkEx,幾乎就是Cairo愛好者眼中的瑰寶,在某些全鏈游戲開髮者心中,Starknet和Cairo簡直就是web3的一切,無論是Solidity還是Move都無法與之相提併論。現如今橫亘在“技術極客”和“用戶”之間的最大代溝,其實更多歸因於人們對Starknet的認知欠缺。抱著對區塊鏈技術的興趣與探索欲,以及對Starknet的價值髮現,本文作者從Starknet的智能合約模型與原生AA出髮,爲大家簡單梳理其技術方案與機製設計,在爲更多人展示Starknet技術特性的衕時,也希望讓人們了解這個“不被人所理解的獨行俠”。Cairo語言極簡科普下文中我們將重點討論Starknet的智能合約模型與原生賬戶抽象,説明Starknet是如何實現原生AA的。讀完此文,大家也可以理解爲什麽Starknet中不衕錢包的助記詞不能混用。但在介紹原生賬戶抽象前,讓我們先了解下Starknet獨創的Cairo語言。在Cairo的髮展歷程中,出現了名爲Cairo0的早期版本,以及後來的的現代版。Cairo的現代版本整體語法類似於Rust,實際上是一門通用的ZK語言,除了可以在Starknet上編寫智能合約,也可以用於通用應用的開髮。比如我們可以用Cairo語言開髮ZK身份驗證繫統,這段程序可以在自己搭建的服務器上運行,不必依賴於StarkNet網絡。可以説,任何需要可驗證計算屬性的程序都可以用Cairo語言來實現。而Cairo可能是目前最利於生成ZK證明的編程語言。

從編譯流程來看,Cairo使用了基於中間語言的編譯方法,如下圖所示。圖中的Sierra是Cairo語言編譯過程中的一道中間形態(IR),而Sierra會再被編譯爲更底層的二進製代碼形式,名爲CASM,在Starknet節點設備上直接運行。

引入Sierra作爲中間形態,便於Cairo語言增加新特性,許多時候隻要在Sierra這道中間語言上做手腳,不必直接變更底層的CASM代碼,這就省去了很多麻煩事,Starknet的節點客戶端就不必頻繁更新。這樣就可以在不變更StarkNet底層邏輯的情況下,實現Cairo語言的頻繁迭代。而在Cairo的標準庫內,還納入了賬戶抽象所需要的許多基本數據結構。Cairo的其他創新,包括一種被稱爲Cairo Native的理論方案,該方案計畫把Cairo編譯爲能適配不衕硬件設備的底層機器代碼,Starknet節點在運行智能合約時,將不必依賴於CairoVM虛擬機,這樣可以大幅度提升代碼執行速度【目前還處於理論階段,未落地】。

Starknet智能合約模型:代碼邏輯與狀態存儲的剝離

與EVM兼容鏈不衕,Starknet在智能合約繫統的設計上,有著突破性的創新,這些創新很大程度是爲原生AA以及未來上線的併行交易功能準備的。在這裡,我們要知道,以太坊等傳統公鏈上,智能合約的部署往往遵循“編譯後部署”的方式,以ETH智能合約舉例:

1.開髮者在本地編寫好智能合約後,通過編輯器將Solidity程序編譯爲EVM的字節碼,這樣就可以被EVM直接理解併處理;

2.開髮者髮起一筆部署智能合約的交易請求,把編譯好的EVM字節碼部署到以太坊鏈上。


(圖片來源:not-satoshi.com)Starknet的智能合約雖然也遵循“先編譯後部署”的思路,智能合約以CairoVM支持的CASM字節碼形式部署在鏈上,但在智能合約的調用方式與狀態存儲模式上,Starknet與EVM兼容鏈有著巨大差異。準確的説,以太坊智能合約=業務邏輯+狀態信息,比如USDT的合約中不光實現了Transfer、Approval等常用的函數功能,還存放著所有USDT持有者的資産狀態,代碼和狀態被耦合在了一起,這帶來了諸多麻煩,首先不利於DAPP合約升級與狀態遷移,也不利於交易的併行處理,是一種沉重的技術包袱。

對此,Starknet對狀態的存儲方式進行了改良,在其智能合約實現方案中,DAPP的業務邏輯與資産狀態完全解耦,分別存放在不衕地方,這樣做的好處很明顯,首先可以讓繫統更快速的分辨出,是否存在重覆或多餘的代碼部署。這裡的原理是這樣:以太坊的智能合約=業務邏輯+狀態數據,假如有幾個合約的業務邏輯部分完全一緻,但狀態數據不衕,則這幾個合約的hash也不衕,此時繫統不太好分辨是否有“垃圾合約”存在。而在Starknet的方案中,代碼部分和狀態數據直接分開,繫統根據代碼部分的hash,更容易分辨出是否有相衕的代碼被多次部署,因爲他們的hash是相衕的。這樣便於製止重覆的代碼部署行爲,節約Starknet節點的存儲空間。在Starknet的智能合約繫統中,合約的部署與使用,分爲“編譯、聲明、部署”三個階段。資産髮行者如果要部署Cairo合約,第一步要在自己的設備本地,把寫好的Cairo代碼,編譯爲 Sierra 以及底層字節碼CASM形式。然後,合約部署者要髮布聲明“declare”交易,把合約的 CASM 字節碼和 Sierra 中間代碼部署到鏈上,名爲Contract Class。


(圖片來源:Starknet官網)之後,如果你要要採用該資産合約裡定義的函數功能,可以通過DAPP前端髮起“deploy”交易,部署一個和Contract Class相關聯的Contract實例,這個實例裡麵會存放資産狀態。之後,用戶可以調用Contract Class裡的函數功能,變更Contract實例的狀態。其實,但凡了解麵曏對象編程的人,都應該能很容易的理解Starknet這裡的Class和Instance各自代錶啥。開髮者聲明的Contract Class,隻包含智能合約的業務邏輯,是一段誰都可以調用的函數功能,但沒有實際的資産狀態,也就沒有直接實現“資産實體”,隻有“靈魂”沒有“肉體”。而當用戶部署具體的Contract實例後,資産就完成了“實體化”。如果你要對資産“實體”的狀態進行變更,比如把自己的token轉移給別人,可以直接調用Contract Class裡寫好的函數功能。上述過程就和傳統麵曏對象編程語言裡的“實例化”有些類似(但不完全一緻)。

智能合約被分離爲Class和實例後,業務邏輯與狀態數據解耦合,爲Starknet帶來了以下特性:

1.利於存儲分層和“存儲租賃製”的實現

所謂的存儲分層,就是開髮者可以按照自己的需求,將數據放在自定義的位置,比如Starknet鏈下。StarkNet準備兼容Celestia等DA層,DAPP開髮者可以將數據存放在這些第三方DA層裡。比如一個游戲可以將最重要的資産數據存放在Starknet主網上,而將其他數據存儲在Celestia等鏈下DA層。這種按照安全需求定製化選擇DA層的方案,被Starknet命名爲”Volition”。

而所謂的存儲租賃製,是指每個人應當持續的爲自己占用的存儲空間付費。你占用的鏈上空間有多少,理論上就該持續的支付租金。

在以太坊智能合約模型中,合約的所有權不明確,難以分辨出一個ERC-20合約應該由部署者還是資産持有者支付“租金”,遲遲沒有上線存儲租賃功能,隻在合約部署時曏部署者收取一筆費用,這種存儲費用模型併不合理。

而在Starknet和Sui以及CKB、Solana的智能合約模型下,智能合約的所有權畫分更明確,便於收取存儲資金【目前Starknet沒有直接上線存儲租賃製,但未來會實現】

2.實現真正的代碼覆用,減少垃圾合約的部署

我們可以聲明一個通用的代幣合約作爲class存儲到鏈上,然後所有人都可以調用這個class裡的函數,來部署屬於自己的代幣實例。而且合約也可以直接調用class內的代碼,這就實現了類似於Solidity中的Library函數庫的效果。衕時,Starknet的這種智能合約模型,有助於分辨“垃圾合約”。前麵對此有所解釋。在支持代碼覆用與垃圾合約檢測後,Starknet可以大幅度減少上鏈的數據量,盡可能減輕節點的存儲壓力。3.真正的合約“狀態”覆用區塊鏈上的合約升級主要涉及到業務邏輯的變更,在Starknet的場景下,智能合約的業務邏輯與資産狀態天生就是分離的,合約實例變更了關聯的合約類型class,就可以完成業務邏輯升級,不需要把資産狀態遷移到新去處,這種合約升級形式比以太坊的更徹底、更原生。

而以太坊合約要變更業務邏輯,往往就要把業務邏輯“外包”給代理合約,通過變更依賴的代理合約,來實現主合約業務邏輯的變更,但這種方式不夠簡潔,也“不原生”。

(圖片來源:wtf Academy)

在某些場景下,如果舊的以太坊合約被整個棄用,裡麵的資産狀態就無法直接遷移到新去處,非常麻煩;而Cairo合約就不需要把狀態遷移走,可以直接“覆用”舊的狀態。4.利於交易併行化處理要盡可能提升不衕交易指令的可併行度,必要一環是把不衕人的資産狀態分散開存儲,這在比特幣、CKB和Sui身上可見一斑。而上述目標的先決條件,就是把智能合約的業務邏輯和資産狀態數據剝離開。雖然Starknet還沒有針對交易併行進行深度的技術實現,但未來將把併行交易作爲一個重要目標。

Starknet的原生AA與賬戶合約部署

其實,所謂的賬戶抽象與AA,是以太坊社區髮明出來的獨特概念,在許多新公鏈中,併沒有EOA賬戶和智能合約賬戶的分野,從一開始就避開了以太坊式賬戶體繫的坑。比如在以太坊的設定下,EOA賬戶控製者必鬚在鏈上有ETH才可以髮起交易,沒有辦法直接選用多樣性的身份驗證方式,要添加一些定製化的支付邏輯也極爲麻煩。甚至有人認爲,以太坊的這種賬戶設計簡直就是反人類的。

如果我們去觀察Starknet或zkSyncEra等主打“原生AA”的鏈,可以觀察到明顯的不衕:首先,Starknet和zkSyncEra統一了賬戶類型,鏈上隻有智能合約賬戶,從一開始就沒有EOA賬戶這種東西(zkSync Era會在用戶新創建的賬戶上,默認部署一套合約代碼,模擬出以太坊EOA賬戶的特徵,這樣就便於兼容Metamask)。


而Starknet沒有考慮直接兼容Metamask等以太坊周邊設施,用戶在初次使用Starknet錢包時,會自動部署專用的合約賬戶,説白了就是部署前麵提到的合約實例,這個合約實例會和錢包項目方事先部署的合約class相關聯,可以直接調用class裡麵寫好的一些功能。下麵我們將談及一個有意思的話題:在領取STRK空投時,很多人髮現Argent與Braavos錢包彼此不能兼容,將Argent的助記詞導入Braavos後,無法導出對應的賬戶,這其實是因爲Argent和Braavos採用了不衕的賬戶生成計算方式,導緻相衕助記詞生成的賬戶地址不衕。具體而言,在Starknet中,新部署的合約地址可以通過確定性的算法得出,具體使用以下公式:

上述公式中的pedersen(),是一種易於在ZK繫統中使用的哈希算法,生成賬戶的過程,其實就是給pedersen函數輸入幾個特殊參數,産生相應的hash,這個hash就是生成的賬戶地址。上麵的圖片中顯示了Starknet生成“新的合約地址”時用到的幾個參數,deployer_address代錶“合約部署者”的地址,這個參數可以爲空,即便你事先沒有Starknet合約賬戶,也可以部署新的合約。salt爲計算合約地址的鹽值,簡單來説,就是一個隨機數,該變量實際上是爲了避免合約地址重覆引入的。class_hash就是前麵介紹過的,合約實例對應的class的哈希值。而constructor_calldata_hash,代錶合約初始化參數的哈希。基於上述公式,用戶可以在合約部署至鏈上之前,就預先算出生成的合約地址。Starknet允許用戶在事先沒有Starknet賬戶的情況下,直接部署合約,流程如下:1. 用戶先確定自己要部署的合約實例,要關聯哪個合約class,把該class的hash作爲初始化參數之一,併算出salt,得知自己生成的合約地址;2. 用戶知道自己將會把合約部署在哪後,先曏該地址轉入一定量的ETH,作爲合約部署費用。一般來説,這部分ETH要通過跨鏈橋從L1跨到Starknet網絡;3. 用戶髮起合約部署的交易請求。其實,所有的Starknet賬戶都是通過上述流程部署的,但大部分錢包屏蔽了這裡麵的細節,用戶根本感知不到裡麵的過程,就好像自己轉入ETH後合約賬戶就部署完了。

上述方案帶來了一些兼容性問題,因爲不衕的錢包在生成賬戶地址時,生成的結果併不一緻,隻有滿足以下條件的錢包才可以混用:

  1. 錢包使用的私鑰派生公鑰與簽名算法相衕;
  2. 錢包的salt計算流程相衕;
  3. 錢包的智能合約class在實現細節上沒有根本性不衕;

在之前談到的案例中,Argent與Braavos都使用了ECDSA簽名算法,但雙方的salt計算方法不衕,相衕的助記詞在兩款錢包中生成的賬戶地址會不一緻。

我們再回到賬戶抽象的話題上。Starknet和zkSync Era把交易處理流程中涉及的一繫列流程,如身份驗證(驗證數字簽名)、Gas費支付等核心邏輯,全部挪到“鏈底層”之外去實現。用戶可以在自己的賬戶中,自定義上述邏輯的實現細節.比如你可以在自己的Starknet智能合約賬戶裡,部署專用的數字簽名驗證函數,當Starknet節點收到了你髮起的交易後,會調用你在鏈上賬戶中自定義的一繫列交易處理邏輯。這樣顯然要更靈活。而在以太坊的設計中,身份驗證(數字簽名)等邏輯是寫死在節點客戶端代碼裡的,不能原生支持賬戶功能的自定義。

(Starknet架構師指明的原生AA方案示意圖,交易驗證和gas費資格驗證都被轉移到鏈上合約去處理,鏈的底層虛擬機可以調用用戶自定義或指定的這些函數)按照zkSyncEra和Starknet官方人員的説法,這套賬戶功能模塊化的思路,借鑒了EIP-4337。但不衕的是,zkSync和Starknet從一開始就把賬戶類型合併了,統一了交易類型,併且用統一入口接收處理所有交易,而以太坊因爲存在歷史包袱,且基金會希望盡可能避免硬分叉等粗暴的迭代方案,所以支持了EIP-4337這種“曲線救國”的方案,但這樣的效果是,EOA賬戶和4337方案各自採用獨立的交易處理流程,顯得別扭而且臃腫,不像原生AA那麽靈便。

(圖片來源:ArgentWallet)但目前Starknet的原生賬戶抽象還沒有達到完全的成熟,從實踐進度來看,Starknet的AA賬戶實現了簽名驗證算法的自定義,但對於手續費支付的自定義,目前Starknet實際上僅支持ETH和STRK繳納gas費,併且還沒有支持第三方代繳gas。所以Starknet在原生AA上的進度,可以説是“理論方案基本成熟,實踐方案還在推進”。由於Starknet內隻有智能合約賬戶,所以其交易的全流程都考慮了賬戶智能合約的影響。首先,一筆交易被Starknet節點的內存池(Mempool)接收後,要進行校驗,驗證步驟包括:

  1. 交易的數字簽名是否正確,此時會調用交易髮起者賬戶中,自定義的驗簽函數;
  2. 交易髮起人的賬戶餘額能否支付得起gas費;

這裡要註意,使用賬戶智能合約中自定義的簽名驗證函數,就意味著存在攻擊場景。因爲內存池在對新來的交易進行簽名驗證時,併不收取gas費(如果直接收取gas費,會帶來更嚴重的攻擊場景)。惡意用戶可以先在自己的賬戶合約中自定義超級覆雜的驗簽函數,再髮起大量交易,讓這些交易被驗簽時,都去調用自定義的覆雜驗簽函數,這樣可以直接耗盡節點的計算資源。爲了避免此情況的髮生,StarkNet對交易進行了以下限製:

  1. 單一用戶在單位時間內,可髮起的交易筆數有上限;
  2. Starknet賬戶合約中自定義的簽名驗證函數,存在覆雜度上的限製,過於覆雜的驗簽函數不會被執行。Starknet限製了驗簽函數的gas消耗上限,如果驗簽函數消耗的gas量過高,則直接拒絶此交易。衕時,也不允許賬戶合約內的驗簽函數調用其他合約。

Starknet交易的流程圖如下:

值得註意的是,爲了進一步加速交易校驗流程,Starknet節點客戶端中直接實現了Braavos和Argent錢包的簽名驗證算法,節點髮現交易生成自這兩大主流Starknet錢包時,會調用客戶端裡自帶的Braavos/Argent簽名算法,通過這種類似於緩存的思想,Starknet可以縮短交易驗證時間。交易數據再通過排序器的驗證後(排序器的驗證步驟比內存池驗證會深入很多),排序器會將來自內存池的交易打包處理,併遞交給ZK證明生成者。進入此環節的交易即使失敗,也會被收取gas。但如果讀者了解Starknet的歷史,會髮現早期的Starknet對執行失敗的交易不收取手續費,最常見的交易失敗情況是,用戶僅有1ETH 的資金,但是對外轉出10ETH,這種交易顯然有邏輯錯誤,最終必然失敗,但在具體執行前誰也不知道結果是啥。但StarkNet在過去不會對這種失敗交易收取手續費。這種無成本的錯誤交易會浪費Starknet節點的計算資源,會衍生出ddos攻擊場景。錶麵上看,對錯誤交易收取手續費似乎很好實現,實際上卻相當覆雜。Starknet推出新版的Cairo1語言,很大程度就是爲了解決失敗交易的gas收取問題。我們都知道,ZK Proof是一種有效性證明,而執行失敗的交易,其結果是無效的,無法在鏈上留下輸出結果。嘗試用有效性證明,來證明某筆指令執行無效,不能産生輸出結果,聽起來就相當奇怪,實際上也不可行。所以過去的Starknet在生成證明時,直接把不能産生輸出結果的失敗交易都刨除了出去。Starknet團隊後來採用了更聰明的解決方案,構建了一門新的合約語言Cairo1,使得“所有交易指令都能産生輸出結果併onchain”。乍一看,所有交易都能産生輸出,就意味著從不出現邏輯錯誤,而大多數時候交易失敗,是因爲遇到一些bug,導緻指令執行中斷了。讓交易永不中斷併成功産生輸出,很難實現,但實際上有一種很簡單的替代方案,就是在交易遇到邏輯錯誤導緻中斷時,也讓他産生輸出結果,隻不過這時候會返回一個False值,使大家知道這筆交易的執行不順利。但要註意,返回False值,也就返回了輸出結果,也就是説,Cairo1裡麵,不管指令有沒有遇到邏輯錯誤,有沒有臨時中斷,都能夠産生輸出結果併onchain。這個輸出結果可以是正確的,也可以是False報錯信息。For Example,假如存在以下代碼段

此處的 _balances::read(from) - amount可能因爲曏下溢出而報錯,這個時候就會導緻相應的交易指令中斷併停止執行,不會在鏈上留下交易結果;而如果將其改寫爲以下形式,在交易失敗時仍然返回一個輸出結果,留存在鏈上,單純從觀感上來看,這就好像所有的交易都能順利的在鏈上留下交易輸出,統一收取手續費就顯得特別合理。

StarknetAA合約概述

考慮到本文有部分讀者可能存在編程背景,所以此處簡單展示了一下Starknet中的賬戶抽象合約的接口:

上述接口中的validate_declare,用於用戶髮起的declare交易的驗證,而validate則用於一般交易的驗證,主要驗證用戶的簽名是否正確,而execute則用於交易的執行。我們可以看到Starknet合約賬戶默認支持multicall即多重調用。多重調用可以實現一些很有趣的功能,比如在進行某些DeFi交互時打包以下三筆交易:

  1. 第一筆交易將代幣授權給DeFi合約
  2. 第二筆交易觸髮DeFi合約邏輯
  3. 第三筆交易清空對DeFi合約的授權

當然,由於多重調用是具有原子性的,所以存在一些更加覆雜的用法,比如執行某些套利交易。

總結

  • Starknet最主要的幾大技術特性,包括利於ZK證明生成的Cairo語言、原生級別的AA、業務邏輯與狀態存儲相獨立的智能合約模型。
  • Cairo是一種通用的ZK語言,既可以在Starknet上實現智能合約,也可以用於開髮偏傳統的應用,其編譯流程中引入Sierra作爲中間語言,使得Cairo可以頻繁迭代,但又不必變更最底層的字節碼,隻需要把變化傳導至中間語言身上;在Cairo的標準庫內,還納入了賬戶抽象所需要的許多基本數據結構。
  • Starknet智能合約將業務邏輯與狀態數據分開來存儲,不衕於EVM鏈,Cairo合約部署包含“編譯、聲明、部署”三階段,業務邏輯被聲明在Contract class中,包含狀態數據的Contract實例可以與class建立關聯,併調用後者包含的代碼;
  • Starknet的上述智能合約模型利於代碼覆用、合約狀態覆用、存儲分層、檢測垃圾合約,也利於存儲租賃製和交易併行化的實現。雖然後兩者目前暫未落地,但Cairo智能合約的架構,還是爲其創造了“必要條件”。
  • Starknet鏈上隻有智能合約賬戶,沒有EOA賬戶,從一開始就支持原生級別的AA賬戶抽象。其AA方案一定程度吸收了ERC-4337的思路,允許用戶選擇高度定製化的交易處理方案。爲了防止潛在的攻擊場景,Starknet做出了諸多反製措施,爲AA生態做出了重要的探索。

聲明:

  1. 本文轉載自[極客 Web3],著作權歸屬原作者[Shew & Faust],如對轉載有異議,請聯繫Gate Learn團隊,團隊會根據相關流程盡速處理。
  2. 免責聲明:本文所錶達的觀點和意見僅代錶作者個人觀點,不構成任何投資建議。
  3. 文章其他語言版本由Gate Learn團隊翻譯, 在未提及Gate.io的情況下不得覆製、傳播或抄襲經翻譯文章。
Розпочати зараз
Зареєструйтеся та отримайте ваучер на
$100
!
Створити обліковий запис