手机就是POS机.png

首页 区块链正文

分析以太坊的共识算法之一:实现了POWFunction内存计算困难

佚名 区块链 2021-04-26 22:00:24 20 0

本文专门分析了以太坊的共识算法之一:实现POW的以太坊共识引擎ethash。

关键字:ethash,共识算法,pow,Dagger Hashimoto,ASIC,struct {},nonce,FNV哈希,位操作,纪元

Ethash

在分析以太坊挖矿的源代码之前,挖了一个共识引擎坑,并研究了DAG有向无环图的算法。这些是本文要研究的Ethash的基础。 Ethash当前是基于POW工作量证明的以太坊的共识引擎(也称为挖掘算法)。它的前身是Dagger Hashimoto算法。

桥本匕首

作为以太坊挖矿算法Ethash的前身,Dagger Hashimoto的目的是:

匕首和桥本实际上是两件事,

桥本算法

此人是Thaddeus Dryja创建的。旨在通过IO限制抵制采矿机器。在挖掘过程中,内存访问受到限制。由于存储设备本身将比计算设备更便宜,更通用,因此世界各地的大公司也已经在存储升级和优化方面投入了大量资金,以使存储适合各种用户。场景,因此存在随机存取存储器RAM的概念,因此,现有存储器可能更接近最佳评估算​​法。桥本算法使用区块链作为源数据,并满足上述1和3的要求。

匕首算法

它是这个人Vitalik Buterin发明的。它使用有向无环图DAG,实现了难于记忆但难以验证的难于记忆的存储器计算(我们知道这是哈希算法一)的重要特征之一。其理论基础是基于在每个特定场合下,随机数仅需要大数据总量树的一小部分,并且禁止针对每个特定场合重新计算该随机数的子树,因此,既需要存储该树又要支持一个独立的场合随机数验证值:Dagger算法注定要替换现有的仅在内存中难以计算的算法,例如Scrypt(由Litecoin使用),这是一种难以计算且也难以验证的算法。计算难度增加到了真正的安全水平,验证的难度也增加了,但是Dagger算法被证明是脆弱的。 o由Sergio Lerner发明的共享内存硬件加速技术,然后将该算法抛弃在其他路径的研究中。

内存硬功能

直接翻译的是记忆难度函数,这是解决采矿机问题所产生的想法。我们知道采矿是基于我们的计算机的,但是一些硬件制造商将制造专门用于采矿的硬件设备。它们不是完整的PC,例如ASIC,GPU和FPGA(我们经常听到GPU挖掘Wait)。因此,作为采矿机的这些设备超过了普通PC采矿的存在,这与我们的区块链的分散精神不符,因此我们必须使采矿设备相等。

那么如何使采矿设备平等?

我在谈论上面的Dagger算法时提到了它。这是介绍它的另一种方法。现在,CPU都是多核的。在计算能力方面,CPU具有几个内核,可以同时模拟多个设备。矿山自然会更高效,但是这里使用的测量对象是内存,一台计算机只有一个内存。我们完成Java多线程开发的朋友知道,无论计算机的性能有多高,但是我们编写的程序都是单线程的,所以该程序可以在高配置的多核计算机和低配置的计算机上运行。单核计算机的配置没有太大区别比特币挖矿程序源代码,只要它们的单核计算能力与内存大小相同即可。因此,这也是原则。通过Dagger算法,我们将挖掘过程锁定在以内存衡量的硬件性能上。只要“将一堆数据插入内存”的方法,多核并行处理就无法进行,并且硬件也减少了。的计算优势仅与存储器的大小有关,因此无论是PC还是ASIC,GPU和FPGA,它都可以满足均等挖掘的要求。这也是目前抵抗采矿机的主要方法-抗ASIC的原理。

研究两个问题

在Dagger和Dagger Hashimoto算法中,对两个问题的研究被搁置了,

桥本匕首算法

(与Hashimoto不同)Dagger Hashimoto不直接使用区块链作为数据源,而是使用1GB定制生成的数据集缓存。

