ساخت بلاکچین با نگاهی به ساختار بین کوین - قسمت دوم (الگوریتم اثبات کار یا Proof-of-Work)

Post image

مقدمه

در مقاله قبلی ما یک ساختار داده بسیار ساده ساختیم که ماهیت پایگاه داده بلاکچین است و ما امکان افزودن بلوک‌ها را به آن با رابطه زنجیره‌ای بین آنها فراهم کردیم. هر بلوک به بلوک قبلی مرتبط است. افسوس که پیاده‌سازی بلاکچین ما یک نقص مهم دارد و آن این است که افزودن بلاک‌ها به زنجیره آسان و ارزان است. یکی از اصل های بلاکچین و بیت‌کوین این است که افزودن بلاک‌های جدید کار سختی باشد. امروز قصد داریم این نقص را برطرف کنیم.

الگوریتم اثبات کار یا Proof-of-Work

یکی از ایده های کلیدی بلاکچین این است که برای قرار دادن داده ها در آن باید کار سختی انجام داد. همین کار سخت است که بلاکچین را ایمن و سازگار می کند. همچنین برای این کار سخت پاداش نیز پرداخت می شود (اینگونه است که مردم برای استخراج ، سکه دریافت می کنند).

این مکانیسم بسیار شبیه سازوکار زندگی واقعی است. فرد باید سخت کار کند تا پاداش بگیرد و زندگی خود را حفظ کند. در بلاکچین، برخی از شرکت‌کنندگان (ماینرها) شبکه برای حفظ شبکه، اضافه کردن بلوک‌های جدید به آن و دریافت پاداش برای کار خود تلاش می‌کنند. در نتیجه کار آنها، یک بلوک به روشی ایمن در بلاکچین گنجانده می شود که ثبات کل پایگاه داده بلاکچین را حفظ می کند. شایان ذکر است که کسی که کار را تمام کرده است باید این را ثابت کند.

کل این مکانیسم “کار سخت و اثبات” را اثبات کار یا proof-of-work می نامند. سخت است زیرا به قدرت محاسباتی زیادی نیاز دارد. حتی رایانه هایی با کارایی بالا نیز نمی توانند آن را به سرعت انجام دهند. علاوه بر این، سختی این کار هر از گاهی افزایش می یابد تا نرخ بلوک های جدید در حدود 6 بلوک در ساعت حفظ شود. در بیت کوین، هدف چنین کاری یافتن هش برای یک بلوک است که برخی از الزامات را برآورده می کند. و این هش است که به عنوان یک مدرک عمل می کند. بنابراین، همین یافتن دلیل و اثبات کار، یک کار واقعی است.

در آخرین نکته قابل توجه الگوریتم‌های اثبات کار ، باید یک الزام را برآورده کنند و آن اینکه انجام کار سخت است، اما تأیید اثبات آسان است. یک مدرک معمولاً به شخص دیگری تحویل داده می شود، بنابراین برای آنها، تأیید آن نباید زمان زیادی را صرف کند.

هش کردن یا Hashing

در این پاراگراف، هش کردن را مورد بحث قرار خواهیم داد. اگر با مفهوم آن آشنا هستید، می توانید از این قسمت صرف نظر کنید.

هش کردن فرآیند به دست آوردن هش برای داده های مشخص است. هش یک نمایش منحصر به فرد از داده هایی است که روی آنها محاسباتی شده است. تابع هش تابعی است که داده هایی با اندازه دلخواه را می گیرد و یک هش با اندازه ثابت تولید می کند. در اینجا برخی از ویژگی های کلیدی هش وجود دارد:

  • داده های اصلی را نمی توان از هش بازیابی کرد. بنابراین، هش رمزگذاری نیست.
  • داده ها می توانند فقط یک هش داشته باشند و هش منحصر به فرد است.
  • تغییر حتی یک بایت در داده های ورودی منجر به هش کاملاً متفاوتی می شود.

توابع هش به طور گسترده ای برای بررسی سازگاری داده ها استفاده می شود. برخی از ارائه دهندگان نرم افزار علاوه بر بسته نرم افزاری، چک سام ها (checksum) را نیز منتشر می کنند. پس از دانلود یک فایل، می توانید آن را به یک تابع درهم سازی وارد کنید و هش تولید شده را با هش ارائه شده توسط توسعه دهنده نرم افزار مقایسه کنید.

در بلاکچین، هش برای تضمین ثبات یک بلاک استفاده می شود. داده های ورودی برای یک الگوریتم هش شامل هش بلوک قبلی است، بنابراین تغییر یک بلوک در زنجیره را غیرممکن (یا حداقل بسیار دشوار) می کند و فرد باید هش آن و هش همه بلوک های بعد از آن را دوباره محاسبه کند.

هش کش یا Hashcash

