Solidity 102 #1: เขียนโค้ดอย่างไรไม่ให้ Gas cost บานปลาย

Bun Uthaitirat
Band Protocol Thailand
3 min readJun 17, 2019

--

บทความนี้แปลมาจากบทความ ที่เขียนโดย @sorawit จาก Band Protocol

บทความนี้เป็นบทความแรกในซี่รีส์ “Solidity 102” ซึ่งเขียนโดย Band Protocol มีเนื้อหาเกี่ยวกับ data structure และเทคนิคในการเขียนโค้ด solidity อย่างมีประสิทธิภาพบน EVM ซี่รีส์นี้จึงเหมาะกับผู้อ่านที่พอคุ้นเคยกับภาษา solidity และเข้าใจคร่าวๆว่า EVM ทำงานอย่างไร

อย่างที่เราทราบๆกัน ทุกสิ่งทุกอย่างในโลกล้วนไม่มีคำว่าฟรี ยิ่งในโลกของ Ethereum ยิ่งชัดเจนถึงแม้ว่าการอ่านค่าอาจจะฟรี แต่เมื่อถึงเวลาที่ต้องการทำธุรกรรม หรือเปลี่ยนแปลงอะไรสักอย่างบน blockchain ย่อมมีค่าใช้จ่ายหรือที่เรียกว่า gas นั่นเอง ซึ่งการคิดคำนวณ gas ในการทำงานบางอย่างบน EVM (Ethereum Virtual Machine) เมื่อเปรียบเทียบกับภาษาอื่นๆ (คิดในแง่ของ perfomance) นั้นแตกต่างกันอย่างมาก ดังนั้นเพื่อที่จะเขียน Solidity ให้มีประสิทธิภาพมากขึ้น เราจึงจำเป็นต้องเข้าใจถึงข้อจำกัดของ EVM และเรียนรู้เทคนิคในการเขียน

โดยบทความนี้จะอธิบายถึงสาเหตุหลักๆที่ทำให้เราต้องใช้ gas ในการทำ transaction สูงเกินความจำเป็นและวิธีแก้ปัญหาเหล่านั้นแบบคร่าวๆ ส่วนเรื่องรายละเอียดหรือการเขียนโค้ดจริงๆ จะอยู่ในตอนต่อๆไป ของซีรีส์นี้นะครับ

Interaction with Persistent Storage

ค่า gas ของแต่ละ opcode สามารถดูได้จาก Ethereum Yellow Paper Appendix G

เมื่อเราทำการเก็บข้อมูลบน Blockchain จะมีการเรียกใช้ Opcode ชื่อ SSTORE เกิดขึ้น ซึ่งใช้ gas เยอะมาก ณ ปัจจุบันการเขียนข้อมูลครั้งแรก (จองพื้นที่) ใช้ 20,000 gas ต่อการเก็บ 32-bytes word 1 คำ (คิดเป็นเงินไทยก็ประมาณ 1.50 บาทที่ gas price 10 Gwei) และ 5,000 gas ต่อการแก้ไขในแต่ละครั้ง ในขณะที่ค่าใช้จ่ายในการกระทำอย่างอื่นเช่น การคำนวณพื้นฐานหรือ การจองพื้นที่ในหน่วยความจำชั่วคราว(memory) นั้นถูกกว่าการเขียนลง storage มากๆ เมื่อรวมกับอีกข้อจำกัดอีกข้อ นั่นคือ gas limit ต่อบล็อคซึ่งปัจจุบัน (มิถุนายน 2562) อยู่ที่ 8,000,000 gas ทำให้ผู้พัฒนา smart contract ต้องคิดว่าจะเขียนโค้ดอย่างไรให้มีการใช้ storage ให้น้อยที่สุด และในอนาคตการใช้ storage แบบไม่จำเป็นจะยิ่งส่งผลร้ายมากขึ้นจากกฏใหม่ที่กำลังจะถูกเพิ่มเข้ามา(?) นั่นคือ การเช่าหน่วยความจำ(state rent) แต่ไม่ต้องตกใจไป มีหลายวิธีที่ช่วยแก้ปัญหานี้ได้

Don’t Store Unnecessary Data

วิธีนี้มักเป็นวิธีที่หลายคนมองข้าม แต่มีความสำคัญและทำได้ง่ายที่สุดจึงอยากหยิบมาเล่าให้ฟัง การเขียน smart contract ที่ดี เราควรเก็บเฉพาะข้อมูลที่จำเป็นในการตรวจสอบว่า transaction ในอนาคตสามารถทำได้หรือไม่ ส่วนข้อมูลที่ไม่จำเป็นเช่น โน้ตว่าทำอะไรไปหรือคำอธิบายยาวๆ ที่ไม่เกี่ยวข้องกับ logic ในโค้ด ก็ไม่ควรเก็บไว้ใน storage ลองดูตัวอย่างจาก contract PollContract ข้างล่างนี้ ที่เอาไว้ให้คนมาสร้างโพล แล้วให้ทุกคนมาโหวตเห็นด้วยหรือไม่เห็นด้วย