此数据集基于块数据每N个块更新一次。该数据集是使用Dagger算法生成的,因此可以针对每个轻客户端验证算法随机数进行自己的有效计算。

(与Dagger不同)Dashi桥本克服了Dagger的缺点。用于查询块数据的数据集是半永久性的,只会偶尔更新(例如,每周一次)。这意味着生成数据集将非常容易,因此Sergio Lerner备受争议的共享内存加速变得微不足道。

采矿补充

我之前写过一篇有关采矿的文章,本节是对采矿的补充。

以太坊将过渡到POS(权益证明),而不是传统的POW,将取消采矿,因此不建议现在成为矿工(早期购买设备的成本很高,POS它可能无法在实现之前就还清。)

Mining Ether =网络安全=验证估算

以太坊当前的POW算法是Ethash,

Ethash算法涉及查找随机数值并将其输入到算法中。结果低于基于特定难度的阈值。

POW算法的关键点在于,除了蛮力枚举之外,没有找到该随机数的方法比特币挖矿程序源代码,但是验证输出结果非常简单容易。如果输出结果具有均匀分布,我们可以确保找到随机数值所需的平均时间取决于难度阈值。因此,我们可以通过调整难度阈值来控制寻找新块的时间。这是为了控制块的生成。速度原理。

DAG

Ethash的POW记忆力很强,它支持采矿机进行抵抗。这意味着POW计算需要选择依赖于随机数值和块头的固定资源子集。

此资源(大小约为1G)是DAG!

我时代

每30,000个块将花费几个小时来生成有向无环图DAG。此DAG称为纪元I(为了记忆起见,请参阅Qin II)。 DAG仅取决于块的高度,它可以预生成,如果没有预生成,则客户需要等待预生成过程结束才能继续进行块生产操作。除非客户端实际上预先预先缓存了DAG,否则网络可能会在每个时期的过渡期间遇到巨大的块延迟。

特殊情况:从头开始创建节点时,挖掘将仅在创建实际DAG之后开始。

采矿奖励

共有三个部分:

Ethash

Ethash算法路线图:

上述大数据集每30,000块更新一次,因此大多数矿工的工作是读取数据集而不是对其进行更改。

Pkg ethash源代码分析

上面,我们已经抽象出了所有概念,包括POW,挖掘,Ethash原理过程等,让我们将这些理论知识纳入源代码以分析特定的实现。作为主题,本文主要分析ethash算法,因此整个源代码范围仅限于go-ethereum / consensus / ethash程序包,该程序包实现了ethash pow共识引擎。

入口

必须有一个入口来分析源代码。该入口是在“以太坊源代码机制:挖掘”中挖出的“密封方法”坑。原始文本留下了此标记,我们将在本文中对其进行讨论。

在go-ethereum / consensus / consensus.go界面中定义了以下方法,该方法对应于上面的“密封方法”。接口方法定义如下:

Seal(chain ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error)//该方法通过输入一个包含本地矿工挖出的最高区块在主干上生成一个新块。

这些参数包括ChainReader,Block和Stop结构信号,它们会在主链上返回一个新的block实体。

// 定义了一些方法,用于在区块头验证以及叔块验证期间,访问本地区块链。
type ChainReader interface {
	// 获取区块链的链配置
	Config() *params.ChainConfig
	// 从本地链获取当前块头
	CurrentHeader() *types.Header
	// 通过hash和number从主链中获取一个区块头
	GetHeader(hash common.Hash, number uint64) *types.Header
	// 通过number从主链中获取一个区块头
	GetHeaderByNumber(number uint64) *types.Header
	// 通过hash从主链中获取一个区块头
	GetHeaderByHash(hash common.Hash) *types.Header
	// 通过hash和number从主链中获取一个区块
	GetBlock(hash common.Hash, number uint64) *types.Block
}

比特币 挖矿 程序_比特币挖矿程序源代码_比特币挖矿代码实现

总结,ChainReader定义了几种方法:从本地区块链获取配置和块头,从主链获取块头和块,参数条件包括哈希和数字,可以随意组合。

