Skip to content

พิสูจน์การทำงาน HashWX

HashWX คือโปรโตคอล challenge เริ่มต้นของ Cap แทนที่จะใช้ฟังก์ชันแฮชตายตัวอย่าง SHA-256 challenge ทุกข้อจะสร้างฟังก์ชันทางเดียว ตัวใหม่ ขึ้นมาจาก seed โดยประกอบจากการดำเนินการกับจำนวนเต็มและการแตกกิ่ง ซึ่งเลือกมาให้ GPU รันได้ไม่เร็วกว่า CPU มากนัก

ผู้ออกแบบคือ tevador คนเดียวกับที่เขียน RandomX และ HashX ส่วน Cap ก็รวมบิลด์ WebAssembly ฉบับอ้างอิงมาให้ในตัว

TIP

HashWX เป็นค่าเริ่มต้นของคีย์ Standalone ที่สร้างใหม่ ส่วนใน cap-core ต้องเลือกเปิดเองผ่าน API แบบ format 2 และที่นั่น proof of work แบบ SHA-256 ยังเป็นค่าเริ่มต้นอยู่ ดู challenge แบบ HashWX

ทำไมต้อง HashWX

ปัญหาของ proof of work แบบ SHA-256 คือปริมาณงานต่อวินาที GPU รันฟังก์ชันตายตัวตัวเดียวกันพร้อมกันนับพันเลนอย่างสอดประสาน จึงแก้ challenge ได้ต่อวินาทีมากกว่า CPU อยู่มาก และนั่นคือตัวเลขที่สำคัญจริง ๆ สำหรับการกันบอท: ผู้โจมตีไม่สนว่า challenge ข้อเดียวใช้เวลานานแค่ไหน สนแค่ว่าเคลียร์ได้กี่ข้อต่อชั่วโมง

ก่อนหน้านี้ Cap ใช้ปริศนา time-lock แบบ RSW เพื่อการนี้ RSW ชนะในแง่ เวลาแฝง เพราะการยกกำลังสองตามลำดับภายในปริศนาข้อเดียวทำขนานไม่ได้ แต่แพ้ขาดในแง่ ปริมาณงานต่อวินาที เพราะ GPU รันปริศนาที่เป็นอิสระต่อกันพร้อมกันได้เป็นพันข้อ เมื่อวัดเทียบกับ GPU ระดับผู้ใช้ทั่วไป:

อัลกอริทึมCPU, Ryzen 3700X, 16 เธรดGPU, RTX 5060 TiGPU ได้เปรียบ
SHA-25641 MH/s6150 MH/s~150x
RSW26 H/s4400 H/s~170x
HashWX2.8 MH/s5.8 MH/s~2x

ตัวเลขชุดนี้เป็นของ tevador ที่ใช้โค้ด CUDA ของเขาเองซึ่งไม่ได้เปิดเผย เราวัดแถว RSW ซ้ำเองแยกต่างหาก: M3 วัดได้ 2.112 H/s ต่อเธรด ซึ่งคิดเป็น 26 H/s สำหรับ 16 เธรดของ Ryzen

RSW ถูกเลิกใช้แล้ว มันยังเลือกใช้ได้รายคีย์และคีย์เดิมยังทำงานต่อได้ แต่ไม่ควรใช้กับระบบที่ติดตั้งใหม่

โปรโตคอลทำงานอย่างไร

การสร้าง challenge

เซิร์ฟเวอร์สุ่ม 32 ไบต์มาเป็น challenge C แล้วกำหนดความยาก d ตรงนี้ไม่มีสาระกุญแจและไม่มีการคำนวณล่วงหน้า การสร้าง challenge จึงเป็นแค่การอ่านค่าสุ่มหนึ่งครั้งกับการเซ็น JWT หนึ่งครั้ง

ไคลเอนต์จะได้รับ C, d และ n ซึ่งคือจำนวน nonce ที่ฟังก์ชันแต่ละตัวที่สร้างขึ้นครอบคลุม

การแก้ฝั่งไคลเอนต์

ไคลเอนต์ต้องหา nonce ขนาด 64 บิต N ที่ทำให้

H(N) <= (2^64 - 1) / d      where H = hashwx_make(sha256(C || u64le(N / n)))

nonce ที่ต่อเนื่องกัน n ตัวในหนึ่งบล็อกจะใช้ฟังก์ชันแฮชที่สร้างขึ้นร่วมกันหนึ่งตัว ไคลเอนต์สร้างฟังก์ชันนั้น รันมันทั้งบล็อก แล้วขยับไปบล็อกถัดไปถ้าไม่มีค่าไหนตกลงไปต่ำกว่าเป้าหมาย งานที่คาดหวังคือการแฮช d ครั้ง