بیت کوین از Hashcash استفاده می کند، یک الگوریتم اثبات کار (Proof-of-Work) که در ابتدا برای جلوگیری از هرزنامه ایمیل ایجاد شد. می توان آن را به مراحل زیر تقسیم کرد:

۱- برخی از داده‌ های شناخته شده عمومی به عنوان مثال ( در مورد ایمیل، آدرس ایمیل گیرنده است، در مورد بیت‌کوین، هدرهای بلوکی است) را می گیرد.

۲- یک شمارنده به آن اضافه می کند. شمارنده از 0 شروع می شود.

۳- یک هش از ترکیب داده + شمارنده دریافت می کنید.

۴- بررسی میکند که هش الزامات خاصی را برآورده کند.

۴-۱ - اگر این کار را کرد، کار شما تمام شده است.

۴-۲ - اگر نشد، شمارنده را افزایش دهید و مراحل 3 و 4 را تکرار کنید.

بنابراین، این یک الگوریتم brute force است. شما شمارنده را تغییر می‌دهید، یک هش جدید را محاسبه می‌کنید، آن را بررسی می‌کنید، شمارنده را افزایش می‌دهید، یک هش را محاسبه می‌کنید، و الی آخر.

به همین دلیل است که از نظر محاسباتی گران است.

اکنون بیایید به الزاماتی که یک هش باید برآورده کند، دقیق تر نگاه کنیم. در اجرای اولیه Hashcash، این الزام به نظر می رسد “20 بیت اول هش باید صفر باشد”. در بیت کوین، این نیاز هر از چند گاهی تنظیم می شود، زیرا، با وجود طراحی، هر 10 دقیقه یک بلاک باید تولید شود، علیرغم اینکه قدرت محاسباتی با زمان افزایش می یابد و ماینرهای بیشتری به شبکه می پیوندند.

برای نشان دادن این الگوریتم، داده‌های مثال قبلی (من دونات را دوست دارم “I like donuts”) گرفتم و یک هش پیدا کردم که با 3 بایت صفر شروع می‌شود (هر بایت دو کلمه است):

این ca07ca مقدار هگزادسیمال شمارنده است که در سیستم اعشاری معادل 13240266 است.

پیاده سازی

خب، ما تئوری قضیه را تمام کردیم، بیایید کد بنویسیم! ابتدا، اجازه دهید دشواری استخراج را تعریف کنیم:

const targetBits = 24

در بیت‌کوین، «بیت‌های هدف target bits » هدر بلوک است که دشواری استخراج بلوک را ذخیره می‌کند. ما در حال حاضر یک الگوریتم تنظیم هدف را پیاده سازی نمی کنیم، بنابراین می توانیم دشواری را به عنوان یک ثابت جهانی تعریف کنیم.

مقدار 24 یک عدد دلخواه است، هدف ما این است هدف یا target ای داشته باشیم که کمتر از 256 بیت در حافظه مصرف کند. و ما می خواهیم تفاوت به اندازه کافی قابل توجه باشد، اما نه خیلی بزرگ، زیرا هر چه تفاوت بزرگتر باشد، یافتن هش مناسب دشوارتر است.

type ProofOfWork struct {
	block  *Block
	target *big.Int
}

func NewProofOfWork(b *Block) *ProofOfWork {
	target := big.NewInt(1)
	target.Lsh(target, uint(256-targetBits))

	pow := &ProofOfWork{b, target}

	return pow
}

در اینجا ساختار ProofOfWork ایجاد می کنیم که یک اشاره گر به یک block و یک اشاره گر به یک target را نگه می دارد. “target” نام دیگری برای نیاز توضیح داده شده در پاراگراف قبلی است. ما از یک عدد صحیح بزرگ به دلیل نحوه مقایسه هش با target استفاده می کنیم: یک هش را به یک عدد صحیح بزرگ تبدیل می کنیم و بررسی می کنیم که آیا کمتر از target است یا خیر.

در تابع NewProofOfWork، یک big.Int را با مقدار 1 مقداردهی اولیه می کنیم

و آن را با

256 - targetBits

به سمت چپ شیفت می دهیم. 256 طول هش SHA-256 بر حسب بیت است و الگوریتم هش SHA-256 است که ما از آن استفاده خواهیم کرد. نمایش هگزادسیمال target به صورت زیر است:

0x10000000000000000000000000000000000000000000000000000000000

و 29 بایت در حافظه اشغال می کند. و در اینجا مقایسه بصری آن با هش های نمونه های قبلی است:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000  
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

هش اول (محاسبه شده بر روی “من دونات را دوست دارم”) بزرگتر از هدف است، بنابراین اثبات معتبری برای کار نیست. هش دوم (محاسبه شده روی “من donutsca07ca را دوست دارم”) کوچکتر از هدف است، بنابراین یک اثبات معتبر است.

شما می توانید یک target را به عنوان مرز بالایی یک محدوده در نظر بگیرید: اگر یک عدد (هش) کمتر از مرز باشد، معتبر است و بالعکس. کاهش مرز منجر به اعداد معتبر کمتری خواهد شد و بنابراین، کار دشوارتری برای یافتن یک عدد معتبر مورد نیاز است.

