您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“Solidity運(yùn)行原理是什么”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
作為一門面向智能合約的語言,Solidity與其他經(jīng)典語言既有差異也有相似之處。
一方面,服務(wù)于區(qū)塊鏈的屬性使其與其他語言存在差異。例如,合約的部署與調(diào)用均要經(jīng)過區(qū)塊鏈網(wǎng)絡(luò)確認(rèn);執(zhí)行成本需要被嚴(yán)格控制,以防止惡意代碼消耗節(jié)點(diǎn)資源。
另一方面,身為編程語言,Solidity的實(shí)現(xiàn)并未脫離經(jīng)典語言,比如Solidity中包含類似棧、堆的設(shè)計(jì),采用棧式虛擬機(jī)來進(jìn)行字節(jié)碼處理。
與其他語言一樣,Solidity的代碼生命周期離不開編譯、部署、執(zhí)行、銷毀這四個(gè)階段。下圖整理展現(xiàn)了Solidity程序的完整生命周期:
經(jīng)編譯后,Solidity文件會生成字節(jié)碼。這是一種類似jvm字節(jié)碼的代碼。部署時(shí),字節(jié)碼與構(gòu)造參數(shù)會被構(gòu)建成交易,這筆交易會被打包到區(qū)塊中,經(jīng)由網(wǎng)絡(luò)共識過程,最后在各區(qū)塊鏈節(jié)點(diǎn)上構(gòu)建合約,并將合約地址返還用戶。
當(dāng)用戶準(zhǔn)備調(diào)用該合約上的函數(shù)時(shí),調(diào)用請求同樣也會經(jīng)歷交易、區(qū)塊、共識的過程,最終在各節(jié)點(diǎn)上由EVM虛擬機(jī)來執(zhí)行。
下面是一個(gè)示例程序,我們通過remix探索它的生命周期。
pragma solidity ^0.4.25; contract Demo{ uint private _state; constructor(uint state){ _state = state; } function set(uint state) public { _state = state; } }
源代碼編譯完后,可以通過ByteCode按鈕得到它的二進(jìn)制:
608060405234801561001057600080fd5b506040516020806100ed83398101806040528101908080519060200190929190
還可以得到對應(yīng)的字節(jié)碼(OpCode):
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x40 MLOAD PUSH1 0x20 DUP1 PUSH2 0xED DUP4 CODECOPY DUP2 ADD DUP1 PUSH1 0x40 MSTORE DUP2 ADD SWAP1 DUP1 DUP1 MLOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP PUSH1 0xA4 DUP1 PUSH2 0x49 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x60FE47B1 EQ PUSH1 0x44 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x4F JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x6C PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x6E JUMP JUMPDEST STOP JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0x4e 0xd9 MOD DIFFICULTY 0x4c 0xc4 0xc9 0xaa 0xbd XOR EXTCODECOPY MSTORE 0xb2 0xd4 DUP7 0xdf 0xc5 0xde 0xa9 DUP1 SLT PUSH1 0xC3 CALLDATACOPY XOR 0x5d 0xad KECCAK256 0xe1 0x1f DUP2 SHL STOP 0x29
其中下述指令集為set函數(shù)對應(yīng)的代碼,后面會解釋set函數(shù)如何運(yùn)行。
JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP
編譯完后,即可在remix上對代碼進(jìn)行部署,構(gòu)造參數(shù)傳入0x123:
部署成功后,可得到一條交易回執(zhí):
點(diǎn)開input,可以看到具體的交易輸入數(shù)據(jù):
上面這段數(shù)據(jù)中,標(biāo)黃的部分正好是前文中的合約二進(jìn)制;而標(biāo)紫的部分,則對應(yīng)了傳入的構(gòu)造參數(shù)0x123。
這些都表明,合約部署以交易作為介質(zhì)。結(jié)合區(qū)塊鏈交易知識,我們可以還原出整個(gè)部署過程:
客戶端將部署請求(合約二進(jìn)制,構(gòu)造參數(shù))作為交易的輸入數(shù)據(jù),以此構(gòu)造出一筆交易
交易經(jīng)過rlp編碼,然后由發(fā)送者進(jìn)行私鑰簽名
已簽名的交易被推送到區(qū)塊鏈上的節(jié)點(diǎn)
區(qū)塊鏈節(jié)點(diǎn)驗(yàn)證交易后,存入交易池
輪到該節(jié)點(diǎn)出塊時(shí),打包交易構(gòu)建區(qū)塊,廣播給其他節(jié)點(diǎn)
其他節(jié)點(diǎn)驗(yàn)證區(qū)塊并取得共識。不同區(qū)塊鏈可能采用不同共識算法,F(xiàn)ISCO BCOS中采用PBFT取得共識,這要求經(jīng)歷三階段提交(pre-prepare,prepare, commit)
節(jié)點(diǎn)執(zhí)行交易,結(jié)果就是智能合約Demo被創(chuàng)建,狀態(tài)字段_state的存儲空間被分配,并被初始化為0x123
根據(jù)是否帶有修飾符view,我們可將函數(shù)分為兩類:調(diào)用與交易。由于在編譯期就確定了調(diào)用不會引起合約狀態(tài)的變更,故對于這類函數(shù)調(diào)用,節(jié)點(diǎn)直接提供查詢即可,無需與其他區(qū)塊鏈節(jié)點(diǎn)確認(rèn)。而由于交易可能引起狀態(tài)變更,故會在網(wǎng)絡(luò)間確認(rèn)。
下面將以用戶調(diào)用了set(0x10)為假設(shè),看看具體的運(yùn)行過程。
首先,函數(shù)set沒有配置view/pure修飾符,這意味著其可能更改合約狀態(tài)。所以這個(gè)調(diào)用信息會被放入一筆交易,經(jīng)由交易編碼、交易簽名、交易推送、交易池緩存、打包出塊、網(wǎng)絡(luò)共識等過程,最終被交由各節(jié)點(diǎn)的EVM執(zhí)行。
在EVM中,由SSTORE字節(jié)碼將參數(shù)0xa存儲到合約字段_state中。該字節(jié)碼先從棧上拿到狀態(tài)字段_state的地址與新值0xa,隨后完成實(shí)際存儲。
下圖展示了運(yùn)行過程:
這里僅粗略介紹了set(0xa)是如何運(yùn)行,下節(jié)將進(jìn)一步展開介紹EVM的工作機(jī)制以及數(shù)據(jù)存儲機(jī)制。
由于合約上鏈后就無法篡改,所以合約生命可持續(xù)到底層區(qū)塊鏈被徹底關(guān)停。若要手動(dòng)銷毀合約,可通過字節(jié)碼selfdestruct。銷毀合約也需要進(jìn)行交易確認(rèn),在此不多作贅述。
在前文中,我們介紹了Solidity程序的運(yùn)行原理。經(jīng)過交易確認(rèn)后,最終由EVM執(zhí)行字節(jié)碼。對EVM,上文只是一筆帶過,這一節(jié)將具體介紹其工作機(jī)制。
EVM是棧式虛擬機(jī),其核心特征就是所有操作數(shù)都會被存儲在棧上。下面我們將通過一段簡單的Solidity語句代碼看看其運(yùn)行原理:
uint a = 1; uint b = 2; uint c = a + b;
這段代碼經(jīng)過編譯后,得到的字節(jié)碼如下:
PUSH1 0x1 PUSH1 0x2 ADD
為了讀者更好了解其概念,這里精簡為上述3條語句,但實(shí)際的字節(jié)碼可能更復(fù)雜,且會摻雜SWAP和DUP之類的語句。
我們可以看到,在上述代碼中,包含兩個(gè)指令:PUSH1和ADD,它們的含義如下:
PUSH1:將數(shù)據(jù)壓入棧頂。
ADD:POP兩個(gè)棧頂元素,將它們相加,并壓回棧頂。
這里用半動(dòng)畫的方式解釋其執(zhí)行過程。下圖中,sp表示棧頂指針,pc表示程序計(jì)數(shù)器。當(dāng)執(zhí)行完push2 0x1后,pc和sp均往下移:
類似地,執(zhí)行push2 0x2后,pc和sp狀態(tài)如下:
最后,當(dāng)add執(zhí)行完后,棧頂?shù)膬蓚€(gè)操作數(shù)都被彈出作為add指令的輸入,兩者的和則會被壓入棧:
在開發(fā)過程中,我們常會遇到令人迷惑的memory修飾符;閱讀開源代碼時(shí),也會看到各種直接針對內(nèi)存進(jìn)行的assembly操作。不了解存儲機(jī)制的開發(fā)者遇到這些情況就會一頭霧水,所以,這節(jié)將探究EVM的存儲原理。
在前文《智能合約編寫之Solidity的基礎(chǔ)特性》中我們介紹過,一段Solidity代碼,通常會涉及到局部變量、合約狀態(tài)變量。
而這些變量的存儲方式存在差別,下面代碼表明了變量與存儲方式之間的關(guān)系。
contract Demo{ //狀態(tài)存儲 uint private _state; function set(uint state) public { //棧存儲 uint i = 0; //內(nèi)存存儲 string memory str = "aaa"; } }
棧用于存儲字節(jié)碼指令的操作數(shù)。在Solidity中,局部變量若是整型、定長字節(jié)數(shù)組等類型,就會隨著指令的運(yùn)行入棧、出棧。
例如,在下面這條簡單的語句中,變量值1會被讀出,通過PUSH操作壓入棧頂:
uint i = 1;
對于這類變量,無法強(qiáng)行改變它們的存儲方式,如果在它們之前放置memory修飾符,編譯會報(bào)錯(cuò)。
內(nèi)存類似java中的堆,它用于儲存"對象"。在Solidity編程中,如果一個(gè)局部變量屬于變長字節(jié)數(shù)組、字符串、結(jié)構(gòu)體等類型,其通常會被memory修飾符修飾,以表明存儲在內(nèi)存中。
本節(jié)中,我們將以字符串為例,分析內(nèi)存如何存儲這些對象。
1. 對象存儲結(jié)構(gòu)
下面將用assembly語句對復(fù)雜對象的存儲方式進(jìn)行分析。
assembly語句用于調(diào)用字節(jié)碼操作。mload指令將被用于對這些字節(jié)碼進(jìn)行調(diào)用。mload(p)表示從地址p讀取32字節(jié)的數(shù)據(jù)。開發(fā)者可將對象變量看作指針直接傳入mload。
在下面代碼中,經(jīng)過mload調(diào)用,data變量保存了字符串str在內(nèi)存中的前32字節(jié)。
string memory str = "aaa"; bytes32 data; assembly{ data := mload(str) }
掌握mload,即可用此分析string變量是如何存儲的。下面的代碼將揭示字符串?dāng)?shù)據(jù)的存儲方式:
function strStorage() public view returns(bytes32, bytes32){ string memory str = "你好"; bytes32 data; bytes32 data2; assembly{ data := mload(str) data2 := mload(add(str, 0x20)) } return (data, data2); }
data變量表示str的0~31字節(jié),data2表示str的32~63字節(jié)。運(yùn)行strStorage函數(shù)的結(jié)果如下:
0: bytes32: 0x0000000000000000000000000000000000000000000000000000000000000006 1: bytes32: 0xe4bda0e5a5bd0000000000000000000000000000000000000000000000000000
可以看到,第一個(gè)數(shù)據(jù)字得到的值為6,正好是字符串"你好"經(jīng)UTF-8編碼后的字節(jié)數(shù)。第二個(gè)數(shù)據(jù)字則保存的是"你好"本身的UTF-8編碼。
熟練掌握了字符串的存儲格式之后,我們就可以運(yùn)用assembly修改、拷貝、拼接字符串。讀者可搜索Solidity的字符串庫,了解如何實(shí)現(xiàn)string的concat。
2. 內(nèi)存分配方式
既然內(nèi)存用于存儲對象,就必然涉及到內(nèi)存分配方式。
memory的分配方式非常簡單,就是順序分配。下面我們將分配兩個(gè)對象,并查看它們的地址:
function memAlloc() public view returns(bytes32, bytes32){ string memory str = "aaa"; string memory str2 = "bbb"; bytes32 p1; bytes32 p2; assembly{ p1 := str p2 := str2 } return (p1, p2); }
運(yùn)行此函數(shù)后,返回結(jié)果將包含兩個(gè)數(shù)據(jù)字:
0: bytes32: 0x0000000000000000000000000000000000000000000000000000000000000080 1: bytes32: 0x00000000000000000000000000000000000000000000000000000000000000c0
這說明,第一個(gè)字符串str1的起始地址是0x80,第二個(gè)字符串str2的起始地址是0xc0,之間64字節(jié),正好是str1本身占據(jù)的空間。此時(shí)的內(nèi)存布局如下,其中一格表示32字節(jié)(一個(gè)數(shù)據(jù)字,EVM采用32字節(jié)作為一個(gè)數(shù)據(jù)字,而非4字節(jié)):
0x40~0x60:空閑指針,保存可用地址,本例中是0x100,說明新的對象將從0x100處分配??梢杂胢load(0x40)獲取到新對象的分配地址。
0x80~0xc0:對象分配的起始地址。這里分配了字符串a(chǎn)aa
0xc0~0x100:分配了字符串bbb
0x100~...:因?yàn)槭琼樞蚍峙?,新的對象將會分配到這里。
顧名思義,狀態(tài)存儲用于存儲合約的狀態(tài)字段。
從模型而言,存儲由多個(gè)32字節(jié)的存儲槽構(gòu)成。在前文中,我們介紹了Demo合約的set函數(shù),里面0x0表示的是狀態(tài)變量_state的存儲槽。所有固定長度變量會依序放到這組存儲槽中。
對于mapping和數(shù)組,存儲會更復(fù)雜,其自身會占據(jù)1槽,所包含數(shù)據(jù)則會按相應(yīng)規(guī)則占據(jù)其他槽,比如mapping中,數(shù)據(jù)項(xiàng)的存儲槽位由鍵值k、mapping自身槽位p經(jīng)keccak計(jì)算得來。
從實(shí)現(xiàn)而言,不同的鏈可能采用不同實(shí)現(xiàn),比較經(jīng)典的是以太坊所采用的MPT樹。由于MPT樹性能、擴(kuò)展性等問題,F(xiàn)ISCO BCOS放棄了這一結(jié)構(gòu),而采用了分布式存儲,通過rocksdb或mysql來存儲狀態(tài)數(shù)據(jù),使存儲的性能、可擴(kuò)展性得到提高。
“Solidity運(yùn)行原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。