Cap ใช้ n = 65536 ส่วนโปรโตคอลอ้างอิงใช้ 463 สำหรับไคลเอนต์เนทีฟ แต่ในเบราว์เซอร์ต้อง JIT คอมไพล์ฟังก์ชันผ่าน WebAssembly.Module ใหม่ทุกบล็อก บล็อกที่ใหญ่ขึ้นจึงช่วยเฉลี่ยต้นทุนส่วนนั้น tevador ระบุข้อแลกเปลี่ยนนี้ไว้ชัดเจน: ยิ่ง nonce ต่อฟังก์ชันมากขึ้น โปรโตคอลก็ยิ่งเสี่ยงต่อเคอร์เนล GPU ที่ JIT คอมไพล์มากขึ้น และสิ่งที่ยังคงความต้านทาน GPU ไว้ที่ค่านี้คือการแตกกิ่งที่แยกทางกัน ตัวเลข ~2x ข้างบนวัดที่ 65536 จึงรวมผลข้อนี้ไว้แล้ว

อะไรทำให้ต้านทาน GPU

สี่คุณสมบัติ ทั้งหมดมาจากเอกสารการออกแบบ:

แต่ละอินสแตนซ์คือโปรแกรม 32 ตัว แต่ละตัวเป็นลูปที่กระโดดกลับไปยังจุดเริ่มต้นของตัวเองด้วยความน่าจะเป็น 1/2 ซึ่งคิดออกมาได้พอดี 256 การแตกกิ่งต่อการแฮชหนึ่งครั้ง บน CPU นั่นคือการทำนายผิดไม่กี่ครั้ง แต่บน GPU มันทำให้ warp แตกออกเป็นเส้นทางที่แยกจากกัน และต้องรันทีละเส้นทาง

มี scratchpad ขนาด 16 KB และการอ่านจากมันถูกจงใจให้ไม่จัดแนว CPU เก็บมันไว้ใน L1 แล้วซ่อนเวลาแฝง 3 ถึง 4 รอบสัญญาณนาฬิกาด้วยการจัดลำดับคำสั่งใหม่ ส่วน GPU ต้องเก็บไว้ใน local memory ที่หนุนด้วย L2 ซึ่งใช้ราว 100 รอบ แล้วสถาปัตยกรรม GPU ส่วนใหญ่ยังต้องจำลองการอ่านแบบไม่จัดแนวด้วยการรวมการอ่านสองครั้งที่อยู่ติดกัน

รีจิสเตอร์ต้นทางถูกเลือกจากรายการ "ตื้น" และ "ลึก" ที่สลับกันอยู่ CPU จึงเจอการอ่านที่ขึ้นต่อกันเฉลี่ย 2.75 ครั้ง ส่วนตัวแปลคำสั่งบน GPU ต้องปรับให้เข้ากับรายการลึก เพราะราว 95% ของกรณีจะมีอย่างน้อยหนึ่งเธรดใน warp ที่รันโปรแกรมแบบลึก มันจึงต้องรับโซ่การอ่านที่ขึ้นต่อกันครบ 6 ชั้นทุกครั้ง

ชุดคำสั่งถูกจำกัดไว้เท่าที่ WebAssembly 1.0 มีให้: การคูณ บวก ลบ XOR OR หมุนบิต และเลื่อนบิตขนาด 64 บิต พร้อมค่าคงที่ 6 บิต นี่แหละคือสิ่งที่ทำให้อัลกอริทึมเดียวกันรันในเบราว์เซอร์ได้

การตรวจสอบฝั่งเซิร์ฟเวอร์

เซิร์ฟเวอร์คำนวณ seed ใหม่จาก C และดัชนีบล็อกที่ได้จาก nonce ที่ส่งมา สร้างฟังก์ชันแฮชตัวนั้นขึ้นมาหนึ่งตัว รันหนึ่งครั้ง แล้วเทียบกับเป้าหมาย การสร้างโปรแกรมถูกกว่า HashX ราว 5 เท่า นั่นคือเหตุผลที่การตรวจสอบอยู่ในระดับหลักสิบไมโครวินาที

ต้นทุน

ต้นทุนฝั่งเซิร์ฟเวอร์ต่อ challenge หนึ่งข้อ วัดบนคอร์เดียวของ Apple M3 เป็นค่ามัธยฐานจากการสร้าง 200 ครั้งและการตรวจสอบ 40 ครั้งผ่าน validateChallenge:

โปรโตคอลการสร้างการตรวจสอบรวม
HashWX, 1 challenge14 µs40 µs54 µs
HashWX, 4 challenge ย่อย (ค่าเริ่มต้น)20 µs129 µs149 µs
SHA-256 (50 challenge, ความยาก 4)4 µs83 µs87 µs
RSW (t = 75,000)1522 µs14 µs1536 µs