// Block代表以太坊区块链中的一个完整的区块
type Block struct {
	header       *Header // 区块包括头
	uncles       []*Header // 叔块
	transactions Transactions // 交易集合
	// caches缓存
	hash atomic.Value
	size atomic.Value
	// Td用于core包存储所有的链上的难度
	td *big.Int
	// 这些字段用于eth包来跟踪inter-peer内部端点区块的接替
	ReceivedAt   time.Time
	ReceivedFrom interface{}
}

总而言之,除了块头,叔叔块以及众所周知的块中已打包和存储的交易信息外,Block还具有高速缓存的内容以及链上每个块的难度值,例如以及跟踪内部端点的字段。

stop是一个空的结构作为信号源。

关于空结构的讨论,为什么struct {}经常出现在行进中?

除struct {}类型外,go中的所有其他类型均为width,它占用存储空间,而struct {}没有字段,没有方法,width为0,灵活性高并且不占用内存空间,这可能被Gopher的理由所青睐。

密封器

密封方法有两种实现。我们选择ethash。此方法存在于共识文件/ethash/sealer.go文件中。第一个功能是密封的实现。首先让我们看一下方法的声明部分:

// 尝试找到一个nonce值能够满足区块难度需求。
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {

可以看出该方法属于Ethash的指针对象,

type Ethash struct {
    // cache配置
	cachedir     string // 缓存位置
	cachesinmem  int    // 在内存中缓存的数量
	cachesondisk int    // 在硬盘中缓存的数量
	
	// DAG挖矿数据集配置
	dagdir       string // DAG位置,存储全部挖矿数据集
	dagsinmem    int    // 在内存中DAG的数量
	dagsondisk   int    // 在硬盘中DAG的数量
    // 内存cache
	caches   map[uint64]*cache   // 内存缓存,可反复使用避免再生太频繁
	fcache   *cache              // 为了下一世估算的预生产缓存
	
	// 内存数据集
	datasets map[uint64]*dataset // 内存数据集,可反复使用避免再生太频繁
	fdataset *dataset            // 为了下一世估算的预生产数据集
	// 挖矿相关字段
	rand     *rand.Rand    // 随机工具,用来为nonce做适当的种子
	threads  int           // 如果在挖矿,代表挖矿的线程编号
	update   chan struct{} // 更新挖矿中参数的通道
	hashrate metrics.Meter // 测量跟踪平均哈希率
	// 以下字段是用于测试
	tester    bool          // 是否使用一个小型测试数据集的标志位
	shared    *Ethash       // 共享pow模式,无法再生缓存
	fakeMode  bool          // Fake模式,是否取消POW检查的标志位
	fakeFull  bool          // 是否取消所有共识规则的标志位
	fakeFail  uint64        // 未通过POW检查的区块号(包含fake模式)
	fakeDelay time.Duration // 验证工作返回消息前的休眠延迟时间
	lock sync.Mutex // 为了内存中的缓存和挖矿字段,保证线程安全
}

为了更好地理解以下代码,我们需要分析块头的数据结构:

type Header struct {
	ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
	UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
	Coinbase    common.Address `json:"miner"            gencodec:"required"`
	Root        common.Hash    `json:"stateRoot"        gencodec:"required"`
	TxHash      common.Hash    `json:"transactionsRoot" gencodec:"required"`
	ReceiptHash common.Hash    `json:"receiptsRoot"     gencodec:"required"`
	Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`
	Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`
	Number      *big.Int       `json:"number"           gencodec:"required"`
	GasLimit    *big.Int       `json:"gasLimit"         gencodec:"required"`
	GasUsed     *big.Int       `json:"gasUsed"          gencodec:"required"`
	Time        *big.Int       `json:"timestamp"        gencodec:"required"`
	Extra       []byte         `json:"extraData"        gencodec:"required"`
	MixDigest   common.Hash    `json:"mixHash"          gencodec:"required"`
	Nonce       BlockNonce     `json:"nonce"            gencodec:"required"`
}

您会看到一个区块头包含父区块哈希值,叔叔区块哈希值,Coinbase节点账户地址,状态根,交易哈希,收件人哈希,日志,难度值,区块编号,最低付款额,成本气体,时间戳,额外数据,混合哈希,随机数(8个字节)。我们需要清楚这些块头的成员属性,以便可以更好地理解以下源代码内容。让我们继续使用Seal方法,并在下面显示完整的代码:

func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {
	// fake模式立即返回0 nonce
	if ethash.fakeMode {
		header := block.Header()
		header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}
		return block.WithSeal(header), nil
	}
	// 共享pow的话,则转到它的共享对象执行Seal操作
	if ethash.shared != nil {
		return ethash.shared.Seal(chain, block, stop)
	}
	// 创建一个runner以及它指挥的多重搜索线程
	abort := make(chan struct{})
	found := make(chan *types.Block)
	ethash.lock.Lock() // 线程上锁,保证内存的缓存(包含挖矿字段)安全
	threads := ethash.threads // 挖矿的线程s
	if ethash.rand == nil {// rand为空,则为ethash的字段rand赋值
	    // 获得种子
		seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
		if err != nil {// 执行失败,有报错
			ethash.lock.Unlock() // 先解锁
			return nil, err // 程序中止,直接返回空块和报错信息
		}
		ethash.rand = rand.New(rand.NewSource(seed.Int64())) // 执行成功,拿到合法种子seed,通过其获得rand对象,赋值。
	}
	ethash.lock.Unlock() // 解锁
	if threads == 0 {// 挖矿线程编号为0,则通过方法返回当前物理上可用CPU编号
		threads = runtime.NumCPU()
	}
	if threads < 0 { // 非法结果
		threads = 0 // 置为0,允许在本地或远程没有额外逻辑的情况下,取消本地挖矿操作
	}
	var pend sync.WaitGroup // 创建一个倒计时锁对象,go语法参照 http://www.cnblogs.com/Evsward/p/goPipeline.html#sync.waitgroup
	for i := 0; i < threads; i++ {
		pend.Add(1)
		go func(id int, nonce uint64) {// 核心代码通过闭包多线程技术来执行。
			defer pend.Done()
			ethash.mine(block, id, nonce, abort, found) // Seal核心工作
		}(i, uint64(ethash.rand.Int63()))//闭包第二个参数表达式uint64(ethash.rand.Int63())通过上面准备好的rand函数随机数结果作为nonce实参传入方法体
	}
	// 直到seal操作被中止或者找到了一个nonce值,否则一直等
	var result *types.Block // 定义一个区块对象result,用于接收操作结果并作为返回值返回上一层
	select { // go语法参照 http://www.cnblogs.com/Evsward/p/go.html#select
	case <-stop:
		// 外部意外中止,停止所有挖矿线程
		close(abort)
	case result = <-found:
		// 其中一个线程挖到正确块,中止其他所有线程
		close(abort)
	case <-ethash.update:
		// ethash对象发生改变,停止当前所有操作,重启当前方法
		close(abort)
		pend.Wait()
		return ethash.Seal(chain, block, stop)
	}
	// 等待所有矿工停止或者返回一个区块
	pend.Wait()
	return result, nil
}

