Алгоритм консенсуса Ethash для анализа исходного кода Ethereum
Ветка кода:GitHub.com/ether EU M/go…
введение
В настоящее время в Ethereum существует две реализации алгоритмов консенсуса:cliqueиethash. иethashэто текущая основная сеть Ethereum (Homesteadверсия) изPOWАлгоритм консенсуса.
Структура каталогов
ethashМодули находятся в каталоге проекта Ethereum.consensus/ethashПод содержанием.
-
algorithm.goДостигнуто
Dagger-HashimotoВсе функции алгоритма, такие как генерацияcacheиdataset,в соответствии сHeaderиNonceВычислить хэши майнинга и т.д. -
api.goпонял
RPCв использованииapiметод. -
consensus.goРеализованы некоторые методы интерфейса консенсуса Ethereum, в том числе
Verifyметод серии (VerifyHeader,VerifySealЖдать),PrepareиFinalize,CalcDifficulty,Author,SealHash. -
ethash.goДостигнуто
cacheструктура иdatasetструктуры и соответствующие им методы,MakeCache/MakeDatasetфункция,EthashобъектNewфункция, иEthashвнутренний метод. -
sealer.goреализует интерфейс консенсуса
SealМетоды иEthashвнутренний методmine. Эти методы реализуютethashфункция майнинга.
Принципы дизайна Ethash
Цели дизайна Ethash
Когда Эфириум разработал алгоритм консенсуса, он рассчитывал достичь трех целей:
- анти-
ASICСексуальность: Преимущество создания специального оборудования для алгоритма должно быть как можно меньше, чтобы обычные пользователи компьютеров могли майнить с помощью процессоров.- Сопротивление по лимиту памяти (
ASICДорого использовать память майнера) - Когда считывается большой объем случайных данных из памяти, скорость вычислений ограничивается не только вычислительным блоком, но и скоростью считывания памяти.
- Сопротивление по лимиту памяти (
- Проверка легкого клиента: блок должен быть проверен легким клиентом быстро и эффективно.
- Майнеры должны быть обязаны хранить полное состояние блокчейна.
набор хеш-данных
ethashЧтобы вычислить хэш, вам нужно сначала иметь набор данных. Этот набор данных большой, с начальным размером примерно1G, будет обновляться каждые 30 000 блоков, и каждое обновление будет больше, чем раньше.8Mо. Источник данных для вычисления хэша исходит из этого набора данных; это данные, которые решают, какие данные в наборе данных используются для вычисления хэша.headerданные иNonceполе. Эта часть сделанаDaggerреализуется по алгоритму.
Dagger
DaggerАлгоритмы используются для создания наборов данныхDatasetДа, основная частьDatasetГенерация и организационная структура.
можно поставитьDatasetдумать о несколькихitem(dataItem) массива, каждыйitemда64Байтовый массив байтов (хэш).datasetНачальный размер примерно1G, каждые 30 000 блоков (одинepochинтервал) будет обновляться один раз, и каждое обновление будет больше, чем раньше8Mо.
Datasetкаждогоitemсостоит из блока кэша (cache), блок кеша также можно рассматривать как множественныйitem(cacheItem), объем памяти, занимаемый блоком кеша, меньшеdatasetнамного меньше, его первоначальный размер составляет около16M. такой жеdatasetТочно так же он будет обновляться каждые 30 000 блоков, и каждое обновление будет больше, чем раньше.128Kо.
генерироватьdataItemПроцесс таков: выбрать «случайное» из блока кеша («случайное» здесь не является реальным случайным числом, а означает, что его нельзя определить заранее, но каждый раз получается одно и то же значение)cacheItemПроведите расчет, и полученный результат участвует в следующем расчете, этот процесс будет повторяться 256 раз.
Кэш-блоки создаютсяseedгенерируется, в то время какseedЗначение связано с высотой блока. так сгенерироватьdatasetПроцесс показан на следующем рисунке:
DaggerЕще один ключевой момент – уверенность. то есть то же самоеepoch, каждый раз вычисляемыйseedкеш,datasetвсе одинаковые. В противном случае для одного и того же блока майнер и валидатор используют разныеdataset, это невозможно проверить.
Алгоритм Хашимото
даThaddeus DryjaТворческий. направленный на прохождениеIOОграничения бойкота майнеров. В процессе майнинга устанавливается предел чтения памяти.Поскольку само запоминающее устройство будет дешевле и более распространено, чем вычислительное устройство, крупные компании по всему миру также вложили значительные средства в оптимизацию обновления памяти, чтобы сделать память адаптируемой для различных пользователей. сцена, поэтому есть понятие оперативной памятиRAM, следовательно, существующая память может быть относительно близка к оптимальному алгоритму оценки.HashimotoАлгоритм использует блокчейн в качестве исходных данных и удовлетворяет требованиям 1 и 3 выше.
Его функция состоит в том, чтобы использовать поля хэша и одноразового номера заголовка блока, а также использовать данные набора данных для создания окончательного значения хеш-функции.
Анализ исходного кода
Создать набор хеш-данных
generateфункционировать вethash.goфайл, в основном для созданияdataset, который включает в себя следующее содержимое.
Создать размер кеша
cache sizeглавныйРазмер кеша проверки ethash для определенного номера блока*,epochLength30000, еслиepochменьше 2048, из известныхepochвернуть соответствующийcache size, иначе пересчитатьepoch
cacheРазмер , растет линейно,sizeЗначение равно (2 ^ 24 ^ + 2 ^ 17 ^ * эпоха - 64), разделите это значение на 64, чтобы увидеть, является ли результат простым числом, если нет, вычтите 128 и пересчитайте, пока не найдете наибольшее простое число. номер.
csize := cacheSize(d.epoch*epochLength + 1)
func cacheSize(block uint64) uint64 {
epoch := int(block / epochLength)
if epoch < maxEpoch {
return cacheSizes[epoch]
}
return calcCacheSize(epoch)
}
func calcCacheSize(epoch int) uint64 {
size := cacheInitBytes + cacheGrowthBytes*uint64(epoch) - hashBytes
for !new(big.Int).SetUint64(size / hashBytes).ProbablyPrime(1) { // Always accurate for n < 2^64
size -= 2 * hashBytes
}
return size
}
Создать размер набора данных
dataset SizeглавныйРазмер кеша проверки ethash для определенного номера блока, аналогично созданному вышеcache size
dsize := datasetSize(d.epoch*epochLength + 1)
func datasetSize(block uint64) uint64 {
epoch := int(block / epochLength)
if epoch < maxEpoch {
return datasetSizes[epoch]
}
return calcDatasetSize(epoch)
}
генерировать семя
*seedHash — это начальное число, используемое для создания кэша проверки и набора данных для майнинга. *Длина 32.
seed := seedHash(d.epoch*epochLength + 1)
func seedHash(block uint64) []byte {
seed := make([]byte, 32)
if block < epochLength {
return seed
}
keccak256 := makeHasher(sha3.NewLegacyKeccak256())
for i := 0; i < int(block/epochLength); i++ {
keccak256(seed, seed)
}
return seed
}
генерировать кеш
generateCache(cache, d.epoch, seed)
Следующий анализgenerateCacheКод ключа:
Узнайте первымhashBytes, в следующих расчетах используется эта единица, и ее значение равно 64, что эквивалентноkeccak512Длина хэша, как показано нижеitemвызов[hashBytes]byte.
①: Инициализацияcache
Этот цикл используется для инициализацииcache: первыйseedХэш заполнитьcacheпервоеitem, затем используйте предыдущийitemхэш заполнения послеitem.
for offset := uint64(hashBytes); offset < size; offset += hashBytes {
keccak512(cache[offset:], cache[offset-hashBytes:offset])
atomic.AddUint32(&progress, 1)
}
②: XOR данных в кеше согласно правилам
для каждогоitem(srcOff), выберите один "наугад"item(xorOff) XOR с ним, записываем хэш результата вdstOffсередина. Эта операционная логика будетcacheRoundsВторосортный.
Две вещи, которые следует отметить:
- Один
srcOffменяется с хвоста на голову иdstOffОн меняется от головы к хвосту. и они соответствующие, т. е. когдаsrcOffПри представлении предпоследнего x-го элементаdstOffОн представляет положительный x-й элемент. - два это
xorOffвыбор. Обратите внимание, что мы просто взяли «случайный» в кавычки.xorOffЗначение кажется случайным, поскольку даноseedРаньше вы не могли знать, каково значение xorOff, но однаждыseedЗначение определено, тогда каждый разxorOffзначения детерминированы. Значение seed определяется высотой блока. это тоже самоеepochвсегда получай одно и то жеcacheпричина данных.
for i := 0; i < cacheRounds; i++ {
for j := 0; j < rows; j++ {
var (
srcOff = ((j - 1 + rows) % rows) * hashBytes
dstOff = j * hashBytes
xorOff = (binary.LittleEndian.Uint32(cache[dstOff:]) % uint32(rows)) * hashBytes
)
bitutil.XORBytes(temp, cache[srcOff:srcOff+hashBytes], cache[xorOff:xorOff+hashBytes])
keccak512(cache[dstOff:], temp)
atomic.AddUint32(&progress, 1)
}
}
создать набор данных
datasetрасчет размеров иcacheТочно так же величина отличается: 2 ^ 30 ^ + 2 ^ 23 ^ * эпоха - 128, затем каждый раз вычитать 256, чтобы найти наибольшее простое число.
Генерация данных представляет собой цикл, каждый раз генерирующий 64 байта, основная функцияgenerateDatasetItem:
generateDatasetItemИсточник данныхcachedata, а окончательное значение набора данных будет храниться в переменной mix. Весь процесс также состоит из нескольких циклов.
①: ИнициализацияmixПеременная
По паре значений кешаmixпеременные инициализируются. вhashWordsпредставляетhashсколько их тамwordСтоимость: одинhashдлинаhashBytesто есть 64 байта,word(тип uint32) имеет длину 4 байта, поэтомуhashWordsЗначение равно 16. ВыбратьcacheКакие из данных определяются параметромindexиiопределяется переменными.
mix := make([]byte, hashBytes)
binary.LittleEndian.PutUint32(mix, cache[(index%rows)*hashWords]^index)
for i := 1; i < hashWords; i++ {
binary.LittleEndian.PutUint32(mix[i*4:], cache[(index%rows)*hashWords+uint32(i)])
}
keccak512(mix, mix)
②: будетmixпреобразовать в[]uint32тип
intMix := make([]uint32, hashWords)
for i := 0; i < len(intMix); i++ {
intMix[i] = binary.LittleEndian.Uint32(mix[i*4:])
}
③: будетcacheданные объединяются вintmix
for i := uint32(0); i < datasetParents; i++ {
parent := fnv(index^i, intMix[i%16]) % rows
fnvHash(intMix, cache[parent*hashWords:])
}
FNVАлгоритм хеширования — это алгоритм хеширования, который не требует использования ключа.
Алгоритм прост: умножьте a на простое число FNV 0x01000193, затем XOR с b.
Сначала используйте этот алгоритм для расчета значения индекса, используйте этот индекс изcacheвыбрать значение (data), затем кmixВычисляется один раз для каждого байта вFNV, чтобы получить окончательное значение хеш-функции.
func fnv(a, b uint32) uint32 {
return a*0x01000193 ^ b
}
func fnvHash(mix []uint32, data []uint32) {
for i := 0; i < len(mix); i++ {
mix[i] = mix[i]*0x01000193 ^ data[i]
}
}
④: будетintMixвосстановлен вmixи рассчитатьmixхэш вернулся
for i, val := range intMix {
binary.LittleEndian.PutUint32(mix[i*4:], val)
}
keccak512(mix, mix)
return mix
generateCacheиgenerateDatasetреализуетсяDaggerОсновная функция алгоритма и весь процесс создания набора хэш-данных завершены.
Основные функции механизма консенсуса
Код находится наconsensus.go
①:Author
// 返回coinbase, coinbase是打包第一笔交易的矿工的地址
func (ethash *Ethash) Author(header *types.Header) (common.Address, error) {
return header.Coinbase, nil
}
②:VerifyHeader
Существует два основных этапа проверки: первая проверкаЗаголовок известенилиэто неизвестный предок, второй шагethashчек:
2.1 заголовок. Дополнительный не может превышать 32 байта
if uint64(len(header.Extra)) > params.MaximumExtraDataSize { // 不超过32字节
return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize)
}
2.2 Отметка времени не может превышать 15 секунд, а блоки после 15 секунд считаются будущими блоками.
if !uncle {
if header.Time > uint64(time.Now().Add(allowedFutureBlockTime).Unix()) {
return consensus.ErrFutureBlock
}
}
2.3 Временная метка текущего заголовка меньше, чем у родительского блока
if header.Time <= parent.Time { // 当前header的时间小于等于父块的
return errZeroBlockTime
}
2.4 Проверка сложности блока на основе метки времени и сложности родительского блока
expected := ethash.CalcDifficulty(chain, header.Time, parent)
if expected.Cmp(header.Difficulty) != 0 {
return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected)
}
2.5 Проверкаgas limitменее 2^63^-1
cap := uint64(0x7fffffffffffffff)
if header.GasLimit > cap {
return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap)
}
2.6 ПодтверждениеgasUsedдля gasLimit
if header.GasUsed > header.GasLimit {
return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
}
2.7 Убедитесь, что номер блока равен родительскому блоку плюс 1
if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
return consensus.ErrInvalidNumber
}
2.8 Проверить, соответствует ли данный блок требованиям сложности pow
if seal {
if err := ethash.VerifySeal(chain, header); err != nil {
return err
}
}
③:VerifyUncles
3.1 До двух дядюшек
if len(block.Uncles()) > maxUncles {
return errTooManyUncles
}
3.2 Собирайте блоки дяди и блоки предков
number, parent := block.NumberU64()-1, block.ParentHash()
for i := 0; i < 7; i++ {
ancestor := chain.GetBlock(parent, number)
if ancestor == nil {
break
}
ancestors[ancestor.Hash()] = ancestor.Header()
for _, uncle := range ancestor.Uncles() {
uncles.Add(uncle.Hash())
}
parent, number = ancestor.ParentHash(), number-1
}
ancestors[block.Hash()] = block.Header()
uncles.Add(block.Hash())
3.3 Убедитесь, что дядя вознаграждается только один раз и что у дяди есть действительный предок
for _, uncle := range block.Uncles() {
// Make sure every uncle is rewarded only once
hash := uncle.Hash()
if uncles.Contains(hash) {
return errDuplicateUncle
}
uncles.Add(hash)
// Make sure the uncle has a valid ancestry
if ancestors[hash] != nil {
return errUncleIsAncestor
}
if ancestors[uncle.ParentHash] == nil || uncle.ParentHash == block.ParentHash() {
return errDanglingUncle
}
if err := ethash.verifyHeader(chain, uncle, ancestors[uncle.ParentHash], true, true); err != nil {
return err
}
④:Prepare
инициализация
headerизDifficultyполе
parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}
header.Difficulty = ethash.CalcDifficulty(chain, header.Time, parent)
return nil
⑤:FinalizeВсе модификации состояния после транзакции (например, награды за блок) выполняются, ноне собираетсяблок.
5.1 Накапливайте награды за любой блок и дяди-блоки
accumulateRewards(chain.Config(), state, header, uncles)
5.2 Рассчитайте корневой хэш дерева состояний и отправьте его вheader
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
⑥:FinalizeAndAssembleЗапустите любые модификации состояния после транзакции (например, награды за блок) и соберите окончательный блок.
func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) {
accumulateRewards(chain.Config(), state, header, uncles)
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
return types.NewBlock(header, txs, uncles, receipts), nil
}
Очевидно, чемFinalizeпереборtypes.NewBlock
⑦:SealHashобратно вsealхэш предыдущего блока (за которым последуетsealХэш блока после этого другой)
func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) {
hasher := sha3.NewLegacyKeccak256()
rlp.Encode(hasher, []interface{}{
header.ParentHash,
header.UncleHash,
header.Coinbase,
header.Root,
header.TxHash,
header.ReceiptHash,
header.Bloom,
header.Difficulty,
header.Number,
header.GasLimit,
header.GasUsed,
header.Time,
header.Extra,
})
hasher.Sum(hash[:0])
return hash
}
⑧:SealГенерирует новый запечатанный запрос для данного входного блока (добыча полезных ископаемых) и поместите результат в указанный канал.
Обратите внимание, что метод вернется немедленно и отправит результат асинхронно. В зависимости от алгоритма консенсуса также может быть возвращено несколько результатов. Эта часть будет подробно проанализирована в следующем майнинге, пропустите ее здесь.
Детали майнинга
Если у вас возникнут вопросы при чтении этой статьи, вы можете оставить мне сообщение, и я отвечу вовремя. Если вы считаете, что это хорошо написано, вы можете обратить внимание на нижнюю частьСсылаться наиз
github项目, можно впервые обратить внимание на динамику статьи автора.
Определение основного интерфейса майнинга:
Seal(chain ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error
Входитьsealфункция:
①: Если операция невернаPOW, сразу вернуть пустымnonceиMixDigest, и блок также является пустым блоком.
if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake {
header := block.Header()
header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}
select {
case results <- block.WithSeal(header):
default:
ethash.config.Log.Warn("Sealing result is not read by miner", "mode", "fake", "sealhash", ethash.SealHash(block.Header()))
}
return nil
}
②: ПоделитьсяpowЕсли это так, перейдите к его общему объекту для выполненияSealдействовать
if ethash.shared != nil {
return ethash.shared.Seal(chain, block, results, stop)
}
③: Получите исходный код и сгенерируйте его на его основе.ethashнужны семена
f ethash.rand == nil {
// 获得种子
seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
ethash.lock.Unlock()
return err
}
ethash.rand = rand.New(rand.NewSource(seed.Int64())) // 给rand赋值
}
④: Основная работа по добыче полезных ископаемых переданаmine
for i := 0; i < threads; i++ {
pend.Add(1)
go func(id int, nonce uint64) {
defer pend.Done()
ethash.mine(block, id, nonce, abort, locals) // 真正执行挖矿的动作
}(i, uint64(ethash.rand.Int63()))
}
⑤: Обработать результат майнинга
- Внешнее неожиданное прерывание, остановить все потоки майнинга
- Один из потоков копает правильный блок, прерывая все остальные потоки
- Объект ethash изменяется, останавливает все текущие операции и перезапускает текущий метод.
go func() {
var result *types.Block
select {
case <-stop:
close(abort)
case result = <-locals:
select {
case results <- result: //其中一个线程挖到正确块,中止其他所有线程
default:
ethash.config.Log.Warn("Sealing result is not read by miner", "mode", "local", "sealhash", ethash.SealHash(block.Header()))
}
close(abort)
case <-ethash.update:
close(abort)
if err := ethash.Seal(chain, block, results, stop); err != nil {
ethash.config.Log.Error("Failed to restart sealing after update", "err", err)
}
}
Из вышеизложенного можно узнатьsealОсновная работаmineФункция завершена, давайте сосредоточимся на ней.
mineФункция на самом деле относительно проста, онадействительноpowшахтер, используемый для поискаnonceценность,nonceЗначение начинается сseedценность,seedЗначение - это сложность, которая в конечном итоге приведет к созданию правильного сопоставляемого и проверяемого блока.
①: Извлеките соответствующие данные из заголовка блока и поместите их в поле глобальной переменной.
var (
header = block.Header()
hash = ethash.SealHash(header).Bytes()
target = new(big.Int).Div(two256, header.Difficulty) // 这是用来验证的target
number = header.Number.Uint64()
dataset = ethash.dataset(number, false)
)
②: начать генерировать случайныеnonce, пока мы не прервем или не найдем хорошийnonce
var (
attempts = int64(0)
nonce = seed
)
③: Совокупность завершенаdatasetданные для генерации окончательного хэша для определенного заголовка и одноразового номера
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
//定义一个lookup函数,用于在数据集中查找数据
lookup := func(index uint32) []uint32 {
offset := index * hashWords //hashWords是上面定义的常量值= 16
return dataset[offset : offset+hashWords]
}
return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}
можно найти на самом делеhashimotoFullРабота функции состоит в том, чтобы прочитать и разделить исходный набор данных, а затем передать его вhashimotoфункция. Следующий анализ фокусаhashimotoфункция:
3.1 Получить заголовок блока в соответствии с начальным числом
rows := uint32(size / mixBytes) ①
seed := make([]byte, 40) ②
copy(seed, hash) ③
binary.LittleEndian.PutUint64(seed[32:], nonce)④
seed = crypto.Keccak512(seed)⑤
seedHead := binary.LittleEndian.Uint32(seed)⑥
- Подсчитайте количество строк в наборе данных
- сливаться
header+nonceдо 40 байтseed - заголовок блока
hashскопировать вseedсередина - будет
nonceВведите значениеseedПосле (40-32=8) байт (сам одноразовый номерuint64тип, который составляет 64 бита, что соответствует размеру 8 байт), просто поместитеhashиnonceПолностью заполнен 40 байтами seed -
Keccak512шифрованиеseed - от
seedПолучить заголовок блока из
3.2 Начать смешивание с повторных семян
-
mixBytesпостоянная = 128,mixимеет длину 32, а элементыuint32, составляет 32 бита, что соответствует размеру 4 байта. такmixОбщий размер 4*32=128 байт.
mix := make([]uint32, mixBytes/4)
for i := 0; i < len(mix); i++ {
mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])
}
3.3 Узел набора гибридных случайных данных
temp := make([]uint32, len(mix))//与mix结构相同,长度相同
for i := 0; i < loopAccesses; i++ {
parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows
for j := uint32(0); j < mixBytes/hashBytes; j++ {
copy(temp[j*hashWords:], lookup(2*parent+j))
}
fnvHash(mix, temp)
}
3.4 Компрессионное смешивание
for i := 0; i < len(mix); i += 4 {
mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3])
}
mix = mix[:len(mix)/4]
digest := make([]byte, common.HashLength)
for i, val := range mix {
binary.LittleEndian.PutUint32(digest[i*4:], val)
}
return digest, crypto.Keccak256(append(seed, digest...))
То, что наконец возвращено,digestиdigestиseedхэш от ; иdigestНа самом деле этоmixиз[]byteформа. спередиEthash.mineКод, который мы видели, использует второе возвращаемое значение сtargetпеременные сравниваются, чтобы определить, является ли это допустимым значением хеш-функции.
проверить силу
Проверка информации о майнинге состоит из двух частей:
- проверять
Header.Difficultyэто правильно или нет - проверять
Header.MixDigestиHeader.Nonceэто правильно или нет
①: ПроверкаHeader.DifficultyКод в основном вEthash.verifyHeaderсередина:
func (ethash *Ethash) verifyHeader(chain consensus.ChainReader, header, parent *types.Header, uncle bool, seal bool) error {
......
expected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)
if expected.Cmp(header.Difficulty) != 0 {
return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected)
}
}
Рассчитывается с учетом высоты блока и разницы во времени в качестве параметровDifficultyзначение, а затем с блоком для проверкиHeader.DifficultyПоля сравниваются, и если они равны, считается правильным.
②:MixDigestиNonceПроверка проводится в основномHeader.verifySealсередина:
Метод аутентификации: использоватьHeader.Nonceи хэш заголовка черезhashimotoпересчитыватьMixDigestиresultХэш-значение и проверяющие узлы не нуждаются в данных набора данных.