اکنون، ما به داده ها برای هش نیاز داریم. بیایید آن را آماده کنیم:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.Data,
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

این قطعه ساده است. ما فقط فیلدهای block را با target و nonce ادغام می کنیم.

این nonceدر اینجا در نقش شمارنده از توضیحات Hashcash بالا است، این یک اصطلاح رمزنگاری است.

بسیار خوب، تمام آماده سازی ها انجام شده است، اجازه دهید هسته الگوریتم PoW را پیاده سازی کنیم:

func (pow *ProofOfWork) Run() (int, []byte) {
	var hashInt big.Int
	var hash [32]byte
	nonce := 0

	fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
	for nonce < maxNonce {
		data := pow.prepareData(nonce)
		hash = sha256.Sum256(data)
		fmt.Printf("\r%x", hash)
		hashInt.SetBytes(hash[:])

		if hashInt.Cmp(pow.target) == -1 {
			break
		} else {
			nonce++
		}
	}
	fmt.Print("\n\n")

	return nonce, hash[:]
}

ابتدا متغیرها را مقداردهی اولیه می کنیم. hashInt نمایش عدد صحیح هش است. nonce شمارنده است.

در مرحله بعد، یک حلقه “بی نهایت” را اجرا می کنیم: این حلقه توسط maxNonce محدود شده است که برابر با math.MaxInt64 است. این کار برای جلوگیری از سرریز احتمالی nonce انجام می شود. اگرچه دشواری اجرای PoW ما برای سرریز شدن شمارنده بسیار کم است، اما بهتر است این بررسی را انجام دهید، فقط در صورت امکان.

در حلقه ما:

۱- داده ها را آماده کنید.

۲- آن را با SHA-256 هش کنید.

۳- هش را به یک عدد صحیح بزرگ تبدیل کنید.

۴- عدد صحیح را با هدف مقایسه کنید.

به همین راحتی که قبلا توضیح داده شد. اکنون می‌توانیم متد SetHash را از Block حذف کرده و تابع NewBlock را تغییر دهیم:

func NewBlock(data string, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
	pow := NewProofOfWork(block)
	nonce, hash := pow.Run()

	block.Hash = hash[:]
	block.Nonce = nonce

	return block
}

در اینجا می توانید ببینید که nonce به عنوان یک ویژگی Block ذخیره می شود. این امر ضروری است، زیرا برای تأیید یک مدرک نیاز به nonce است. ساختار Block اکنون به نظر می شود:

type Block struct {
	Timestamp     int64
	Data          []byte
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}

بسیار خوب! بیایید برنامه را اجرا کنیم تا ببینیم آیا همه چیز خوب کار می کند یا خیر:

Mining the block containing "Genesis Block"
00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Mining the block containing "Send 1 BTC to Ivan"
00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Mining the block containing "Send 2 more BTC to Ivan"
000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

Prev. hash:
Data: Genesis Block
Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1
Data: Send 1 BTC to Ivan
Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Data: Send 2 more BTC to Ivan
Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

آری می بینید که هر هش اکنون با سه بایت صفر شروع می شود و مدتی طول می کشد تا این هش ها را دریافت کنید.

یک کار دیگر برای انجام باقی مانده است: اجازه دهید تأیید اعتبار (Proof of Work) آثار را ممکن کنیم.

func (pow *ProofOfWork) Validate() bool {
	var hashInt big.Int

	data := pow.prepareData(pow.block.Nonce)
	hash := sha256.Sum256(data)
	hashInt.SetBytes(hash[:])

	isValid := hashInt.Cmp(pow.target) == -1

	return isValid
}

و اینجاست که ما به nonce ذخیره شده نیاز داریم.

بیایید یک بار دیگر بررسی کنیم که همه چیز خوب است:

func main() {
	...

	for _, block := range bc.blocks {
		...
		pow := NewProofOfWork(block)
		fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
		fmt.Println()
	}
}

خروجی:

...

Prev. hash:
Data: Genesis Block
Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true

Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send 1 BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true

Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send 2 more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true

نتیجه گیری

بلاکچین ما یک قدم به معماری واقعی خود نزدیک‌تر است. افزودن بلاک‌ها اکنون به کار سخت نیاز دارد، بنابراین استخراج امکان‌پذیر است. اما هنوز برخی از ویژگی های حیاتی را ندارد. پایگاه داده بلاکچین پایدار نیست، کیف پول، آدرس، تراکنش وجود ندارد و مکانیسم توافقی (consensus) وجود ندارد. همه این موارد را در مقالات آینده پیاده سازی خواهیم کرد، و در حال حاضر، ماینینگ تان مبارک!

پایان قسمت دوم.

قسمت سوم

لینک منبع

باقی قسمت ها نسخه اصلی

You May Also Like