上述Seal方法主体已对各种状态的ethash进行了检查和处理,并对线程资源进行了控制。让我们看一下Seal的核心工作(sealer.go文件只有两个功能,一个是Seal方法)。 ,另一种是防雷方法。可以看出,Seal方法是外部方法,而mine方法是内部方法,只能由当前的ethash包域调用):mine方法

// mine函数是真正的pow矿工,用来搜索一个nonce值,nonce值开始于seed值,seed值是能最终产生正确的可匹配可验证的区块难度
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
	// 从区块头中提取出一些数据,放在一个全局变量域中
	var (
		header = block.Header()
		hash   = header.HashNoNonce().Bytes()
		target = new(big.Int).Div(maxUint256, header.Difficulty) // 后面有大用,这是用来验证的target
		number  = header.Number.Uint64()
		dataset = ethash.dataset(number)
	)
	// 开始生成随机nonce值知道我们中止或者成功找到了一个合适的值
	var (
		attempts = int64(0) // 初始化一个尝试次数的变量,下面会利用该变量耍一些花枪
		nonce    = seed // 初始化为seed值,后面每次尝试以后会累加
	)
	logger := log.New("miner", id)
	logger.Trace("Started ethash search for new nonces", "seed", seed)
	for {
		select {
		case <-abort: // 中止命令
			// 挖矿中止,更新状态,中止当前操作,返回空
			logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
			ethash.hashrate.Mark(attempts)
			return
		default: // 默认执行
			// 我们没必要在每一次尝试nonce值的时候更新hash率,可以在尝试了2的X次方nonce值以后再更新即可
			attempts++ // 通过次数attemp来控制
			if (attempts % (1 << 15)) == 0 {// 这里是定的2的15次方,位操作符请参考 http://www.cnblogs.com/Evsward/p/go.html#%E5%B8%B8%E9%87%8F
				ethash.hashrate.Mark(attempts) // 满足条件了以后,要更新ethash的hash率字段的状态值
				attempts = 0 // 重置尝试次数
			}
			// 为这个nonce值计算pow值
			digest, result := hashimotoFull(dataset, hash, nonce) // 调用的hashimotoFull函数在本包的算法库中,后面会介绍。
			if new(big.Int).SetBytes(result).Cmp(target) <= 0 { // 验证标准,后面介绍
				// 找到正确nonce值,创建一个基于它的新的区块头
				header = types.CopyHeader(header)
				header.Nonce = types.EncodeNonce(nonce) // 将输入的整型值转换为一个区块nonce值
				header.MixDigest = common.BytesToHash(digest) // 将字节数组转换为Hash对象【Hash是32位的根据任意输入数据的Keccak256哈希算法的返回值】
				// 封装返回一个区块
				select {
				case found <- block.WithSeal(header):
					logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
				case <-abort:
					logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
				}
				return
			}
			nonce++ // 累加nonce
		}
	}
}