ตัวอย่าง struct Poll ที่ทีตัวแปรที่ไม่จำเป็น

จากโค้ดถ้าฟังก์ชั่น createPoll ถูกเรียกบ่อยๆ ก็ควรเอา memo ออกไปจาก struct Poll เพราะมันไม่เกี่ยวกับการตัดสินผลโหวตโดยตรง ยิ่งไปกว่านั้น memo ก็ได้ประกาศออกไปผ่านทาง event อยู่แล้วดังนั้นเก็บแค่ hash value(32 bytes) ของ memo ไว้ใน blockchain เพื่อใช้ในการตรวจสอบในอนาคตก็เพียงพอ แต่การเก็บแค่ hash อาจจะทำให้การหาข้อมูลยากขึ้น เช่น อนาคตเราอยากรู้จริงๆว่า memo คืออะไรก็จะไม่สามารถทราบได้ ทำให้เป็นเรื่องที่ต้องคิดว่าจะข้อมูลแต่ละอย่างควรเก็บหรือไม่เก็บแล้วเก็บแค่ hash พอ

Pack Multiple Small Variables into Single Word

EVM ทำงานบนโมเดล 32-byte word memory กล่าวคือแต่ละช่องสามารถเก็บข้อมูลได้ 32 byte และคิดค่าใช้จ่ายเป็นช่องๆที่เก็บ การนำตัวแปรที่ขนาดน้อยกว่า 32 byte หลายๆตัวมาแพครวมเป็นช่องๆเดียวเพื่อใช้การเรียก opcode SSTORE น้อยที่สุด ถึงแม้ว่าตัวภาษา Solidity จะพยายามแพคตัวแปรเล็กๆลงช่องเดียวกันโดยอัตโนมัติ แต่การเรียงตัวแปรใน struct อาจจะทำให้การ optimize ของ compiler ไม่สามารถทำได้ ลองพิจารณาตัวอย่างข้างล่าง

ตัวอย่างการเขียน Good/ Bad struct ในการเรียงตัวแปร

หลังจากคอมไพล์โค้ดด้วย solc 0.5.9+commit.e560f70dและเปิดการใช้ optimization โดยหลังจากรันฟังก์ชั่น doBad()ใช้ gas ประมาณ 60,000 ในการรัน ขณะที่ฟังก์ชั่น doGood() ใช้แค่ 40,000 gas สังเกตว่า 20,000 gas ที่ต่างกันเกิดจาก struct Good แพคตัวแปร 2 ตัวเป็นคำเดียวทำให้ประหยัดกว่า

จำนวน gas ที่ใช้ในการรันฟังก์ชั่น doBad
จำนวน gas ที่ใช้ในการรันฟังก์ชั่น doGood

Only Store Merkle Root as the State

เป็นวิธีที่ค่อนข้างสุดโต่ง คือเก็บแค่ Merkle Root ของ state ทั้งหมดคำเดียวลงบน blockchain ทำให้เป็นหน้าที่ของคนที่เรียก transaction ที่ต้องส่งค่าและ proof เพื่อยืนยันว่าข้อมูลที่บอกมีความถูกต้อง ตัว contract ทำหน้าที่แค่ตรวจสอบ proof ที่ได้รับว่าถูกต้องหรือไม่ และหลังจากทำการอัพเดต state ใหม่จากนั้นคำนวณ Merkle root hash ใหม่แล้วนำไปแทนที่ของเก่า โดยที่ทำเพียงแก้ state แค่หนึ่งคำ นั่นคือใช้ gas แค่ 5000 gas เท่านั้น!

Potentially Unbounded Iterations

เนื่องจากภาษา Solidity เป็น turing complete ทำให้โปรแกรมสามารถเขียน while(true) ยกตัวอย่างเช่นฟังก์ชั่นที่ลูปบน user ทุกคนหรือลูปบนของทุกอย่างบน list ซึ่งการรันแต่ละครั้งเราไม่ทราบว่าต้องลูปของทั้งหมดกี่ชิ้น ดังนั้นการหลีกเลี่ยงการลูปบน list เป็นสิ่งที่ควรทำถ้าเราต้องการทราบและจำกัด gas cost ในการทำงานนั้นๆได้ โดยวิธีหลีกเลี่ยงก็มีได้หลายแบบ ได้แก่

