区块链是做为分布式系统来构建的,因为它们不依赖于一个中央权威,所以分散的节点须要就交易的有效与否达成一致,而达成一致的机制即是共识算法。node
以太坊目前的算法是相似POW的算法:ethash。它除了和比特币同样须要消耗电脑资源外进行计算外,还考虑了对专用矿机的抵制,增长了挖矿的公平性。算法
POW即工做量证实,也就是经过工做结果来证实你完成了相应的工做。 它的初衷但愿全部人可参与,门槛低(验证容易),可是获得结果难(计算复杂)。在这一点上,只匹配部分特征的hash算法(不可逆)很是符合要求。数组
经过不断地更换随机数来获得哈希值,比较是否小于给定值(难度),符合的即为合适的结果。 随机数通常来自区块header结构里的nonce字段。 由于出块时间是必定的,但整体算力是不肯定的,因此难度通常会根据时间来调整。缓存
ethash与pow相似,数据来源除了像比特币同样来自header结构和nonce,还有本身定的一套数据集dataset。 精简后的核心代码以下:bash
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
var (
header = block.Header()
hash = ethash.SealHash(header).Bytes()
target = new(big.Int).Div(two256, header.Difficulty)
number = header.Number.Uint64()
dataset = ethash.dataset(number, false)
)
var (
attempts = int64(0)
nonce = seed
)
for {
attempts++
digest, result := hashimotoFull(dataset.dataset, hash, nonce)
if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
// Correct nonce found, create a new header with it
header = types.CopyHeader(header)
header.Nonce = types.EncodeNonce(nonce)
header.MixDigest = common.BytesToHash(digest)
...
}
...
}
...
}
复制代码
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()))
}
复制代码
在miner方法中hashimotoFull返回result,result <= target,则计算到合适的nonce了(挖到矿了,矿工的努力终于没有白费哈哈)。而target则是2^256/难度,result的来源则是随机数nonce,区块头的hash值以及数据集dataset(另外,另外一个返回值摘要digest存储在区块头,用来和验证获得的digest进行核对)。网络
说完了总体,下面将重点对dataset和header.Difficulty进行具体分析。app
该dataset是ethash的主要部分,主要是为了抵抗ASIC矿机的。由于生成的dataset很大(初始就有1G),因此该算法的性能瓶颈不在于cpu运算速度,而在于内存读取速度。大内存是昂贵的,而且普通计算机现有内存也足够跑了,经过内存来限制,去除专用硬件的运算优点。less
ethash是Dagger-Hashimoto算法的变种,因为ethash对原来算法的特征改变很大,因此不介绍算法的原理了。只结合现有的ethash源码,对生成dataset和使用dataset,分红Dagger和Hashimoto两部分讨论。dom
Dagger是用来生成dataset的。 async
如图所示:
verifySeal 全量级验证部分
if fulldag {
dataset := ethash.dataset(number, true)
if dataset.generated() {
digest, result = hashimotoFull(dataset.dataset, ethash.SealHash(header).Bytes(), header.Nonce.Uint64())
// Datasets are unmapped in a finalizer. Ensure that the dataset stays alive
// until after the call to hashimotoFull so it's not unmapped while being used.
runtime.KeepAlive(dataset)
} else {
// Dataset not yet generated, don't hang, use a cache instead
fulldag = false
}
}
复制代码
verifySeal 轻量级验证部分
if !fulldag {
cache := ethash.cache(number)
size := datasetSize(number)
if ethash.config.PowMode == ModeTest {
size = 32 * 1024
}
digest, result = hashimotoLight(size, cache.cache, ethash.SealHash(header).Bytes(), header.Nonce.Uint64())
// Caches are unmapped in a finalizer. Ensure that the cache stays alive
// until after the call to hashimotoLight so it's not unmapped while being used.
runtime.KeepAlive(cache)
}
复制代码
generateCache生成cache:大体思路是在给定种子数组seed[]的状况下,对固定容量的一块buffer(即cache)进行一系列操做,使得buffer的数值分布变得随机、无规律可循。
func generateCache(dest []uint32, epoch uint64, seed []byte) {
log.Info("luopeng2 generateCache")
// Print some debug logs to allow analysis on low end devices
logger := log.New("epoch", epoch)
start := time.Now()
defer func() {
elapsed := time.Since(start)
logFn := logger.Debug
if elapsed > 3*time.Second {
logFn = logger.Info
}
logFn("Generated ethash verification cache", "elapsed", common.PrettyDuration(elapsed))
}()
// Convert our destination slice to a byte buffer
header := *(*reflect.SliceHeader)(unsafe.Pointer(&dest))
header.Len *= 4
header.Cap *= 4
cache := *(*[]byte)(unsafe.Pointer(&header))
// Calculate the number of theoretical rows (we'll store in one buffer nonetheless)
size := uint64(len(cache))
rows := int(size) / hashBytes
// Start a monitoring goroutine to report progress on low end devices
var progress uint32
done := make(chan struct{})
defer close(done)
go func() {
for {
select {
case <-done:
return
case <-time.After(3 * time.Second):
logger.Info("Generating ethash verification cache", "percentage", atomic.LoadUint32(&progress)*100/uint32(rows)/4, "elapsed", common.PrettyDuration(time.Since(start)))
}
}
}()
// Create a hasher to reuse between invocations
keccak512 := makeHasher(sha3.NewLegacyKeccak512())
// Sequentially produce the initial dataset
keccak512(cache, seed)
for offset := uint64(hashBytes); offset < size; offset += hashBytes {
keccak512(cache[offset:], cache[offset-hashBytes:offset])
atomic.AddUint32(&progress, 1)
}
// Use a low-round version of randmemohash
temp := make([]byte, hashBytes)
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)
}
}
// Swap the byte order on big endian systems and return
if !isLittleEndian() {
swap(cache)
}
}
复制代码
mine中获得dataset的代码
var (
header = block.Header()
hash = ethash.SealHash(header).Bytes()
target = new(big.Int).Div(two256, header.Difficulty)
number = header.Number.Uint64()
dataset = ethash.dataset(number, false)
)
复制代码
generateDataset函数生成整个dataset:大体思路是将数据分红多段,使用多个goroutine调用generateDatasetItem生成全部的。
func generateDataset(dest []uint32, epoch uint64, cache []uint32) {
// Print some debug logs to allow analysis on low end devices
logger := log.New("epoch", epoch)
start := time.Now()
defer func() {
elapsed := time.Since(start)
logFn := logger.Debug
if elapsed > 3*time.Second {
logFn = logger.Info
}
logFn("Generated ethash verification cache", "elapsed", common.PrettyDuration(elapsed))
}()
// Figure out whether the bytes need to be swapped for the machine
swapped := !isLittleEndian()
// Convert our destination slice to a byte buffer
header := *(*reflect.SliceHeader)(unsafe.Pointer(&dest))
header.Len *= 4
header.Cap *= 4
dataset := *(*[]byte)(unsafe.Pointer(&header))
// Generate the dataset on many goroutines since it takes a while
threads := runtime.NumCPU()
size := uint64(len(dataset))
var pend sync.WaitGroup
pend.Add(threads)
var progress uint32
for i := 0; i < threads; i++ {
go func(id int) {
defer pend.Done()
// Create a hasher to reuse between invocations
keccak512 := makeHasher(sha3.NewLegacyKeccak512())
// Calculate the data segment this thread should generate
batch := uint32((size + hashBytes*uint64(threads) - 1) / (hashBytes * uint64(threads)))
first := uint32(id) * batch
limit := first + batch
if limit > uint32(size/hashBytes) {
limit = uint32(size / hashBytes)
}
// Calculate the dataset segment
percent := uint32(size / hashBytes / 100)
for index := first; index < limit; index++ {
item := generateDatasetItem(cache, index, keccak512)
if swapped {
swap(item)
}
copy(dataset[index*hashBytes:], item)
if status := atomic.AddUint32(&progress, 1); status%percent == 0 {
logger.Info("Generating DAG in progress", "percentage", uint64(status*100)/(size/hashBytes), "elapsed", common.PrettyDuration(time.Since(start)))
}
}
}(i)
}
// Wait for all the generators to finish and return
pend.Wait()
}
复制代码
generateDataItem函数获得指定的dataset:大体思路是计算出cache数据的索引,经过fnv聚合算法将cache数据混入,最后获得dataItem。
func generateDatasetItem(cache []uint32, index uint32, keccak512 hasher) []byte {
//log.Info("luopeng1 generateDatasetItem")
// Calculate the number of theoretical rows (we use one buffer nonetheless)
rows := uint32(len(cache) / hashWords)
// Initialize the mix
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)
// Convert the mix to uint32s to avoid constant bit shifting
intMix := make([]uint32, hashWords)
for i := 0; i < len(intMix); i++ {
intMix[i] = binary.LittleEndian.Uint32(mix[i*4:])
}
// fnv it with a lot of random cache nodes based on index
for i := uint32(0); i < datasetParents; i++ {
parent := fnv(index^i, intMix[i%16]) % rows
fnvHash(intMix, cache[parent*hashWords:])
}
// Flatten the uint32 mix into a binary one and return
for i, val := range intMix {
binary.LittleEndian.PutUint32(mix[i*4:], val)
}
keccak512(mix, mix)
return mix
}
复制代码
前面或多或少说到了,不过为了脉络更清晰,着重梳理一下。 dataset是最终参与hashimoto的数据集,因此要获得dataset必须获得cache。因此不论是经过generateDataset函数获得dataset(所有的),仍是generateDatasetItem函数datasetItem(指定部分),它都来源于cache,而cache则来源于seed。 但因为seed是来源于指定的block而且加工处理规则是固定的,因此其余节点依然能够根据block-->seed-->cache-->dataset,生成一致的结果。
今后图即可以看出,不论是挖矿的节点仍是验证的节点(全量级验证),生成dataset调用的方法是相同的,参数number区块高度相同,那么dataset也相同(参数async在此处不影响,为true则会在后台生成dataset)。
Hashimoto聚合数据集 、区块头 hash和nonce生成最终值。
如图所示:
hashimotoFull函数
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
lookup := func(index uint32) []uint32 {
offset := index * hashWords
return dataset[offset : offset+hashWords]
}
return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}
复制代码
hashtoLight函数
func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
keccak512 := makeHasher(sha3.NewLegacyKeccak512())
lookup := func(index uint32) []uint32 {
rawData := generateDatasetItem(cache, index, keccak512)
data := make([]uint32, len(rawData)/4)
for i := 0; i < len(data); i++ {
data[i] = binary.LittleEndian.Uint32(rawData[i*4:])
}
return data
}
return hashimoto(hash, nonce, size, lookup)
}
复制代码
hashimoto函数:将header hash、nonce、dataset屡次哈希聚合获得最终值result
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
// Calculate the number of theoretical rows (we use one buffer nonetheless)
rows := uint32(size / mixBytes)
// Combine header+nonce into a 64 byte seed
seed := make([]byte, 40)
copy(seed, hash)
binary.LittleEndian.PutUint64(seed[32:], nonce)
seed = crypto.Keccak512(seed)
seedHead := binary.LittleEndian.Uint32(seed)
// Start the mix with replicated seed
mix := make([]uint32, mixBytes/4)
for i := 0; i < len(mix); i++ {
mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])
}
// Mix in random dataset nodes
temp := make([]uint32, len(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)
}
// Compress 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])
}
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...))
}
复制代码
consensus\ethash\algorithm.go里面的generateCache、generateDataset、generateDatasetItem、hashimoto函数这里只提供大体的思路并无深究,由于它涉及到了一些密码学算法,单独看代码我也没看懂。另外这部分细节对总体理解ethash算法没有太大影响,往后研究密码学算法再分析这里好了。
func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
next := new(big.Int).Add(parent.Number, big1)
switch {
case config.IsConstantinople(next):
return calcDifficultyConstantinople(time, parent)
case config.IsByzantium(next):
return calcDifficultyByzantium(time, parent)
case config.IsHomestead(next):
return calcDifficultyHomestead(time, parent)
default:
return calcDifficultyFrontier(time, parent)
}
}
复制代码
以太坊难度通过多个阶段的调整,会根据区块高度所在的阶段使用该阶段的计算公式。因为目前是君士坦丁堡阶段,因此只对该代码进行说明。
君士坦丁堡计算难度的函数是makeDifficultyCalculator,因为注释里面已经包含了相应的公式了,代码就不贴出来了,梳理以下:
step = parent_diff // 2048
direction = max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99)
periodCount = (block.number - (5000000-1)) if (block.number >= (5000000-1)) else 0
diff = parent_diff + step *direction+ 2^(periodCount - 2)
这部分难度值是在父块的基础上进行微调的。调整的单位是step,根据如今的块与父块的时间间隔,以及是否存在叔块获得一个单位调整幅度direction。step *direction即为调整值。 也就是说若是时间间隔过小,direction为正,难度会增长一点,间隔过小,难度会减小一点。为了防止总是出叔块,会将时间拉长一点,难度增长。若出现意外状况或程序有漏洞致使难度大大下降,最低也有一个阀值(-99)。 值得注意的是,除了direction有最低阀值,parent_diff + step *direction也存在一个最低难度。
// minimum difficulty can ever be (before exponential factor)
if x.Cmp(params.MinimumDifficulty) < 0 {
x.Set(params.MinimumDifficulty)
}
复制代码
MinimumDifficulty = big.NewInt(131072) // The minimum that the difficulty may ever be.
复制代码
指数增加这部分在以太坊中叫作难度炸弹(也称为冰河时代)。因为以太坊的共识算法将从pow过渡到pos,设计一个难度炸弹,会让挖矿难度愈来愈大以致于不可能出块。这样矿工们就有个预期,平和过渡到pos。 目前在君士坦丁堡阶段,periodCount减去数字5000000,是为了防止指数部分增加过快,也就是延迟难度炸弹生效。
关于header.Difficulty具体的难度分析,这篇文章分析得很好。
我本地私有链当前区块高度是2278,配置的君士坦丁堡起始高度是5,即已进入君士坦丁堡阶段。 刚启动程序挖矿时,由于没有其余节点挖矿,因此上一区块时间与当前区块时间间隔相差很大,direction被设置成-99。而区块高度未超过5000000,指数部分的结果为0,父难度是689054,则最终难度是655790。
而一段时间后,direction基本上是在-一、0、1之间徘徊,挖矿的时间间隔平均也就十几秒。
INFO [06-09|17:49:46.059] luopeng2 block_timestamp - parent_timestamp=25
INFO [06-09|17:49:53.529] luopeng2 block_timestamp - parent_timestamp=7
INFO [06-09|17:51:19.667] luopeng2 block_timestamp - parent_timestamp=18
INFO [06-09|17:51:23.387] luopeng2 block_timestamp - parent_timestamp=4
INFO [06-09|18:46:02.608] luopeng2 block_timestamp - parent_timestamp=31
INFO [06-09|18:46:18.575] luopeng2 block_timestamp - parent_timestamp=1
复制代码
目前以太坊网络情况正常,算力稳定,ethgasstation 网站上查到的时间间隔也与本身本地状况差很少
综述,ethash基本思路和比特币的pow相似,都是不断随机nonce获得的值与难度进行比较,知足条件则挖矿成功,不然继续尝试。与比特币比拼cpu算力不一样的是,ethash经过生成一个巨大的数据集,经过限制内存来防止具有强大算力的ASIC矿机垄断,加强了去中心化能力。