mine方法主要是随机数的运算和块头的重构。在评论中,我们还为随机数的工作留下了漏洞。这部分内容将被转移到算法库中进行介绍。

算法

ethash软件包包含几个以算法开头的文件。这些文件的内容是pow支持挖掘操作的核心算法。首先,我们继续研究上方左侧的凹坑。

hashimotoFull函数

该功能位于ethash / algorithm.go文件中,

桥本功能

继续分析,上面的hashimotoFull函数返回hashimoto函数的返回值。我们在上面的概念部分介绍了hashimoto算法。如果您不了解源代码,则可以返回上面以了解更多信息,然后返回此处。继续学习。

// 该函数与hashimotoFull有着相同的愿景:在传入的数据集中通过hash和nonce值计算加密值
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
	// 计算数据集的理论的行数
	rows := uint32(size / mixBytes)
	// 合并header+nonce到一个40字节的seed
	seed := make([]byte, 40) // 创建一个长度为40的字节数组,名字为seed
	copy(seed, hash)// 将区块头的hash(上面提到了Hash对象是32字节大小)拷贝到seed中。
	binary.LittleEndian.PutUint64(seed[32:], nonce) // 将nonce值填入seed的后(40-32=8)字节中去,(nonce本身就是uint64类型,是64位,对应8字节大小),正好把hash和nonce完整的填满了40字节的seed
	seed = crypto.Keccak512(seed) // seed经历一遍Keccak512加密
	seedHead := binary.LittleEndian.Uint32(seed) // 从seed中获取区块头,代码后面详解
	// 开始与重复seed的混合
	mix := make([]uint32, mixBytes/4)// mixBytes常量= 128,mix的长度为32,元素为uint32,是32位,对应为4字节大小。所以mix总共大小为4*32=128字节大小
	for i := 0; i < len(mix); i++ {
		mix[i] = binary.LittleEndian.Uint32(seed[i*4:])// 共循环32次,前16和后16位的元素值相同
	}
	// 做一个temp,与mix结构相同,长度相同
	temp := make([]uint32, len(mix))
	for i := 0; i < loopAccesses; i++ { // loopAccesses常量 = 64,循环64次
		parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows // mix[i%len(mix)]是循环依次调用mix的元素值,fnv函数在本代码后面详解
		for j := uint32(0); j < mixBytes/hashBytes; j++ {
			copy(temp[j*hashWords:], lookup(2*parent+j))// 通过用种子seed生成的mix数据进行FNV哈希操作以后的数值作为参数去查找源数据(太绕了)拷贝到temp中去。
		}
		fnvHash(mix, temp) // 将mix中所有元素都与temp中对应位置的元素进行FNV hash运算
	}
	// mix大混淆
	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])
	}
	// 最后有效数据只在前8个位置,后面的数据经过上面的循环混淆以后没有价值了,所以将mix的长度减到8,保留前8位有效数据。
	mix = mix[:len(mix)/4]
	digest := make([]byte, common.HashLength) // common.HashLength=32,创建一个长度为32的字节数组digest
	for i, val := range mix {
		binary.LittleEndian.PutUint32(digest[i*4:], val)// 再把长度为8的mix分散到32位的digest中去。
	}
	return digest, crypto.Keccak256(append(seed, digest...))
}