challenge ของ HashWX แบบเดียวมีต้นทุนไป-กลับถูกที่สุดในสามตัวนี้ ค่าเริ่มต้นที่แบ่งเป็นสี่ challenge ย่อยใช้ราว 150 µs มากกว่า SHA-256 แต่เป็นหนึ่งในสิบของ RSW ซึ่งต้องจ่ายค่ายกกำลังมอดุลาร์จริงสี่ครั้งทุกครั้งที่สร้าง ความยากไม่มีผลกับตัวเลขเหล่านี้ เพราะการตรวจสอบคือการแฮชหนึ่งครั้งต่อหนึ่ง challenge ย่อย ไม่ว่าจะหามายากแค่ไหน

ต้นทุนฝั่งไคลเอนต์คืออีกด้านของการแลกเปลี่ยนนี้ challenge เดียวมีเวลาแก้ที่กระจายแบบเอกซ์โพเนนเชียล จึงเหมือนการจับสลาก ความยากเท่ากันอาจใช้ 30 มิลลิวินาทีสำหรับผู้เยี่ยมชมคนหนึ่ง และสามวินาทีสำหรับคนถัดไป Cap จึงแบ่งความยากออกเป็นสี่ challenge ย่อยเป็นค่าเริ่มต้น เพื่อให้เวลาแก้สม่ำเสมอขึ้น วัดผ่านวิดเจ็ตใน Chrome รุ่นเสถียรบน M3 8 คอร์ ชุดละ 72 ครั้ง โดยปรับความยากทั้งสองแบบให้มีค่ามัธยฐานใกล้เคียงกัน:

1 challenge, d = 1,330,0004 challenge ย่อย, d = 1,000,000
ค่ามัธยฐาน536 ms490 ms
p901447 ms778 ms
ช้าที่สุดใน 72 ครั้ง2378 ms1490 ms

คอลัมน์ขวาคือค่าเริ่มต้น เมื่อวัดกับคีย์ของ Standalone ได้ค่ามัธยฐาน 578 มิลลิวินาที และ p90 0.9 วินาที

สำหรับความยากที่กำหนด การแบ่งไม่ได้เปลี่ยนสิ่งที่ผู้โจมตีต้องจ่าย เพราะงานที่คาดหวังยังคงเป็น d แฮชไม่ว่าจะแบ่งอย่างไร สิ่งที่เปลี่ยนคือรูปร่างของการกระจาย ค่ามัธยฐานของ challenge เดียวอยู่ที่ 0.69 เท่าของค่าเฉลี่ย ส่วนสี่ challenge ย่อยจะดันค่ามัธยฐานไปอยู่ราว 0.92 เท่าของค่าเฉลี่ยและทำให้หางสั้นลง ดังนั้นที่ความยากเท่ากัน การแบ่งทำให้การแก้ทั่วไปช้าลงราวหนึ่งในสาม และลด p90 ลงราวหนึ่งในสี่ ตารางด้านบนตรึงค่ามัธยฐานไว้แทน ซึ่งทำให้แบบแบ่งใช้ความยากน้อยลง 25% และผู้โจมตีต้องทำงานน้อยลง 25% ต่อการแก้หนึ่งครั้ง overhead ของวิดเจ็ตเองมีน้อย worker จะตรวจทุก 16 มิลลิวินาทีว่าต้องหยุดหรือไม่ ซึ่งเป็นตัวกำหนดว่าการส่งต่องานระหว่าง challenge ย่อยจะนานได้สูงสุดเท่าใด

เอนจินของเบราว์เซอร์

บิลด์ wasm ทำงานได้ราว 60% ของความเร็วเนทีฟ บน worker เดียวเอนจินหลักทั้งสามเร็วใกล้เคียงกัน แต่เมื่อใช้ทุกคอร์ Safari จะตามหลัง วัดบน M3 เครื่องเดียวกันในรอบเดียวกัน โดยเบราว์เซอร์แต่ละตัวรันแบบ headless หรืออยู่หน้าสุด:

เอนจิน1 worker8 workerการถอยไปใช้โหมดตีความ
Chrome 153440 KH/s2050 KH/s94 KH/s
Firefox 156420 KH/s1850 KH/s105 KH/s
Safari 27.2410 KH/s1480 KH/s105 KH/s

บน worker เดียว ทั้งสามต่างกันไม่เกิน 5% บนแปด worker Safari ทำได้ราว 70% ของอัตราของ Chrome ดังนั้นให้ตั้งความยากโดยอิง Safari ที่ d = 1,000,000 เวลาเฉลี่ยจะอยู่ราว 0.5 วินาทีบน Chrome และ 0.7 วินาทีบน Safari