Off-Chain Computation + On-Chain Verification

เริ่มจากปัญหาง่ายๆ เรามี linked-lists ของตัวเลขเรียงจากน้อยไปมากอยู่ การเพิ่มหรือเอาออกก็เพียงแค่หาตำแหน่งที่ต้องการ แล้วเปลี่ยน pointer ซึ่งสามารถทำได้ง่าย แต่ปัญหาก็คือเราจะหาตำแหน่งที่ถูกได้อย่างไร วิธีแรกทำแบบไม่คิดมาก เราเริ่มลูปจากตัวแรกและหาตัวที่มากกว่าตัวที่ต้องการตัวแรกและก็ใส่ของลงไป แต่นั่นก็เท่ากับว่าเราต้องอ่านข้อมูลตั้งแต่ตัวแรกถึงตำแหน่งที่ต้องการ และการทำวิธีนี้บน blockchain ก็เสีย gas ไม่น้อย เมื่อเทียบกับเราเก็บลิสไว้นอก blockchain ไปด้วยพอเวลาจะใส่ของให้บอกด้วยว่าจะไปใส่ตรงไหน ตัวโค้ดใน blockchain แค่เช็คว่าตัวที่ใส่อยู่ระหว่างกลางที่จะใส่หรือไม่ ถ้าใส่ได้ก็ใส่ ถ้าผิดก็ revert transaction นั้นทิ้งก็จบ จะเห็นว่าเราอ่านข้อมูลจริงๆแค่ 2 ตัวทำให้เรารู้แน่ๆว่าการใส่ของใช้ gas เท่าไร ถ้าผู้อ่านสนใจเกี่ยวกับเรื่องนี้สามารถอ่านต่อได้ที่บทความนี้เลย B9lab

วนลูปในลิสบน blockchain ซึ่งใช้ operation เป็น O(n) ที่จะสเกลไปตามไซส์ของลิส VS คิดตำแหน่งที่ของชิ้นนี้จะไปอยู่โดย off-chain แล้วให้ตัว chain ตรวจสอบว่าถูกหรือไม่ ทำให้ใช้ operation ในการใส่ของไม่ขึ้นกับขนาดของลิส

Break One Complex Transaction into Multiple Smaller Ones

ระหว่างการลูปเพื่อทำแอคชั่นให้กับ user ทุกๆคนในหนึ่ง transaction กับการให้ smart contract มี mapping เก็บว่าแต่ละ user ทำแอคชั่นนั้นแล้วหรือยัง แล้วให้เป็นหน้าที่ของ user แต่ละคนส่ง transaction มาทำแอคชั่นนั้นเอง โดยตัว smart contract ทำหน้าที่ตรวจสอบว่า user จะไม่ทำแอคชั่นนั้นซ้ำ ทำให้แบบหลังในแต่ละ transaction ใช้ gas เท่าๆเดิม ไม่ได้มากขึ้นตามจำนวน user เหมือนแบบแรก ทำให้กำจัดโอกาสที่ gas จะไม่พอในการทำ transaction แต่แบบหลังก็มีข้อเสีย เพราะจำนวน gas ทั้งหมดที่ทำเมื่อเอามารวมกันมีค่ามากกว่าแบบแรกที่ทำทุกอย่างใน transaction เดียว

การส่ง transaction ใหญ่เพียงอันเดียวเพื่อแจกรางวัลให้ทุกคน VS แต่ละคนส่ง transaction แยกเพื่อขอรางวัล

สรุปส่งท้าย

จากเนื้อหาที่ผ่านมาเราพูดถึงวิธีการเขียนโปรแกรมในภาษา Solidity หลายๆแบบที่ทำให้ gas cost สูงกว่าความจำเป็นหรืออาจถึงขั้นไม่สามารถรันฟังก์ชั่นนั้นได้เลยเพราะ block gas limit ไม่พอ แต่ทั้งหมดเป็นเพียงแนวคิดคราวๆในการปรับปรุง contract สำหรับในบทความถัดไปจะว่าด้วยเรื่องนำแนวคิดเหล่านี้ไปใช้จริง อย่าลืมติดตามกันนะครับ

Band Protocol เป็นแพลตฟอร์มที่สนับสนุนให้ทุกคนมีส่วนร่วมในการสร้างข้อมูลที่ถูกต้องเข้าสู่โลก blockchain โดยพวกเราเป็นทีม developer ที่อยากจะเชื่อมต่อโลกความจริงกับโลกของ blockchain เข้าด้วยกัน และถ้าคุณเป็น developer ที่มีความสนใจในด้านนี้และอยากร่วมงานกับเรา ติดต่อเราได้ที่ talent@bandprotocol.com

--

--