除了由hashimotoFull函数调用之外,此函数还将由hashimotoLight函数调用。顾名思义,hashimotoLight与hashimotoFull的存在有关。如有可能,稍后将介绍HashimotoLight(请参阅是否可以绕过我们的路线)。

下划线和位运算|

上面的代码中的seedHead:= binary.LittleEndian.Uint32(seed),我们选择了单个练习,然后跳转到内部方法,如下所示:

func (littleEndian) Uint32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}

FNV哈希算法

FNV来自三个创建者的名称。我们知道哈希算法的最重要目标是均匀分布(高度分散)并避免冲突。最好对相似的源数据进行加密并且完全不同,即使它们只有一个字母也不相同,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]
	}
}

0x01000193是FNV哈希算法的哈希素数(素数,也称为素数,只能被1及其自身整除),该哈希算法将基于常量执行哈希运算。 0x01000193是32位数据的FNV的哈希素数。

验证方法

我们一直提到,战俘难以计算。上面的长篇章深刻地反映了这一点,但是pow易于验证,因此本节讨论ethash的pow的验证方法。这种验证方法也很容易找到。这是我在上面的mine方法中的注释中留下的坑:

new(big.Int).SetBytes(result).Cmp(target) <= 0

我们对对应于nonce摘要方法hashimoto算法的加密值的核心计算返回摘要的两个值和一个结果,从这行代码中我们可以看到结果的值与验证方法。最终,结果由hashimoto算法中的crypto.Keccak256(append(seed,摘要...)的Keccak256加密,并且摘要值也显示在参数列表中。获得结果值后,上面的表达式必须执行的代码行此表达式行非常简单,主要含义是将结果值与目标值进行比较,如果小于或等于0,则将其传递。

那么目标是什么?

目标是在mine方法主体的第一个变量声明部分中定义的,

target = new(big.Int).Div(maxUint256, header.Difficulty)

可以看出,目标的定义是基于块标题中的难度值计算的。因此,这验证了我们在概念部分首先提到的内容,我们可以通过调整Difficulty值来控制战俘操作的难度,生成正确随机数的难度,并实现可控战俘工作量的目标。

摘要

在此处读取代码时,闭环已完成。结合之前的“挖矿”,我们经历了以太坊战俘的整个过程。我在整个过程中都没有懈怠。从内核的入口开始,我们将源代码剥离了(实际上,到目前为止,以太坊的势力并未像我想的那样真正使用DAG)。到目前为止,我们对POW和以太坊ethash的实现有了深刻的理解和理解。我相信,如果我们被允许实施一套战俘,我们也完全有能力。如果您在阅读本文时有任何疑问,可以给我留言,我一定会及时答复。

参考资料

以太坊源代码,以太坊官方文件,网络术语解释文章

有关更多文章,请访问Xingzhedai的博客专区。

创亿伙伴111.png

挖矿算法dagger
版权声明

本文仅代表作者观点,不代表本站立场。本文系作者授权发表,未经许可,不得转载。

发表评论

评论列表(0人评论 , 20人围观)
☹还没有评论,来说两句吧...