ถ้าจะวัดเอง ให้ใช้บิลด์รุ่นเสถียรของแต่ละเบราว์เซอร์และเปิดแท็บไว้หน้าสุด Firefox ที่มาพร้อม Playwright รันงาน WebAssembly ทุกชนิดช้ากว่า Firefox รุ่นเสถียรสามถึงหกเท่า ไม่ใช่แค่ HashWX และแท็บที่อยู่เบื้องหลังอาจถูกย้ายไปรันบนคอร์ประหยัดพลังงาน ทำให้ตัวเลขทุกตัวลดลงครึ่งหนึ่ง

โทรศัพท์มือถือ

ค่าเริ่มต้นบนโทรศัพท์จริงผ่าน BrowserStack เครื่องละ 15 ครั้ง และ 30 ครั้งสำหรับ Vivo ทุกรอบเริ่มจากหน้าที่เพิ่งโหลดใหม่ เหมือนผู้เยี่ยมชมที่เพิ่งเข้ามา หลังจากหน้าเว็บรัน HashWX ไปแล้วหนึ่งนาที Pixel 6 ใช้เวลาต่อการแก้หนึ่งครั้งน้อยลง 24% และ Vivo น้อยลง 29% ดังนั้นการวัดที่วอร์มอัปก่อนจะได้ตัวเลขดีกว่าในตารางนี้

อุปกรณ์ระบบทุกคอร์ค่ามัธยฐานช้าที่สุด
Galaxy S24Android 141238 KH/s1.1 วินาที1.8 วินาที
Pixel 9Android 15837 KH/s1.4 วินาที2.0 วินาที
Pixel 6Android 12746 KH/s1.9 วินาที2.4 วินาที
iPhone 15iOS 17ไม่ได้วัด1.9 วินาที4.3 วินาที
iPhone 12iOS 17620 KH/s2.0 วินาที3.3 วินาที
iPhone 13iOS 15678 KH/s2.2 วินาที4.1 วินาที
iPhone SE 2022iOS 15588 KH/s2.4 วินาที4.0 วินาที
Redmi Note 11Android 11456 KH/s2.4 วินาที4.8 วินาที
Galaxy M32Android 11444 KH/s2.8 วินาที6.1 วินาที
Vivo Y21Android 11316 KH/s5.9 วินาที11.0 วินาที

การแก้แต่ละครั้งรวมการรับส่งข้อมูลไป-กลับกับเซิร์ฟเวอร์ทดสอบสองครั้ง โดยค่ามัธยฐานของแต่ละเครื่องอยู่ที่ 80 ถึง 190 มิลลิวินาที Vivo ซึ่งเป็น Android ราคาประหยัดใช้เวลาราวสิบเท่าของเดสก์ท็อป M3 ถ้าทราฟฟิกส่วนใหญ่ของคุณมาจากมือถือ ให้ลดความยากลง เวลาแก้แปรผันตรงกับความยาก ดังนั้น 500,000 จะทำให้ส่วนที่ใช้คำนวณของตัวเลขทุกตัวในตารางนี้ลดลงราวครึ่งหนึ่ง

ไคลเอนต์ที่ไม่มี WebAssembly แก้ HashWX ไม่ได้เลยและจะได้รับข้อผิดพลาด ถ้าคุณต้องรองรับพวกเขา ให้ใช้ proof of work แบบ SHA-256 ซึ่งมีทางถอยเป็น JS ล้วน บน iPhone วิดเจ็ตต้องใช้ iOS 15 ขึ้นไป

สิ่งที่ HashWX ไม่ได้ป้องกัน

เหมือนกับที่ RSW ป้องกันไม่ได้: ชิปเฉพาะทาง FPGA หรือ ASIC ที่สร้างมาเพื่อสิ่งนี้ยังเอาชนะ CPU ได้อยู่ดี ในเชิงเศรษฐศาสตร์มันไม่คุ้มกับการทำฟาร์ม CAPTCHA เพราะ ASIC มีต้นทุนออกแบบหลายล้าน แต่นี่ก็ไม่ใช่การรับประกันเชิงการเข้ารหัสลับ

และมันก็หยุดฟาร์มที่ใช้คนแก้ไม่ได้ ทั้งตอนนี้และตลอดไป proof of work ทำได้แค่ดันต้นทุนต่อคำขอให้สูงขึ้น ใช้มันคู่กับ challenge แบบ instrumentation เพื่อให้ต้องมีสภาพแวดล้อมเบราว์เซอร์จริงด้วย

ลองใช้งาน

API ของ cap-core มีเอกสารอยู่ที่ challenge แบบ HashWX ตัววิดเจ็ตตรวจจับการตอบกลับแบบ format 2 ได้เอง แค่อัปเกรดเซิร์ฟเวอร์ก็พอ

บน Cap Standalone HashWX เป็นค่าเริ่มต้นของคีย์ที่สร้างใหม่ และสลับได้รายคีย์ในแดชบอร์ด

เผยแพร่ภายใต้สัญญาอนุญาต Apache 2.0