领wfc糖果

领wfc糖果

各种领wfc糖果的攻略,一起来薅羊毛
wfc矿机拍卖

wfc矿机拍卖

wificoin开源矿机
开源wificoin

开源wificoin

给开源wificoin项目打call

用 Go 构建一个区块链 -- Part 4: 交易(1)

区块链技术guazi 发表了文章 • 0 个评论 • 120 次浏览 • 2018-03-20 14:40 • 来自相关话题

[]引言[/]

交易(transaction)是比特币的核心所在,而区块链的唯一目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。在今天的文章中,我们将会开始实现交易这个部分。不过,由于交易是很大的话题,我会把它分为两部分来讲:在今天这个部分,我们会实现交易的通用机制。在第二部分,我们会继续讨论它的一些细节。

此外,由于代码实现变化很大,就不在文章中罗列所有代码了,这里 可以看到所有的改动。
[]没有勺子[/]

如果以前开发过 web 应用,在支付的实现环节,你可能会在数据库中创建这样两张表:

accounts
transactions

account(账户)会存储用户信息,里面包括了个人信息和余额。transaction(交易)会存储资金转移信息,也就是资金从一个账户转移到另一个账户这样的内容。在比特币中,支付是另外一种完全不同的方式:

    1、没有账户(account)
    2、没有余额(balance)
    3、没有住址(address)
    4、没有货币(coin)
    5、没有发送人和接收人(sender,receiver)(这里所说的发送人和接收人是基于目前现实生活场景,交易双方与人是一一对应的。而在比特币中,“交易双方”是地址,地址背后才是人,人与地址并不是一一对应的关系,一个人可能有很多个地址。)

鉴于区块链是一个公开开放的数据库,所以我们并不想要存储钱包所有者的敏感信息(所以具有一定的匿名性)。资金不是通过账户来收集,交易也不是从一个地址将钱转移到另一个地址,也没有一个字段或者属性来保存账户余额。交易就是区块链要表达的所有内容。那么,交易里面到底有什么内容呢?
[]比特币交易[/]

一笔交易由一些输入(input)和输出(output)组合而来:


type Transaction struct {
    ID       []byte
    Vin     []TXInput
    Vout   []TXOutput
}

对于每一笔新的交易,它的输入会引用(reference)之前一笔交易的输出(这里有个例外,也就是我们待会儿要谈到的 coinbase 交易)。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中。交易的输出,也就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:






注意:

    1、有一些输出并没有被关联到某个输入上
    2、一笔交易的输入可以引用之前多笔交易的输出
    3、一个输入必须引用一个输出

贯穿本文,我们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“账户(account)” 等等这样的词。但是在比特币中,实际并不存在这样的概念。交易仅仅是通过一个脚本(script)来锁定(lock)一些价值(value),而这些价值只可以被锁定它们的人解锁(unlock)。
[]交易输出[/]

让我们先从输出(output)开始:

type TXOutput struct {
    Value     int
    ScriptPubKey     string
}

实际上,正是输出里面存储了“币”(注意,也就是上面的 Value 字段)。而这里的存储,指的是用一个数学难题对输出进行锁定,这个难题被存储在 ScriptPubKey 里面。在内部,比特币使用了一个叫做 Script 的脚本语言,用它来定义锁定和解锁输出的逻辑。虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之),并不复杂,但是我们并不会在这里讨论它的细节。你可以在这里 找到详细解释。

在比特币中,value 字段存储的是 satoshi 的数量,而不是>有 BTC 的数量。一个 satoshi 等于一百万分之一的 >BTC(0.00000001 BTC),这也是比特币里面最小的货币单位>(就像是 1 分的硬币)。

由于还没有实现地址(address),所以目前我们会避免涉及逻辑相关的完整脚本。ScriptPubKey 将会存储一个任意的字符串(用户定义的钱包地址)。

顺便说一下,有了一个这样的脚本语言,也意味着比特币其实也可以作为一个智能合约平台。

关于输出,非常重要的一点是:它们是不可再分的(invisible),这也就是说,你无法仅引用它的其中某一部分。要么不用,如果要用,必须一次性用完。当一个新的交易中引用了某个输出,那么这个输出必须被全部花费。如果它的值比需要的值大,那么就会产生一个找零,找零会返还给发送方。这跟现实世界的场景十分类似,当你想要支付的时候,如果一个东西值 1 美元,而你给了一个 5 美元的纸币,那么你会得到一个 4 美元的找零。

交易输入

这里是输入:

type TXInput struct {
    Txid     []byte
    Vout     int
    ScriptSig     string
}

正如之前所提到的,一个输入引用了之前一笔交易的一个输出:Txid 存储的是这笔交易的 ID,Vout 存储的是该输出在这笔交易中所有输出的索引(因为一笔交易可能有多个输出,需要有信息指明是具体的哪一个)。ScriptSig 是一个脚本,提供了可作用于一个输出的 ScriptPubKey 的数据。如果 ScriptSig 提供的数据是正确的,那么输出就会被解锁,然后被解锁的值就可以被用于产生新的输出;如果数据不正确,输出就无法被引用在输入中,或者说,也就是无法使用这个输出。这种机制,保证了用户无法花费属于其他人的币。

再次强调,由于我们还没有实现地址,所以 ScriptSig 将仅仅存储一个任意用户定义的钱包地址。我们会在下一篇文章中实现公钥(public key)和签名(signature)。

来简要总结一下。输出,就是 “币” 存储的地方。每个输出都会带有一个解锁脚本,这个脚本定义了解锁该输出的逻辑。每笔新的交易,必须至少有一个输入和输出。一个输入引用了之前一笔交易的输出,并提供了数据(也就是 ScriptSig 字段),该数据会被用在输出的解锁脚本中解锁输出,解锁完成后即可使用它的值去产生新的输出。

也就是说,每一笔输入都是之前一笔交易的输出,那么从一笔交易开始不断往前追溯,它涉及的输入和输出到底是谁先存在呢?换个说法,这是个鸡和蛋谁先谁后的问题,是先有蛋还是先有鸡呢?
[]先有蛋[/]

在比特币中,是先有蛋,然后才有鸡。输入引用输出的逻辑,是经典的“蛋还是鸡”问题:输入先产生输出,然后输出使得输入成为可能。在比特币中,最先有输出,然后才有输入。换而言之,第一笔交易只有输出,没有输入。

当矿工挖出一个新的块时,它会向新的块中添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不需要引用之前一笔交易的输出。它“凭空”产生了币(也就是产生了新币),这也是矿工获得挖出新块的奖励,可以理解为“发行新币”。

在区块链的最初,也就是第一个块,叫做创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不需要引用之前交易的输出。因为在创世块之前根本不存在交易,也就没有不存在有交易输出。

来创建一个 coinbase 交易:


func NewCoinbaseTX(to, data string) *Transaction {
    if data == "" {
        data = fmt.Sprintf("Reward to '%s'", to)
    }

    txin := TXInput{[]byte{}, -1, data}
    txout := TXOutput{subsidy, to}
    tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
    tx.SetID()

    return &tx
}

coinbase 交易只有一个输出,没有输入。在我们的实现中,它的 Txid 为空,Vout 等于 -1。并且,在目前的视线中,coinbase 交易也没有在 ScriptSig 中存储一个脚本,而只是存储了一个任意的字符串。

在比特币中,第一笔 coinbase 交易包含了如下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看.

subsidy 是奖励的数额。在比特币中,实际并没有存储这个数字,而是基于区块总数进行计算而得:区块总数除以 210000 就是 subsidy。挖出创世块的奖励是 50 BTC,每挖出 210000 个块后,奖励减半。在我们的实现中,这个奖励值将会是一个常量(至少目前是)。
[]将交易保存到区块链[/]

从现在开始,每个块必须存储至少一笔交易。如果没有交易,也就不可能挖出新的块。这意味着我们应该移除 Block 的 Data 字段,取而代之的是存储交易:

type Block struct {
    Timestamp     int64
    Transactions     []*Transaction
    PrevBlockHash     []byte
    Hash     []byte
    Nonce     int
}

NewBlock 和 NewGenesisBlock 也必须做出相应改变:

func NewBlock(transactions []Transaction, prevBlockHash []byte) Block {
    block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
    ...
}

func NewGenesisBlock(coinbase Transaction) Block {
    return NewBlock([]*Transaction{coinbase}, []byte{})
}

接下来修改创建新链的函数:

func CreateBlockchain(address string) *Blockchain {
    ...
    err = db.Update(func(tx *bolt.Tx) error {
        cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
        genesis := NewGenesisBlock(cbtx)

        b, err := tx.CreateBucket([]byte(blocksBucket))
        err = b.Put(genesis.Hash, genesis.Serialize())
        ...
    })
    ...
}

现在,这个函数会接受一个地址作为参数,这个地址会用来接收挖出创世块的奖励。
[]工作量证明[/]

工作量证明算法必须要将存储在区块里面的交易考虑进去,以此保证区块链交易存储的一致性和可靠性。所以,我们必须修改 ProofOfWork.prepareData 方法:


func (pow *ProofOfWork) prepareData(nonce int) []byte {
    data := bytes.Join(
        [][]byte{
            pow.block.PrevBlockHash,
            pow.block.HashTransactions(), // This line was changed
            IntToHex(pow.block.Timestamp),        
            IntToHex(int64(targetBits)),
            IntToHex(int64(nonce)),
        },
        []byte{},
    )
    return data
}

不像之前使用 pow.block.Data,现在我们使用 pow.block.HashTransactions() :

func (b *Block) HashTransactions() []byte {
    var txHashes     [][]byte
    var txHash [32]byte

    for _, tx := range b.Transactions {
        txHashes = append(txHashes, tx.ID)
    }
    txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))

    return txHash[:]
}

我们使用哈希提供数据的唯一表示,这个之前也遇到过。我们想要通过仅仅一个哈希,就可以识别一个块里面的所有交易。为此,我们获得每笔交易的哈希,将它们关联起来,然后获得一个连接后的组合哈希。

比特币使用了一个更加复杂的技术:它将一个块里面包含的所有交易表示为一个  Merkle tree ,然后在工作量证明系统中使用树的根哈希(root hash)。这个方法能够让我们快速检索一个块里面是否包含了某笔交易,即只需 root hash 而无需下载所有交易即可完成判断。

来检查一下到目前为止是否正确:


$ blockchain_go createblockchain -address Ivan 00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!

很好!我们已经获得了第一笔挖矿奖励,但是,我们要如何查看余额呢?
[]未花费的交易输出[/]

我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO)。未花费(unspent) 指的是这个输出还没有被包含在任何交易的输入中,或者说没有被任何输入引用。在上面的图示中,未花费的输出是:

    1、tx0, output 1;
    2、tx1, output 0;
    3、tx3, output 0;
    4、tx4, output 0.

当然了,当我们检查余额时,我们并不需要知道整个区块链上所有的 UTXO,只需要关注那些我们能够解锁的那些 UTXO(目前我们还没有实现密钥,所以我们将会使用用户定义的地址来代替)。首先,让我们定义在输入和输出上的锁定和解锁方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
    return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
     return out.ScriptPubKey == unlockingData
}

在这里,我们只是将 script 字段与 unlockingData 进行了比较。在后续文章我们基于私钥实现了地址以后,会对这部分进行改进。

下一步,找到包含未花费输出的交易,这一步相当困难:


func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
    var unspentTXs []Transaction
    spentTXOs := make(map[string][]int)
    bci := bc.Iterator()

    for {
        block := bci.Next()

        for _, tx := range block.Transactions {
            txID := hex.EncodeToString(tx.ID)

        Outputs:
            for outIdx, out := range tx.Vout {
                // Was the output spent?
                if spentTXOs[txID] != nil {
                    for _, spentOut := range spentTXOs[txID] {
                        if spentOut == outIdx {
                            continue Outputs
                        }
                    }
                }
 
                if out.CanBeUnlockedWith(address) {
                    unspentTXs = append(unspentTXs, *tx)
                }
            }

            if tx.IsCoinbase() == false {
                for _, in := range tx.Vin {
                    if in.CanUnlockOutputWith(address) {
                        inTxID := hex.EncodeToString(in.Txid)
                        spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
                    }
                }
            }
        }

        if len(block.PrevBlockHash) == 0 {
            break
        }
    }

    return unspentTXs
}

由于交易被存储在区块里,所以我们不得不检查区块链里的每一笔交易。从输出开始:


if out.CanBeUnlockedWith(address) {
    unspentTXs = append(unspentTXs, tx)
}

如果一个输出被一个地址锁定,并且这个地址恰好是我们要找的未花费交易输出的地址,那么这个输出就是我们想要的。不过在获取它之前,我们需要检查该输出是否已经被包含在一个输入中,也就是检查它是否已经被花费了:

if spentTXOs[txID] != nil {
    for _, spentOut := range spentTXOs[txID] {
        if spentOut == outIdx {
            continue Outputs
        }
    }
}

我们跳过那些已经被包含在其他输入中的输出(被包含在输入中,也就是说明这个输出已经被花费,无法再用了)。检查完输出以后,我们将所有能够解锁给定地址锁定的输出的输入聚集起来(这并不适用于 coinbase 交易,因为它们不解锁输出):

if tx.IsCoinbase() == false {
    for _, in := range tx.Vin {
        if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
        }
    }
}

这个函数返回了一个交易列表,里面包含了未花费输出。为了计算余额,我们还需要一个函数将这些交易作为输入,然后仅返回一个输出:


func (bc *Blockchain) FindUTXO(address string) []TXOutput {
    var UTXOs []TXOutput
    unspentTransactions := bc.FindUnspentTransactions(address)

        for _, tx := range unspentTransactions {
            for _, out := range tx.Vout {
                if out.CanBeUnlockedWith(address) {
                    UTXOs = append(UTXOs, out)
                }
            }
        }

        return UTXOs
}

就是这么多了!现在我们来实现 getbalance 命令:


func (cli *CLI) getBalance(address string) {
    bc := NewBlockchain(address)
    defer bc.db.Close()

    balance := 0
    UTXOs := bc.FindUTXO(address)

    for _, out := range UTXOs {
         balance += out.Value
    }
    fmt.Printf("Balance of '%s': %d\n", address, balance)
}

账户余额就是由账户地址锁定的所有未花费交易输出的总和。

在挖出创世块以后,来检查一下我们的余额:

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10

这就是我们的第一笔钱!
[]发送币[/]

现在,我们想要给其他人发送一些币。为此,我们需要创建一笔新的交易,将它放到一个块里,然后挖出这个块。之前我们只实现了 coinbase 交易(这是一种特殊的交易),现在我们需要一种通用的交易:


func NewUTXOTransaction(from, to string, amount int, bc Blockchain) Transaction {
    var inputs []TXInput
    var outputs []TXOutput

    acc, validOutputs := bc.FindSpendableOutputs(from, amount)

    if acc < amount {
        log.Panic("ERROR: Not enough funds")
    }
 
    // Build a list of inputs
    for txid, outs := range validOutputs {
        txID, err := hex.DecodeString(txid)

        for _, out := range outs {
            input := TXInput{txID, out, from}
            inputs = append(inputs, input)
        }
    }
     // Build a list of outputs
     outputs = append(outputs, TXOutput{amount, to})
    if acc > amount {
        outputs = append(outputs, TXOutput{acc - amount, from}) // a change
    }

    tx := Transaction{nil, inputs, outputs}
    tx.SetID()

    return &tx
}

在创建新的输出前,我们首先必须找到所有的未花费输出,并且确保它们存储了足够的值(value),这就是 FindSpendableOutputs 方法做的事情。随后,对于每个找到的输出,会创建一个引用该输出的输入。接下来,我们创建两个输出:

一个由接收者地址锁定。这是给实际给其他地址转移的币。

一个由发送者地址锁定。这是一个找零。只有当未花费输出超过新交易所需时产生。记住:输出是不可再分的。

FindSpendableOutputs 方法基于之前定义的 FindUnspentTransactions 方法:


func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) { 
    unspentOutputs := make(map[string][]int)
    unspentTXs := bc.FindUnspentTransactions(address)
    accumulated := 0

Work:
    for _, tx := range unspentTXs {
        txID := hex.EncodeToString(tx.ID)

        for outIdx, out := range tx.Vout {
            if out.CanBeUnlockedWith(address) &&
                accumulated < amount { accumulated += out.Value
                unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

                if accumulated >= amount {
                    break Work
                }
            }
        }
    }

    return accumulated, unspentOutputs
}

这个方法对所有的未花费交易进行迭代,并对它的值进行累加。当累加值大于或等于我们想要传送的值时,它就会停止并返回累加值,同时返回的还有通过交易 ID 进行分组的输出索引。我们并不想要取出超出需要花费的钱。

现在,我们可以修改 Blockchain.MineBlock 方法:

func (bc Blockchain) MineBlock(transactions []Transaction) {
    ...
    newBlock := NewBlock(transactions, lastHash)
    ...
}

最后,让我们来实现 send 方法:


func (cli *CLI) send(from, to string, amount int) {
    bc := NewBlockchain(from)
    defer bc.db.Close()

    tx := NewUTXOTransaction(from, to, amount, bc)
    bc.MineBlock([]*Transaction{tx})
    fmt.Println("Success!")
}

发送币意味着创建新的交易,并通过挖出新块的方式将交易打包到区块链中。不过,比特币并不是一连串立刻完成这些事情(不过我们的实现是这么做的)。相反,它会将所有新的交易放到一个内存池中(mempool),然后当一个矿工准备挖出一个新块时,它就从内存池中取出所有的交易,创建一个候选块。只有当包含这些交易的块被挖出来,并添加到区块链以后,里面的交易才开始确认。

让我们来检查一下发送币是否能工作:


$ blockchain_go send -from Ivan -to Pedro -amount 6 00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 4

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6

很好!现在,让我们创建更多的交易,确保从多个输出中发送币也正常工作:


$ blockchain_go send -from Pedro -to Helen -amount 2 00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf

Success!

$ blockchain_go send -from Ivan -to Helen -amount 2 000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa

Success!

现在,Helen 的币被锁定在了两个输出中:一个来自 Pedro,一个来自 Ivan。让我们把它们发送给其他人:


$ blockchain_go send -from Helen -to Rachel -amount 3 000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3

看起来没问题!现在,来测试一些失败的情况:


$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2
[]总结[/]

虽然不容易,但是现在终于实现交易了!不过,我们依然缺少了一些像比特币那样的一些关键特性:

    1、地址(address)。我们还没有基于私钥(private key)的真实地址。

    2、奖励(reward)。现在挖矿是肯定无法盈利的!

    3、UTXO 集。获取余额需要扫描整个区块链,而当区块非常多的时候,这么做就会花费很长时间。并且,如果我们想要验证后续交易,也需要花费很长时间。而 UTXO 集就是为了解决这些问题,加快交易相关的操作。

    4、内存池(mempool)。在交易被打包到块之前,这些交易被存储在内存池里面。在我们目前的实现中,一个块仅仅包含一笔交易,这是相当低效的。


  查看全部
    []引言[/]


交易(transaction)是比特币的核心所在,而区块链的唯一目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。在今天的文章中,我们将会开始实现交易这个部分。不过,由于交易是很大的话题,我会把它分为两部分来讲:在今天这个部分,我们会实现交易的通用机制。在第二部分,我们会继续讨论它的一些细节。

此外,由于代码实现变化很大,就不在文章中罗列所有代码了,这里 可以看到所有的改动。
    []没有勺子[/]


如果以前开发过 web 应用,在支付的实现环节,你可能会在数据库中创建这样两张表:

accounts
transactions


account(账户)会存储用户信息,里面包括了个人信息和余额。transaction(交易)会存储资金转移信息,也就是资金从一个账户转移到另一个账户这样的内容。在比特币中,支付是另外一种完全不同的方式:

    1、没有账户(account)
    2、没有余额(balance)
    3、没有住址(address)
    4、没有货币(coin)
    5、没有发送人和接收人(sender,receiver)(这里所说的发送人和接收人是基于目前现实生活场景,交易双方与人是一一对应的。而在比特币中,“交易双方”是地址,地址背后才是人,人与地址并不是一一对应的关系,一个人可能有很多个地址。)

鉴于区块链是一个公开开放的数据库,所以我们并不想要存储钱包所有者的敏感信息(所以具有一定的匿名性)。资金不是通过账户来收集,交易也不是从一个地址将钱转移到另一个地址,也没有一个字段或者属性来保存账户余额。交易就是区块链要表达的所有内容。那么,交易里面到底有什么内容呢?
    []比特币交易[/]


一笔交易由一些输入(input)和输出(output)组合而来:


type Transaction struct {
    ID       []byte
    Vin     []TXInput
    Vout   []TXOutput
}


对于每一笔新的交易,它的输入会引用(reference)之前一笔交易的输出(这里有个例外,也就是我们待会儿要谈到的 coinbase 交易)。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中。交易的输出,也就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:

1.png


注意:

    1、有一些输出并没有被关联到某个输入上
    2、一笔交易的输入可以引用之前多笔交易的输出
    3、一个输入必须引用一个输出

贯穿本文,我们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“账户(account)” 等等这样的词。但是在比特币中,实际并不存在这样的概念。交易仅仅是通过一个脚本(script)来锁定(lock)一些价值(value),而这些价值只可以被锁定它们的人解锁(unlock)。
    []交易输出[/]


让我们先从输出(output)开始:

type TXOutput struct {
    Value     int
    ScriptPubKey     string
}


实际上,正是输出里面存储了“币”(注意,也就是上面的 Value 字段)。而这里的存储,指的是用一个数学难题对输出进行锁定,这个难题被存储在 ScriptPubKey 里面。在内部,比特币使用了一个叫做 Script 的脚本语言,用它来定义锁定和解锁输出的逻辑。虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之),并不复杂,但是我们并不会在这里讨论它的细节。你可以在这里 找到详细解释。

在比特币中,value 字段存储的是 satoshi 的数量,而不是>有 BTC 的数量。一个 satoshi 等于一百万分之一的 >BTC(0.00000001 BTC),这也是比特币里面最小的货币单位>(就像是 1 分的硬币)。

由于还没有实现地址(address),所以目前我们会避免涉及逻辑相关的完整脚本。ScriptPubKey 将会存储一个任意的字符串(用户定义的钱包地址)。

顺便说一下,有了一个这样的脚本语言,也意味着比特币其实也可以作为一个智能合约平台。

关于输出,非常重要的一点是:它们是不可再分的(invisible),这也就是说,你无法仅引用它的其中某一部分。要么不用,如果要用,必须一次性用完。当一个新的交易中引用了某个输出,那么这个输出必须被全部花费。如果它的值比需要的值大,那么就会产生一个找零,找零会返还给发送方。这跟现实世界的场景十分类似,当你想要支付的时候,如果一个东西值 1 美元,而你给了一个 5 美元的纸币,那么你会得到一个 4 美元的找零。

交易输入

这里是输入:

type TXInput struct {
    Txid     []byte
    Vout     int
    ScriptSig     string
}


正如之前所提到的,一个输入引用了之前一笔交易的一个输出:Txid 存储的是这笔交易的 ID,Vout 存储的是该输出在这笔交易中所有输出的索引(因为一笔交易可能有多个输出,需要有信息指明是具体的哪一个)。ScriptSig 是一个脚本,提供了可作用于一个输出的 ScriptPubKey 的数据。如果 ScriptSig 提供的数据是正确的,那么输出就会被解锁,然后被解锁的值就可以被用于产生新的输出;如果数据不正确,输出就无法被引用在输入中,或者说,也就是无法使用这个输出。这种机制,保证了用户无法花费属于其他人的币。

再次强调,由于我们还没有实现地址,所以 ScriptSig 将仅仅存储一个任意用户定义的钱包地址。我们会在下一篇文章中实现公钥(public key)和签名(signature)。

来简要总结一下。输出,就是 “币” 存储的地方。每个输出都会带有一个解锁脚本,这个脚本定义了解锁该输出的逻辑。每笔新的交易,必须至少有一个输入和输出。一个输入引用了之前一笔交易的输出,并提供了数据(也就是 ScriptSig 字段),该数据会被用在输出的解锁脚本中解锁输出,解锁完成后即可使用它的值去产生新的输出。

也就是说,每一笔输入都是之前一笔交易的输出,那么从一笔交易开始不断往前追溯,它涉及的输入和输出到底是谁先存在呢?换个说法,这是个鸡和蛋谁先谁后的问题,是先有蛋还是先有鸡呢?
    []先有蛋[/]


在比特币中,是先有蛋,然后才有鸡。输入引用输出的逻辑,是经典的“蛋还是鸡”问题:输入先产生输出,然后输出使得输入成为可能。在比特币中,最先有输出,然后才有输入。换而言之,第一笔交易只有输出,没有输入。

当矿工挖出一个新的块时,它会向新的块中添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不需要引用之前一笔交易的输出。它“凭空”产生了币(也就是产生了新币),这也是矿工获得挖出新块的奖励,可以理解为“发行新币”。

在区块链的最初,也就是第一个块,叫做创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不需要引用之前交易的输出。因为在创世块之前根本不存在交易,也就没有不存在有交易输出。

来创建一个 coinbase 交易:


func NewCoinbaseTX(to, data string) *Transaction {
    if data == "" {
        data = fmt.Sprintf("Reward to '%s'", to)
    }

    txin := TXInput{[]byte{}, -1, data}
    txout := TXOutput{subsidy, to}
    tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
    tx.SetID()

    return &tx
}


coinbase 交易只有一个输出,没有输入。在我们的实现中,它的 Txid 为空,Vout 等于 -1。并且,在目前的视线中,coinbase 交易也没有在 ScriptSig 中存储一个脚本,而只是存储了一个任意的字符串。

在比特币中,第一笔 coinbase 交易包含了如下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看.

subsidy 是奖励的数额。在比特币中,实际并没有存储这个数字,而是基于区块总数进行计算而得:区块总数除以 210000 就是 subsidy。挖出创世块的奖励是 50 BTC,每挖出 210000 个块后,奖励减半。在我们的实现中,这个奖励值将会是一个常量(至少目前是)。
    []将交易保存到区块链[/]


从现在开始,每个块必须存储至少一笔交易。如果没有交易,也就不可能挖出新的块。这意味着我们应该移除 Block 的 Data 字段,取而代之的是存储交易:

type Block struct {
    Timestamp     int64
    Transactions     []*Transaction
    PrevBlockHash     []byte
    Hash     []byte
    Nonce     int
}


NewBlock 和 NewGenesisBlock 也必须做出相应改变:

func NewBlock(transactions []Transaction, prevBlockHash []byte) Block {
    block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
    ...
}

func NewGenesisBlock(coinbase Transaction) Block {
    return NewBlock([]*Transaction{coinbase}, []byte{})
}


接下来修改创建新链的函数:

func CreateBlockchain(address string) *Blockchain {
    ...
    err = db.Update(func(tx *bolt.Tx) error {
        cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
        genesis := NewGenesisBlock(cbtx)

        b, err := tx.CreateBucket([]byte(blocksBucket))
        err = b.Put(genesis.Hash, genesis.Serialize())
        ...
    })
    ...

}

现在,这个函数会接受一个地址作为参数,这个地址会用来接收挖出创世块的奖励。
    []工作量证明[/]


工作量证明算法必须要将存储在区块里面的交易考虑进去,以此保证区块链交易存储的一致性和可靠性。所以,我们必须修改 ProofOfWork.prepareData 方法:


func (pow *ProofOfWork) prepareData(nonce int) []byte {
    data := bytes.Join(
        [][]byte{
            pow.block.PrevBlockHash,
            pow.block.HashTransactions(), // This line was changed
            IntToHex(pow.block.Timestamp),        
            IntToHex(int64(targetBits)),
            IntToHex(int64(nonce)),
        },
        []byte{},
    )
    return data
}


不像之前使用 pow.block.Data,现在我们使用 pow.block.HashTransactions() :

func (b *Block) HashTransactions() []byte {
    var txHashes     [][]byte
    var txHash [32]byte

    for _, tx := range b.Transactions {
        txHashes = append(txHashes, tx.ID)
    }
    txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))

    return txHash[:]
}


我们使用哈希提供数据的唯一表示,这个之前也遇到过。我们想要通过仅仅一个哈希,就可以识别一个块里面的所有交易。为此,我们获得每笔交易的哈希,将它们关联起来,然后获得一个连接后的组合哈希。

比特币使用了一个更加复杂的技术:它将一个块里面包含的所有交易表示为一个  Merkle tree ,然后在工作量证明系统中使用树的根哈希(root hash)。这个方法能够让我们快速检索一个块里面是否包含了某笔交易,即只需 root hash 而无需下载所有交易即可完成判断。

来检查一下到目前为止是否正确:


$ blockchain_go createblockchain -address Ivan 00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!


很好!我们已经获得了第一笔挖矿奖励,但是,我们要如何查看余额呢?
    []未花费的交易输出[/]


我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO)。未花费(unspent) 指的是这个输出还没有被包含在任何交易的输入中,或者说没有被任何输入引用。在上面的图示中,未花费的输出是:

    1、tx0, output 1;
    2、tx1, output 0;
    3、tx3, output 0;
    4、tx4, output 0.

当然了,当我们检查余额时,我们并不需要知道整个区块链上所有的 UTXO,只需要关注那些我们能够解锁的那些 UTXO(目前我们还没有实现密钥,所以我们将会使用用户定义的地址来代替)。首先,让我们定义在输入和输出上的锁定和解锁方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
    return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
     return out.ScriptPubKey == unlockingData
}


在这里,我们只是将 script 字段与 unlockingData 进行了比较。在后续文章我们基于私钥实现了地址以后,会对这部分进行改进。

下一步,找到包含未花费输出的交易,这一步相当困难:


func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
    var unspentTXs []Transaction
    spentTXOs := make(map[string][]int)
    bci := bc.Iterator()

    for {
        block := bci.Next()

        for _, tx := range block.Transactions {
            txID := hex.EncodeToString(tx.ID)

        Outputs:
            for outIdx, out := range tx.Vout {
                // Was the output spent?
                if spentTXOs[txID] != nil {
                    for _, spentOut := range spentTXOs[txID] {
                        if spentOut == outIdx {
                            continue Outputs
                        }
                    }
                }
 
                if out.CanBeUnlockedWith(address) {
                    unspentTXs = append(unspentTXs, *tx)
                }
            }

            if tx.IsCoinbase() == false {
                for _, in := range tx.Vin {
                    if in.CanUnlockOutputWith(address) {
                        inTxID := hex.EncodeToString(in.Txid)
                        spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
                    }
                }
            }
        }

        if len(block.PrevBlockHash) == 0 {
            break
        }
    }

    return unspentTXs
}


由于交易被存储在区块里,所以我们不得不检查区块链里的每一笔交易。从输出开始:


if out.CanBeUnlockedWith(address) {
    unspentTXs = append(unspentTXs, tx)
}


如果一个输出被一个地址锁定,并且这个地址恰好是我们要找的未花费交易输出的地址,那么这个输出就是我们想要的。不过在获取它之前,我们需要检查该输出是否已经被包含在一个输入中,也就是检查它是否已经被花费了:

if spentTXOs[txID] != nil {
    for _, spentOut := range spentTXOs[txID] {
        if spentOut == outIdx {
            continue Outputs
        }
    }
}


我们跳过那些已经被包含在其他输入中的输出(被包含在输入中,也就是说明这个输出已经被花费,无法再用了)。检查完输出以后,我们将所有能够解锁给定地址锁定的输出的输入聚集起来(这并不适用于 coinbase 交易,因为它们不解锁输出):

if tx.IsCoinbase() == false {
    for _, in := range tx.Vin {
        if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
        }
    }
}


这个函数返回了一个交易列表,里面包含了未花费输出。为了计算余额,我们还需要一个函数将这些交易作为输入,然后仅返回一个输出:


func (bc *Blockchain) FindUTXO(address string) []TXOutput {
    var UTXOs []TXOutput
    unspentTransactions := bc.FindUnspentTransactions(address)

        for _, tx := range unspentTransactions {
            for _, out := range tx.Vout {
                if out.CanBeUnlockedWith(address) {
                    UTXOs = append(UTXOs, out)
                }
            }
        }

        return UTXOs
}


就是这么多了!现在我们来实现 getbalance 命令:


func (cli *CLI) getBalance(address string) {
    bc := NewBlockchain(address)
    defer bc.db.Close()

    balance := 0
    UTXOs := bc.FindUTXO(address)

    for _, out := range UTXOs {
         balance += out.Value
    }
    fmt.Printf("Balance of '%s': %d\n", address, balance)
}


账户余额就是由账户地址锁定的所有未花费交易输出的总和。

在挖出创世块以后,来检查一下我们的余额:

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10


这就是我们的第一笔钱!
    []发送币[/]


现在,我们想要给其他人发送一些币。为此,我们需要创建一笔新的交易,将它放到一个块里,然后挖出这个块。之前我们只实现了 coinbase 交易(这是一种特殊的交易),现在我们需要一种通用的交易:


func NewUTXOTransaction(from, to string, amount int, bc Blockchain) Transaction {
    var inputs []TXInput
    var outputs []TXOutput

    acc, validOutputs := bc.FindSpendableOutputs(from, amount)

    if acc < amount {
        log.Panic("ERROR: Not enough funds")
    }
 
    // Build a list of inputs
    for txid, outs := range validOutputs {
        txID, err := hex.DecodeString(txid)

        for _, out := range outs {
            input := TXInput{txID, out, from}
            inputs = append(inputs, input)
        }
    }
     // Build a list of outputs
     outputs = append(outputs, TXOutput{amount, to})
    if acc > amount {
        outputs = append(outputs, TXOutput{acc - amount, from}) // a change
    }

    tx := Transaction{nil, inputs, outputs}
    tx.SetID()

    return &tx
}


在创建新的输出前,我们首先必须找到所有的未花费输出,并且确保它们存储了足够的值(value),这就是 FindSpendableOutputs 方法做的事情。随后,对于每个找到的输出,会创建一个引用该输出的输入。接下来,我们创建两个输出:

一个由接收者地址锁定。这是给实际给其他地址转移的币。

一个由发送者地址锁定。这是一个找零。只有当未花费输出超过新交易所需时产生。记住:输出是不可再分的。

FindSpendableOutputs 方法基于之前定义的 FindUnspentTransactions 方法:


func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) { 
    unspentOutputs := make(map[string][]int)
    unspentTXs := bc.FindUnspentTransactions(address)
    accumulated := 0

Work:
    for _, tx := range unspentTXs {
        txID := hex.EncodeToString(tx.ID)

        for outIdx, out := range tx.Vout {
            if out.CanBeUnlockedWith(address) &&
                accumulated < amount { accumulated += out.Value
                unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

                if accumulated >= amount {
                    break Work
                }
            }
        }
    }

    return accumulated, unspentOutputs
}


这个方法对所有的未花费交易进行迭代,并对它的值进行累加。当累加值大于或等于我们想要传送的值时,它就会停止并返回累加值,同时返回的还有通过交易 ID 进行分组的输出索引。我们并不想要取出超出需要花费的钱。

现在,我们可以修改 Blockchain.MineBlock 方法:

func (bc Blockchain) MineBlock(transactions []Transaction) {
    ...
    newBlock := NewBlock(transactions, lastHash)
    ...
}


最后,让我们来实现 send 方法:


func (cli *CLI) send(from, to string, amount int) {
    bc := NewBlockchain(from)
    defer bc.db.Close()

    tx := NewUTXOTransaction(from, to, amount, bc)
    bc.MineBlock([]*Transaction{tx})
    fmt.Println("Success!")
}


发送币意味着创建新的交易,并通过挖出新块的方式将交易打包到区块链中。不过,比特币并不是一连串立刻完成这些事情(不过我们的实现是这么做的)。相反,它会将所有新的交易放到一个内存池中(mempool),然后当一个矿工准备挖出一个新块时,它就从内存池中取出所有的交易,创建一个候选块。只有当包含这些交易的块被挖出来,并添加到区块链以后,里面的交易才开始确认。

让我们来检查一下发送币是否能工作:


$ blockchain_go send -from Ivan -to Pedro -amount 6 00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 4

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6


很好!现在,让我们创建更多的交易,确保从多个输出中发送币也正常工作:


$ blockchain_go send -from Pedro -to Helen -amount 2 00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf

Success!

$ blockchain_go send -from Ivan -to Helen -amount 2 000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa

Success!


现在,Helen 的币被锁定在了两个输出中:一个来自 Pedro,一个来自 Ivan。让我们把它们发送给其他人:


$ blockchain_go send -from Helen -to Rachel -amount 3 000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3


看起来没问题!现在,来测试一些失败的情况:


$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

    []总结[/]


虽然不容易,但是现在终于实现交易了!不过,我们依然缺少了一些像比特币那样的一些关键特性:

    1、地址(address)。我们还没有基于私钥(private key)的真实地址。

    2、奖励(reward)。现在挖矿是肯定无法盈利的!

    3、UTXO 集。获取余额需要扫描整个区块链,而当区块非常多的时候,这么做就会花费很长时间。并且,如果我们想要验证后续交易,也需要花费很长时间。而 UTXO 集就是为了解决这些问题,加快交易相关的操作。

    4、内存池(mempool)。在交易被打包到块之前,这些交易被存储在内存池里面。在我们目前的实现中,一个块仅仅包含一笔交易,这是相当低效的。


 

用 Go 构建一个区块链 -- Part 3: 持久化和命令行接口

区块链技术guazi 发表了文章 • 0 个评论 • 145 次浏览 • 2018-03-19 15:20 • 来自相关话题

[]引言[/]
到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前的实现离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。
[]选择数据库[/]
目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。
那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是  LevelDB。而我们将要使用的是…
[]BoltDB[/]
因为它:
    1、非常简单和简约
    2、用 Go 实现
    3、不需要运行一个服务器
    4、能够允许我们构造想要的数据结构
BoltDB GitHub 上的 README 是这么说的:
Bolt 是一个纯键值存储的 Go 数据库,启发自 Howard Chu 的 LMDB. 它旨在为那些无须一个像 Postgres 和 MySQL 这样有着完整数据库服务器的项目,提供一个简单,快速和可靠的数据库。
由于 Bolt 意在用于提供一些底层功能,简洁便成为其关键所在。它的
API 并不多,并且仅关注值的获取和设置。仅此而已。

听起来跟我们的需求完美契合!来快速过一下:
Bolt 使用键值存储,这意味着它没有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,没有行和列。相反,数据被存储为键值对(key-value pair,就像 Golang 的 map)。键值对被存储在 bucket 中,这是为了将相似的键值对进行分组(类似 RDBMS 中的表格)。因此,为了获取一个值,你需要知道一个 bucket 和一个键(key)。
需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储(块)Block),我们需要对它们进行序列化,也就说,实现一个从 Go struct 转换到一个 byte array 的机制,同时还可以从一个 byte array 再转换回 Go struct。虽然我们将会使用  encoding/gob  来完成这一目标,但实际上也可以选择使用 JSON, XML, Protocol Buffers 等等。之所以选择使用 encoding/gob, 是因为它很简单,而且是 Go 标准库的一部分。
[]数据库结构[/]
在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:
简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:
    1、其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
    2、另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据
此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。
在 blocks 中,key -> value 为:







在 chainstate,key -> value 为:






详情可见 这里。
因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
    1、32 字节的 block-hash -> block 结构
    2、l -> 链中最后一个块的 hash
这就是实现持久化机制所有需要了解的内容了。
[]序列化[/]
上面提到,在 BoltDB 中,值只能是 []byte 类型,但是我们想要存储 Block 结构。所以,我们需要使用 encoding/gob 来对这些结构进行序列化。
让我们来实现 Block 的 Serialize 方法(为了简洁起见,此处略去了错误处理):

func (b *Block) Serialize() []byte {
     var result bytes.Buffer
    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(b)

    return result.Bytes()
}

这个部分比较直观:首先,我们定义一个 buffer 存储序列化之后的数据。然后,我们初始化一个 gob encoder 并对 block 进行编码,结果作为一个字节数组返回。
接下来,我们需要一个解序列化的函数,它会接受一个字节数组作为输入,并返回一个 Block. 它不是一个方法(method),而是一个单独的函数(function):

func DeserializeBlock(d []byte) *Block {
    var block Block decoder := gob.NewDecoder(bytes.NewReader(d))

    err := decoder.Decode(&block)
    return &block
}

这就是序列化部分的内容了。
[]持久化[/]
让我们从 NewBlockchain 函数开始。在之前的实现中,它会创建一个新的
Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:
    1、打开一个数据库文件
    2、检查文件里面是否已经存储了一个区块链
    3、如果已经存储了一个区块链:
        1、创建一个新的 Blockchain 实例
        2、设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
    4、如果没有区块链:
        1、创建创世块
        2、存储到数据库
        3、将创世块哈希保存为最后一个块的哈希
        4、创建一个新的 Blockchain 实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)
代码大概是这样:

func NewBlockchain() *Blockchain {
    var tip []byte
    db, err := bolt.Open(dbFile, 0600, nil)

    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))

        if b == nil {
            genesis := NewGenesisBlock()
            b, err := tx.CreateBucket([]byte(blocksBucket))
            err = b.Put(genesis.Hash, genesis.Serialize())
            err = b.Put([]byte("l"), genesis.Hash)
            tip = genesis.Hash
        } else {
            tip = b.Get([]byte("l"))
        }

        return nil

    })
    bc := Blockchain{tip, db}

    return &bc
}

来一段一段地看下代码:

db, err := bolt.Open(dbFile, 0600, nil)

这是打开一个 BoltDB 文件的标准做法。注意,即使不存在这样的文件,它也不会返回错误。

err = db.Update(func(tx *bolt.Tx) error {
 ...
 })

在 BoltDB 中,数据库操作通过一个事务(transaction)进行操作。有两种类型的事务:只读(read-only)和读写(read-write)。这里,打开的是一个读写事务(db.Update(...)),因为我们可能会向数据库中添加创世块。

b := tx.Bucket([]byte(blocksBucket))

if b == nil {
    genesis := NewGenesisBlock()
    b, err := tx.CreateBucket([]byte(blocksBucket))
    err = b.Put(genesis.Hash, genesis.Serialize())
    err = b.Put([]byte("l"), genesis.Hash)
    tip = genesis.Hash
} else {
    tip = b.Get([]byte("l"))
}

这里是函数的核心。在这里,我们先获取了存储区块的 bucket:如果存在,就从中读取 l 键;如果不存在,就生成创世块,创建 bucket,并将区块保存到里面,然后更新 l 键以存储链中最后一个块的哈希。
另外,注意创建 Blockchain 一个新的方式:

bc := Blockchain{tip, db}

这次,我们不在里面存储所有的区块了,而是仅存储区块链的 tip。另外,我们存储了一个数据库连接。因为我们想要一旦打开它的话,就让它一直运行,直到程序运行结束。因此,Blockchain 的结构现在看起来是这样:

type Blockchain struct {
    tip []byte
    db *bolt.DB
}

接下来我们想要更新的是 AddBlock 方法:现在向链中加入区块,就不是像之前向一个数组中加入一个元素那么简单了。从现在开始,我们会将区块存储在数据库里面:
func (bc *Blockchain) AddBlock(data string) {
    var lastHash []byte
 
    err := bc.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        lastHash = b.Get([]byte("l"))

        return nil
    })

    newBlock := NewBlock(data, lastHash)

    err = bc.db.Update(func(tx *bolt.Tx) 、error {
        b := tx.Bucket([]byte(blocksBucket))
        err := b.Put(newBlock.Hash, newBlock.Serialize())
        err = b.Put([]byte("l"), newBlock.Hash)
        bc.tip = newBlock.Hash

        return nil
    })
}

继续来一段一段分解开来:

err := bc.db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte(blocksBucket))
    lastHash = b.Get([]byte("l"))

    return nil
})

这是 BoltDB 事务的另一个类型(只读)。在这里,我们会从数据库中获取最后一个块的哈希,然后用它来挖出一个新的块的哈希:

newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
[]检查区块链[/]
现在,产生的所有块都会被保存到一个数据库里面,所以我们可以重新打开一个链,然后向里面加入新块。但是在实现这一点后,我们失去了之前一个非常好的特性:我们再也无法打印区块链的区块了,因为现在不是将区块存储在一个数组,而是放到了数据库里面。让我们来解决这个问题!
BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储,而且我们想要以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。故而,我们需要一个区块链迭代器(BlockchainIterator):

type BlockchainIterator struct {
    currentHash []byte
    db            *bolt.DB
}

每当要对链中的块进行迭代时,我们就会创建一个迭代器,里面存储了当前迭代的块哈希(currentHash)和数据库的连接(db)。通过 db,迭代器逻辑上被附属到一个区块链上(这里的区块链指的是存储了一个数据库连接的 Blockchain 实例),并且通过 Blockchain 方法进行创建:

func (bc Blockchain) Iterator() BlockchainIterator {
    bci := &BlockchainIterator{bc.tip, bc.db}

    return bci
}

注意,迭代器的初始状态为链中的 tip,因此区块将从头到尾,也就是从最新的到最旧的进行获取。实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。
BlockchainIterator 只会做一件事情:返回链中的下一个块。

func (i BlockchainIterator) Next() Block {
    var block *Block

    err := i.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        encodedBlock := b.Get(i.currentHash)
        block = DeserializeBlock(encodedBlock)

        return nil

    })
    i.currentHash = block.PrevBlockHash

    return block
}

这就是数据库部分的内容了!
[]CLI[/]
到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在 main 函数中简单执行了 NewBlockchain 和 bc.AddBlock 。是时候改变了!现在我们想要拥有这些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

所有命令行相关的操作都会通过 CLI 结构进行处理:

type CLI struct {
    bc *Blockchain
}

它的 “入口” 是 Run 函数:
func (cli *CLI) Run() {
    cli.validateArgs()

    addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
    printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)

    addBlockData := addBlockCmd.String("data", "", "Block data")

    switch os.Args[1] {
    case "addblock":
        err := addBlockCmd.Parse(os.Args[2:])
    case "printchain":
        err := printChainCmd.Parse(os.Args[2:])
    default:
        cli.printUsage()
        os.Exit(1)
    }

    if addBlockCmd.Parsed() {
        if *addBlockData == "" {
            addBlockCmd.Usage()
            os.Exit(1)
        }
        cli.addBlock(*addBlockData)
    }

    if printChainCmd.Parsed() {
        cli.printChain()
    }
}

我们会使用标准库里面的 flag 包来解析命令行参数:

addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")

首先,我们创建两个子命令: addblock 和 printchain, 然后给 addblock 添加 -data 标志。printchain 没有任何标志。

switch os.Args[1] {
case "addblock":
    err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
    err := printChainCmd.Parse(os.Args[2:])
default:
    cli.printUsage()
    os.Exit(1)
}

然后,我们检查用户提供的命令,解析相关的 flag 子命令:

if addBlockCmd.Parsed() {
    if *addBlockData == "" {
        addBlockCmd.Usage()
        os.Exit(1)
    }
    cli.addBlock(*addBlockData)
}

 if printChainCmd.Parsed() {
    cli.printChain()
}

接着检查解析是哪一个子命令,并调用相关函数:

func (cli *CLI) addBlock(data string) {
    cli.bc.AddBlock(data)
    fmt.Println("Success!")
}

func (cli *CLI) printChain() {
    bci := cli.bc.Iterator()

    for {
        block := bci.Next()

        fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        pow := NewProofOfWork(block)
        fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
        fmt.Println()
    if len(block.PrevBlockHash) == 0 {
        break
        }
    }
}

这部分内容跟之前的很像,唯一的区别是我们现在使用的是 BlockchainIterator 对区块链中的区块进行迭代:
记得不要忘了对 main 函数作出相应的修改:

func main() {
    bc := NewBlockchain()
    defer bc.db.Close()
   
    cli := CLI{bc}
    cli.Run()
}

注意,无论提供什么命令行参数,都会创建一个新的链。
这就是今天的所有内容了! 来看一下是不是如期工作:

$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block" 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan" 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13

Success!

$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee" 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148

Success!

$ blockchain_go printchain Prev.
hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true

Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true






[]总结[/]
在下篇文章中,我们将会实现地址,钱包,(可能实现)交易。尽请收听! 查看全部
    []引言[/]

到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前的实现离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。
    []选择数据库[/]

目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。
那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是  LevelDB。而我们将要使用的是…
    []BoltDB[/]

因为它:
    1、非常简单和简约
    2、用 Go 实现
    3、不需要运行一个服务器
    4、能够允许我们构造想要的数据结构
BoltDB GitHub 上的 README 是这么说的:
Bolt 是一个纯键值存储的 Go 数据库,启发自 Howard Chu 的 LMDB. 它旨在为那些无须一个像 Postgres 和 MySQL 这样有着完整数据库服务器的项目,提供一个简单,快速和可靠的数据库。
由于 Bolt 意在用于提供一些底层功能,简洁便成为其关键所在。它的
API 并不多,并且仅关注值的获取和设置。仅此而已。


听起来跟我们的需求完美契合!来快速过一下:
Bolt 使用键值存储,这意味着它没有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,没有行和列。相反,数据被存储为键值对(key-value pair,就像 Golang 的 map)。键值对被存储在 bucket 中,这是为了将相似的键值对进行分组(类似 RDBMS 中的表格)。因此,为了获取一个值,你需要知道一个 bucket 和一个键(key)。
需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储(块)Block),我们需要对它们进行序列化,也就说,实现一个从 Go struct 转换到一个 byte array 的机制,同时还可以从一个 byte array 再转换回 Go struct。虽然我们将会使用  encoding/gob  来完成这一目标,但实际上也可以选择使用 JSON, XML, Protocol Buffers 等等。之所以选择使用 encoding/gob, 是因为它很简单,而且是 Go 标准库的一部分。
    []数据库结构[/]

在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:
简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:
    1、其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
    2、另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据
此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。
在 blocks 中,key -> value 为:


1.png


在 chainstate,key -> value 为:

2.png


详情可见 这里。
因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
    1、32 字节的 block-hash -> block 结构
    2、l -> 链中最后一个块的 hash
这就是实现持久化机制所有需要了解的内容了。
    []序列化[/]

上面提到,在 BoltDB 中,值只能是 []byte 类型,但是我们想要存储 Block 结构。所以,我们需要使用 encoding/gob 来对这些结构进行序列化。
让我们来实现 Block 的 Serialize 方法(为了简洁起见,此处略去了错误处理):

func (b *Block) Serialize() []byte {
     var result bytes.Buffer
    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(b)

    return result.Bytes()
}


这个部分比较直观:首先,我们定义一个 buffer 存储序列化之后的数据。然后,我们初始化一个 gob encoder 并对 block 进行编码,结果作为一个字节数组返回。
接下来,我们需要一个解序列化的函数,它会接受一个字节数组作为输入,并返回一个 Block. 它不是一个方法(method),而是一个单独的函数(function):

func DeserializeBlock(d []byte) *Block {
    var block Block decoder := gob.NewDecoder(bytes.NewReader(d))

    err := decoder.Decode(&block)
    return &block
}


这就是序列化部分的内容了。
    []持久化[/]

让我们从 NewBlockchain 函数开始。在之前的实现中,它会创建一个新的
Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:
    1、打开一个数据库文件
    2、检查文件里面是否已经存储了一个区块链
    3、如果已经存储了一个区块链:
        1、创建一个新的 Blockchain 实例
        2、设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
    4、如果没有区块链:
        1、创建创世块
        2、存储到数据库
        3、将创世块哈希保存为最后一个块的哈希
        4、创建一个新的 Blockchain 实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)
代码大概是这样:

func NewBlockchain() *Blockchain {
    var tip []byte
    db, err := bolt.Open(dbFile, 0600, nil)

    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))

        if b == nil {
            genesis := NewGenesisBlock()
            b, err := tx.CreateBucket([]byte(blocksBucket))
            err = b.Put(genesis.Hash, genesis.Serialize())
            err = b.Put([]byte("l"), genesis.Hash)
            tip = genesis.Hash
        } else {
            tip = b.Get([]byte("l"))
        }

        return nil

    })
    bc := Blockchain{tip, db}

    return &bc
}


来一段一段地看下代码:

db, err := bolt.Open(dbFile, 0600, nil)

这是打开一个 BoltDB 文件的标准做法。注意,即使不存在这样的文件,它也不会返回错误。

err = db.Update(func(tx *bolt.Tx) error {
 ...
 })


在 BoltDB 中,数据库操作通过一个事务(transaction)进行操作。有两种类型的事务:只读(read-only)和读写(read-write)。这里,打开的是一个读写事务(db.Update(...)),因为我们可能会向数据库中添加创世块。

b := tx.Bucket([]byte(blocksBucket))

if b == nil {
    genesis := NewGenesisBlock()
    b, err := tx.CreateBucket([]byte(blocksBucket))
    err = b.Put(genesis.Hash, genesis.Serialize())
    err = b.Put([]byte("l"), genesis.Hash)
    tip = genesis.Hash
} else {
    tip = b.Get([]byte("l"))
}


这里是函数的核心。在这里,我们先获取了存储区块的 bucket:如果存在,就从中读取 l 键;如果不存在,就生成创世块,创建 bucket,并将区块保存到里面,然后更新 l 键以存储链中最后一个块的哈希。
另外,注意创建 Blockchain 一个新的方式:

bc := Blockchain{tip, db}

这次,我们不在里面存储所有的区块了,而是仅存储区块链的 tip。另外,我们存储了一个数据库连接。因为我们想要一旦打开它的话,就让它一直运行,直到程序运行结束。因此,Blockchain 的结构现在看起来是这样:

type Blockchain struct {
    tip []byte
    db *bolt.DB
}


接下来我们想要更新的是 AddBlock 方法:现在向链中加入区块,就不是像之前向一个数组中加入一个元素那么简单了。从现在开始,我们会将区块存储在数据库里面:
func (bc *Blockchain) AddBlock(data string) {
    var lastHash []byte
 
    err := bc.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        lastHash = b.Get([]byte("l"))

        return nil
    })

    newBlock := NewBlock(data, lastHash)

    err = bc.db.Update(func(tx *bolt.Tx) 、error {
        b := tx.Bucket([]byte(blocksBucket))
        err := b.Put(newBlock.Hash, newBlock.Serialize())
        err = b.Put([]byte("l"), newBlock.Hash)
        bc.tip = newBlock.Hash

        return nil
    })
}


继续来一段一段分解开来:

err := bc.db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte(blocksBucket))
    lastHash = b.Get([]byte("l"))

    return nil
})


这是 BoltDB 事务的另一个类型(只读)。在这里,我们会从数据库中获取最后一个块的哈希,然后用它来挖出一个新的块的哈希:

newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash

    []检查区块链[/]

现在,产生的所有块都会被保存到一个数据库里面,所以我们可以重新打开一个链,然后向里面加入新块。但是在实现这一点后,我们失去了之前一个非常好的特性:我们再也无法打印区块链的区块了,因为现在不是将区块存储在一个数组,而是放到了数据库里面。让我们来解决这个问题!
BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储,而且我们想要以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。故而,我们需要一个区块链迭代器(BlockchainIterator):

type BlockchainIterator struct {
    currentHash []byte
    db            *bolt.DB
}


每当要对链中的块进行迭代时,我们就会创建一个迭代器,里面存储了当前迭代的块哈希(currentHash)和数据库的连接(db)。通过 db,迭代器逻辑上被附属到一个区块链上(这里的区块链指的是存储了一个数据库连接的 Blockchain 实例),并且通过 Blockchain 方法进行创建:

func (bc Blockchain) Iterator() BlockchainIterator {
    bci := &BlockchainIterator{bc.tip, bc.db}

    return bci
}


注意,迭代器的初始状态为链中的 tip,因此区块将从头到尾,也就是从最新的到最旧的进行获取。实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。
BlockchainIterator 只会做一件事情:返回链中的下一个块。

func (i BlockchainIterator) Next() Block {
    var block *Block

    err := i.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        encodedBlock := b.Get(i.currentHash)
        block = DeserializeBlock(encodedBlock)

        return nil

    })
    i.currentHash = block.PrevBlockHash

    return block
}


这就是数据库部分的内容了!
    []CLI[/]

到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在 main 函数中简单执行了 NewBlockchain 和 bc.AddBlock 。是时候改变了!现在我们想要拥有这些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain


所有命令行相关的操作都会通过 CLI 结构进行处理:

type CLI struct {
    bc *Blockchain
}


它的 “入口” 是 Run 函数:
func (cli *CLI) Run() {
    cli.validateArgs()

    addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
    printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)

    addBlockData := addBlockCmd.String("data", "", "Block data")

    switch os.Args[1] {
    case "addblock":
        err := addBlockCmd.Parse(os.Args[2:])
    case "printchain":
        err := printChainCmd.Parse(os.Args[2:])
    default:
        cli.printUsage()
        os.Exit(1)
    }

    if addBlockCmd.Parsed() {
        if *addBlockData == "" {
            addBlockCmd.Usage()
            os.Exit(1)
        }
        cli.addBlock(*addBlockData)
    }

    if printChainCmd.Parsed() {
        cli.printChain()
    }
}


我们会使用标准库里面的 flag 包来解析命令行参数:

addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")


首先,我们创建两个子命令: addblock 和 printchain, 然后给 addblock 添加 -data 标志。printchain 没有任何标志。

switch os.Args[1] {
case "addblock":
    err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
    err := printChainCmd.Parse(os.Args[2:])
default:
    cli.printUsage()
    os.Exit(1)
}


然后,我们检查用户提供的命令,解析相关的 flag 子命令:

if addBlockCmd.Parsed() {
    if *addBlockData == "" {
        addBlockCmd.Usage()
        os.Exit(1)
    }
    cli.addBlock(*addBlockData)
}

 if printChainCmd.Parsed() {
    cli.printChain()
}


接着检查解析是哪一个子命令,并调用相关函数:

func (cli *CLI) addBlock(data string) {
    cli.bc.AddBlock(data)
    fmt.Println("Success!")
}

func (cli *CLI) printChain() {
    bci := cli.bc.Iterator()

    for {
        block := bci.Next()

        fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        pow := NewProofOfWork(block)
        fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
        fmt.Println()
    if len(block.PrevBlockHash) == 0 {
        break
        }
    }
}


这部分内容跟之前的很像,唯一的区别是我们现在使用的是 BlockchainIterator 对区块链中的区块进行迭代:
记得不要忘了对 main 函数作出相应的修改:

func main() {
    bc := NewBlockchain()
    defer bc.db.Close()
   
    cli := CLI{bc}
    cli.Run()
}


注意,无论提供什么命令行参数,都会创建一个新的链。
这就是今天的所有内容了! 来看一下是不是如期工作:

$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block" 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan" 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13

Success!

$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee" 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148

Success!

$ blockchain_go printchain Prev.
hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true

Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true



3.png

    []总结[/]

在下篇文章中,我们将会实现地址,钱包,(可能实现)交易。尽请收听!

apfree wifidog如何挖wificoin?

开源讨论张增飞 回复了问题 • 4 人关注 • 2 个回复 • 345 次浏览 • 2018-03-19 11:11 • 来自相关话题

让你看懂区块链的小故事

区块链技术区块链徐生 发表了文章 • 0 个评论 • 151 次浏览 • 2018-03-19 10:14 • 来自相关话题

区块链技术对于没有技术背景的普通人太难理解,不如来听个能看懂区块链的故事:

点击【了解更多】获得更多区块链金融方面的产品经理咨询

每天更新一篇区块链小故事【汇新云徐生】

有个封闭的山村,村民的主要工作就是挖玉石,村里的财富也是以玉石来计算。大家挖到的玉石堆放到一起,由村长清点记账。张三、李四、王五各自的财富,都记录在村长的账本上,他们可以依此去换取其他生活用品。

这种方式就是目前的中央记账式金融体系。银行、券商、支付宝等金融机构就是“村长”。我们的流动资产,几乎都在他们的账本里。

但村长是个凡人,老懵懂、帕金森、咸猪手......各种毛病一样不少。记账时,看到貌美农妇,多记两笔;遇到刺头 ,则少记一块。然后账本保管,也经常出问题,有的地方受潮模糊,还有的地方被老鼠啃掉。更过份的是,小寡妇赵六和村长鬼混的时候,账本还被她涂改了几笔。

村民决定将不干活、白吃饭、常揩油、还老出错的村长废除掉。那谁来记账呢?




村民想了一个办法:每个人都带一本账本,谁挖到玉石时,自己记录的同时通知所有人,大家都在各自的本子写下同样的内容,账本都分别由村民们保管。以后村民之间的物品换取、交换玉石,也通过这个方式记账。这样既节省了一个记账的劳力,还避免了账本受潮等问题。即使小寡妇涂改了张三的账本,但大家拿李四、王五、孙七的账本出来对比,就能马上发现问题并更正。只要村民数量足够多(还有女人),即使小寡妇性能再强大,也无法纂改过半人的账本。这就是去中心化的分布式记账。

问题来了,李四突然灵光一闪,“我把已经记录的玉石,拿来再记一次,财富岂不就很快翻倍再翻倍了?” 村民为了解决李四的弄虚作假,想了一个办法。给每块玉石都做标记,记录挖掘到的时间、地点和人物,以及上一块被挖到的玉石的信息。

因此,每个村民的账本,都记录了每块玉的完整信息。每块玉都与上一块玉有信息关联,形成一个链条。李四既无法凭空捏造,也无法更改之前的记录。

这就是区块链——P2P分布式记账。

上面的案例中,分布式记账,每挖到玉石,村民都需要停工,将账目更新一次,反而降低了生产效率。但在互联网的计算机里,这都是一瞬间的事情。

区块链不一定需要挖玉石这个环节,玉石(比特币、或其他东西)可以按各种规则产生。区块链不等于比特币,比特币是基于区块链技术的一种应用。

区块链的作用主要是用于记录这些物品(虚拟货币)的来龙去脉。按目前区块链行业的主流观点,相对于传统中央式记账,分布式的区块链安全、准确、高效、公开。它不需要牺牲巨大的摩擦成本来养活庞大的金融机构(村长),而且由于是P2P方式的去中心化存储,无需担心诸如金融机构的数据库被黑客入侵、机房遭受自然灾害、政变等问题。区块链是传统金融机构的颠覆者。
 
故事看完了,想获得更多区块链金融方面的知识点击【了解更多】获得更多区块链金融方面的产品经理咨询
  查看全部
区块链技术对于没有技术背景的普通人太难理解,不如来听个能看懂区块链的故事:

点击【了解更多】获得更多区块链金融方面的产品经理咨询

每天更新一篇区块链小故事【汇新云徐生】

有个封闭的山村,村民的主要工作就是挖玉石,村里的财富也是以玉石来计算。大家挖到的玉石堆放到一起,由村长清点记账。张三、李四、王五各自的财富,都记录在村长的账本上,他们可以依此去换取其他生活用品。

这种方式就是目前的中央记账式金融体系。银行、券商、支付宝等金融机构就是“村长”。我们的流动资产,几乎都在他们的账本里。

但村长是个凡人,老懵懂、帕金森、咸猪手......各种毛病一样不少。记账时,看到貌美农妇,多记两笔;遇到刺头 ,则少记一块。然后账本保管,也经常出问题,有的地方受潮模糊,还有的地方被老鼠啃掉。更过份的是,小寡妇赵六和村长鬼混的时候,账本还被她涂改了几笔。

村民决定将不干活、白吃饭、常揩油、还老出错的村长废除掉。那谁来记账呢?




村民想了一个办法:每个人都带一本账本,谁挖到玉石时,自己记录的同时通知所有人,大家都在各自的本子写下同样的内容,账本都分别由村民们保管。以后村民之间的物品换取、交换玉石,也通过这个方式记账。这样既节省了一个记账的劳力,还避免了账本受潮等问题。即使小寡妇涂改了张三的账本,但大家拿李四、王五、孙七的账本出来对比,就能马上发现问题并更正。只要村民数量足够多(还有女人),即使小寡妇性能再强大,也无法纂改过半人的账本。这就是去中心化的分布式记账。

问题来了,李四突然灵光一闪,“我把已经记录的玉石,拿来再记一次,财富岂不就很快翻倍再翻倍了?” 村民为了解决李四的弄虚作假,想了一个办法。给每块玉石都做标记,记录挖掘到的时间、地点和人物,以及上一块被挖到的玉石的信息。

因此,每个村民的账本,都记录了每块玉的完整信息。每块玉都与上一块玉有信息关联,形成一个链条。李四既无法凭空捏造,也无法更改之前的记录。

这就是区块链——P2P分布式记账。

上面的案例中,分布式记账,每挖到玉石,村民都需要停工,将账目更新一次,反而降低了生产效率。但在互联网的计算机里,这都是一瞬间的事情。

区块链不一定需要挖玉石这个环节,玉石(比特币、或其他东西)可以按各种规则产生。区块链不等于比特币,比特币是基于区块链技术的一种应用。

区块链的作用主要是用于记录这些物品(虚拟货币)的来龙去脉。按目前区块链行业的主流观点,相对于传统中央式记账,分布式的区块链安全、准确、高效、公开。它不需要牺牲巨大的摩擦成本来养活庞大的金融机构(村长),而且由于是P2P方式的去中心化存储,无需担心诸如金融机构的数据库被黑客入侵、机房遭受自然灾害、政变等问题。区块链是传统金融机构的颠覆者。
 
故事看完了,想获得更多区块链金融方面的知识点击【了解更多】获得更多区块链金融方面的产品经理咨询
 

让你看懂区块链的一则小故事

区块链技术区块链徐生 回复了问题 • 3 人关注 • 4 个回复 • 261 次浏览 • 2018-03-19 10:10 • 来自相关话题

用 Go 构建一个区块链 -- Part 2: 工作量证明

区块链技术guazi 发表了文章 • 0 个评论 • 141 次浏览 • 2018-03-17 17:43 • 来自相关话题

在 前面一文 中,我们构造了一个非常简单的数据结构,这个数据结构也是整个区块链数据库的核心。目前所完成的区块链原型,已经可以通过链式关系把区块相互关联起来:每个块都被连接到前一个块。

但是,我们实现的区块链有一个巨大的缺点:向链中加入区块太容易和廉价了。而区块链和比特币的其中一个核心就是,要想加入新的区块,必须先完成一些非常困难的工作。在本文,我们将会解决这个缺点。
[]工作量证明[/]
区块链的一个关键点就是,一个人必须经过一系列困难的工作,才能将数据放入到区块链中。正是这种困难的工作,才使得区块链是安全和一致的。此外,完成这个工作的人也会获得奖励(这也就是通过挖矿获得币)。

这个机制与生活的一个现象非常类似:一个人必须通过努力工作,才能够获得回报或者奖励,用以支撑他们的生活。在区块链中,是通过网络中的参与者(矿工)不断的工作来支撑整个网络,也就是矿工不断地向区块链中加入新块,然后获得相应的奖励。作为他们努力工作的结果,新生成的区块就能够被安全地被加入到区块链中,这种机制维护了整个区块链数据库的稳定性。值得注意的是,完成了这个工作的人必须要证明这一点,他必须要证明确实是他完成了这些工作。

整个 “努力工作并进行证明” 的机制,就叫做工作量证明(proof-of-work)。要想完成工作非常地不容易,因为这需要大量的计算能力:即便是高性能计算机,也无法在短时间内快速完成。此外,这个工作的困难度会随着时间不断增长,以保持每个小时大概出 6 个新块的速度。在比特币中,这个工作的目的是为了找到一个块的哈希,同时这个哈希满足了一些必要条件。这个哈希,也就充当了证明的角色。因此,寻求证明(寻找有效哈希),就是实际要做的事情。
[]哈希计算[/]
在本节中,我们会讨论哈希计算。如果你已经熟悉了这个概念,可以跳过这一节。

获得指定数据的一个哈希值的过程,就叫做哈希计算。一个哈希,就是对所计算数据的一个唯一的表示。一个哈希函数输入任意大小的数据,输出一个固定大小的哈希值。下面是哈希的几个关键特性:

1、无法从一个哈希值恢复原始数据。也就是说,哈希并不是加密。
2、对于特定的数据,只能有一个哈希,并且这个哈希是唯一的。
3、即使是仅仅改变输入数据中的一个字节,也会导致输出一个完全不同的哈希。






哈希函数被广泛用于检测数据的一致性。一些软件提供者除了提供软件包以外,还会发布校验和。当下载完一个文件以后,你可以用哈希函数对下载好的文件计算一个哈希,并与作者提供的哈希进行比较,以此来保证文件下载的完整性。

在区块链中,哈希被用于保证一个块的一致性。哈希算法的输入数据包含了前一个块的哈希,因此使得不太可能(或者,至少很困难)去修改链中的一个块:因为如果一个人想要修改前面一个块的哈希,那么他必须要重新计算这个块以及后面所有块的哈希。
[]Hashcash[/]
比特币使用 Hashcash ,一个最初用来防止垃圾邮件的工作量证明算法。它可以被分解为以下步骤:

1、取一些公开的数据(比如,如果是 email 的话,它可以是接收者的邮件地址;在比特币中,它是区块头)
2、给这个公开数据添加一个计数器。计数器默认从 0 开始
3、将 data(数据) 和 counter(计数器) 组合到一起,获得一个哈希
4、检查哈希是否符合一定的条件:
    1)如果符合条件,结束
    2)如果不符合,增加计数器,重复步骤 3-4

因此,这是一个暴力算法:改变计数器,计算一个新的哈希,检查,增加计数器,计算一个哈希,检查,如此反复。这也是为什么说它是在计算上是非常昂贵的,因为这一步需要如此反复不断地计算和检查。

现在,让我们来仔细看一下一个哈希要满足的必要条件。在原始的 Hashcash 实现中,它的要求是 “一个哈希的前 20 位必须是 0”。在比特币中,这个要求会随着时间而不断变化。因为按照设计,必须保证每 10 分钟生成一个块,而不论计算能力会随着时间增长,或者是会有越来越多的矿工进入网络,所以需要动态调整这个必要条件。

为了阐释这一算法,我从前一个例子(“I like donuts”)中取得数据,并且找到了一个前 3 个字节是全是 0 的哈希。






ca07ca 是计数器的 16 进制值,十进制的话是 13240266.
[]实现[/]

好了,完成了理论层面,来开始写代码吧!首先,定义挖矿的难度值:

const targetBits = 24

在比特币中,当一个块被挖出来以后,“target bits” 代表了区块头里存储的难度。这里的 24 指的是算出来的哈希前 24 位必须是 0,用 16 进制表示化的话,就是前 6 位必须是 0,这一点可以在最后的输出可以看出来。目前不会实现一个动态调整目标的算法,所以将难度定义为一个全局的常量即可。

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 结构,里面存储了指向一个块和一个目标的指针。“目标” ,也就是前一节中所描述的必要条件。这里使用了一个 大 整数,我们将哈希与目标进行比较:先把一个哈希转换成一个大整数,然后检测它是否小于目标。

在 NewProofOfWork 函数中,我们将 big.Int 初始化为 1,然后左移 256 - targetBits 位。256 是一个 SHA-256 哈希的位数,我们将要使用的是 SHA-256 哈希算法。target(目标) 的 16 进制形式为:


0x10000000000000000000000000000000000000000000000000000000000

它在内存上占据了 29 个字节。下面是与前面例子哈希的形式化比较:


0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3 0000010000000000000000000000000000000000000000000000000000000000 0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一个哈希(基于 “I like donuts” 计算)比目标要大,因此它并不是一个有效的工作量证明。第二个哈希(基于 “I like donutsca07ca” 计算)比目标要小,所以是一个有效的证明。

译者注:评论有人提出上面的形式化比较有些“言不符实”,其实它应该并非由 “I like donuts” 而来,但是原文作者表达的意思是没问题的,可能是疏忽而已。下面是我做的一个小实验:


package main

import (
    "crypto/sha256"
    "fmt"
    "math/big"
)

 func main() {
    data1 := []byte("I like donuts")
    data2 := []byte("I like donutsca07ca")
    targetBits := 24
    target := big.NewInt(1)
    target.Lsh(target, uint(256-targetBits))
    fmt.Printf("%x\n", sha256.Sum256(data1))
    fmt.Printf("%64x\n", target)
    fmt.Printf("%x\n", sha256.Sum256(data2))
}

输出:






你可以把目标想象为一个范围的上界:如果一个数(由哈希转换而来)比上界要小,那么这是有效的,反之无效。因为要求比上界要小,所以会导致更少的有效数字。因此,也就需要通过困难的工作(一系列反复的计算),才能找到一个有效的数字。

现在,我们需要有数据来进行哈希,准备数据:

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
}

这个部分比较直观:只需要将 target ,nonce 与 Block 进行合并。这里的 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)
        hashInt.SetBytes(hash[:])
        if hashInt.Cmp(pow.target) == -1 {
            fmt.Printf("\r%x", hash)
            break
        } else {
            nonce++
        }
    }

    fmt.Print("\n\n")
   
    return nonce, hash[:]
}

首先我们对变量进行初始化:
[]HashInt 是 hash 的整形表示;[/][]nonce 是计数器。[/]

然后开始一个 “无限” 循环:maxNonce 对这个循环进行了限制, 它等于 math.MaxInt64。这是为了避免 nonce 可能出现的溢出。尽管我们的 PoW 实现的难度太小了,以至于计数器其实不太可能会溢出,但最好还是以防万一检查一下。

在这个循环中,我们做的事情有:

1、准备数据
2、用 SHA-256 对数据进行哈希
3、将哈希转换成一个大整数
4、将这个大整数与目标进行比较

跟之前所讲的一样简单。现在我们可以移除 Block 的 SetHash 方法,然后修改 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

成功了!你可以看到每个哈希都是 3 个字节的 0 开始,并且获得这些哈希需要花费一些时间。

还剩下一件事情需要做,对工作量证明进行验证:


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

从下图可以看出,这次我们产生三个块花费了一分多钟,比没有工作量证明之前慢了很多(也就是成本高了很多):





[]总结[/]
我们的区块链离真正的区块链又进了一步:现在需要经过一些困难的工作才能加入新的块,因此挖矿就有可能了。但是,它还缺少一些至关重要的特性:区块链数据库并不是持久化的,没有钱包,地址,交易,也没有共识机制。不过,所有的这些,我们都会在接下来的文章中实现,现在,愉快地挖矿吧!

  查看全部
在 前面一文 中,我们构造了一个非常简单的数据结构,这个数据结构也是整个区块链数据库的核心。目前所完成的区块链原型,已经可以通过链式关系把区块相互关联起来:每个块都被连接到前一个块。

但是,我们实现的区块链有一个巨大的缺点:向链中加入区块太容易和廉价了。而区块链和比特币的其中一个核心就是,要想加入新的区块,必须先完成一些非常困难的工作。在本文,我们将会解决这个缺点。
    []工作量证明[/]

区块链的一个关键点就是,一个人必须经过一系列困难的工作,才能将数据放入到区块链中。正是这种困难的工作,才使得区块链是安全和一致的。此外,完成这个工作的人也会获得奖励(这也就是通过挖矿获得币)。

这个机制与生活的一个现象非常类似:一个人必须通过努力工作,才能够获得回报或者奖励,用以支撑他们的生活。在区块链中,是通过网络中的参与者(矿工)不断的工作来支撑整个网络,也就是矿工不断地向区块链中加入新块,然后获得相应的奖励。作为他们努力工作的结果,新生成的区块就能够被安全地被加入到区块链中,这种机制维护了整个区块链数据库的稳定性。值得注意的是,完成了这个工作的人必须要证明这一点,他必须要证明确实是他完成了这些工作。

整个 “努力工作并进行证明” 的机制,就叫做工作量证明(proof-of-work)。要想完成工作非常地不容易,因为这需要大量的计算能力:即便是高性能计算机,也无法在短时间内快速完成。此外,这个工作的困难度会随着时间不断增长,以保持每个小时大概出 6 个新块的速度。在比特币中,这个工作的目的是为了找到一个块的哈希,同时这个哈希满足了一些必要条件。这个哈希,也就充当了证明的角色。因此,寻求证明(寻找有效哈希),就是实际要做的事情。
    []哈希计算[/]

在本节中,我们会讨论哈希计算。如果你已经熟悉了这个概念,可以跳过这一节。

获得指定数据的一个哈希值的过程,就叫做哈希计算。一个哈希,就是对所计算数据的一个唯一的表示。一个哈希函数输入任意大小的数据,输出一个固定大小的哈希值。下面是哈希的几个关键特性:

1、无法从一个哈希值恢复原始数据。也就是说,哈希并不是加密。
2、对于特定的数据,只能有一个哈希,并且这个哈希是唯一的。
3、即使是仅仅改变输入数据中的一个字节,也会导致输出一个完全不同的哈希。

2.png


哈希函数被广泛用于检测数据的一致性。一些软件提供者除了提供软件包以外,还会发布校验和。当下载完一个文件以后,你可以用哈希函数对下载好的文件计算一个哈希,并与作者提供的哈希进行比较,以此来保证文件下载的完整性。

在区块链中,哈希被用于保证一个块的一致性。哈希算法的输入数据包含了前一个块的哈希,因此使得不太可能(或者,至少很困难)去修改链中的一个块:因为如果一个人想要修改前面一个块的哈希,那么他必须要重新计算这个块以及后面所有块的哈希。
    []Hashcash[/]

比特币使用 Hashcash ,一个最初用来防止垃圾邮件的工作量证明算法。它可以被分解为以下步骤:

1、取一些公开的数据(比如,如果是 email 的话,它可以是接收者的邮件地址;在比特币中,它是区块头)
2、给这个公开数据添加一个计数器。计数器默认从 0 开始
3、将 data(数据) 和 counter(计数器) 组合到一起,获得一个哈希
4、检查哈希是否符合一定的条件:
    1)如果符合条件,结束
    2)如果不符合,增加计数器,重复步骤 3-4

因此,这是一个暴力算法:改变计数器,计算一个新的哈希,检查,增加计数器,计算一个哈希,检查,如此反复。这也是为什么说它是在计算上是非常昂贵的,因为这一步需要如此反复不断地计算和检查。

现在,让我们来仔细看一下一个哈希要满足的必要条件。在原始的 Hashcash 实现中,它的要求是 “一个哈希的前 20 位必须是 0”。在比特币中,这个要求会随着时间而不断变化。因为按照设计,必须保证每 10 分钟生成一个块,而不论计算能力会随着时间增长,或者是会有越来越多的矿工进入网络,所以需要动态调整这个必要条件。

为了阐释这一算法,我从前一个例子(“I like donuts”)中取得数据,并且找到了一个前 3 个字节是全是 0 的哈希。

3.png


ca07ca 是计数器的 16 进制值,十进制的话是 13240266.
    []实现[/]


好了,完成了理论层面,来开始写代码吧!首先,定义挖矿的难度值:

const targetBits = 24

在比特币中,当一个块被挖出来以后,“target bits” 代表了区块头里存储的难度。这里的 24 指的是算出来的哈希前 24 位必须是 0,用 16 进制表示化的话,就是前 6 位必须是 0,这一点可以在最后的输出可以看出来。目前不会实现一个动态调整目标的算法,所以将难度定义为一个全局的常量即可。

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 结构,里面存储了指向一个块和一个目标的指针。“目标” ,也就是前一节中所描述的必要条件。这里使用了一个 大 整数,我们将哈希与目标进行比较:先把一个哈希转换成一个大整数,然后检测它是否小于目标。

在 NewProofOfWork 函数中,我们将 big.Int 初始化为 1,然后左移 256 - targetBits 位。256 是一个 SHA-256 哈希的位数,我们将要使用的是 SHA-256 哈希算法。target(目标) 的 16 进制形式为:


0x10000000000000000000000000000000000000000000000000000000000

它在内存上占据了 29 个字节。下面是与前面例子哈希的形式化比较:


0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3 0000010000000000000000000000000000000000000000000000000000000000 0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一个哈希(基于 “I like donuts” 计算)比目标要大,因此它并不是一个有效的工作量证明。第二个哈希(基于 “I like donutsca07ca” 计算)比目标要小,所以是一个有效的证明。

译者注:评论有人提出上面的形式化比较有些“言不符实”,其实它应该并非由 “I like donuts” 而来,但是原文作者表达的意思是没问题的,可能是疏忽而已。下面是我做的一个小实验:


package main

import (
    "crypto/sha256"
    "fmt"
    "math/big"
)

 func main() {
    data1 := []byte("I like donuts")
    data2 := []byte("I like donutsca07ca")
    targetBits := 24
    target := big.NewInt(1)
    target.Lsh(target, uint(256-targetBits))
    fmt.Printf("%x\n", sha256.Sum256(data1))
    fmt.Printf("%64x\n", target)
    fmt.Printf("%x\n", sha256.Sum256(data2))
}


输出:

4.png


你可以把目标想象为一个范围的上界:如果一个数(由哈希转换而来)比上界要小,那么这是有效的,反之无效。因为要求比上界要小,所以会导致更少的有效数字。因此,也就需要通过困难的工作(一系列反复的计算),才能找到一个有效的数字。

现在,我们需要有数据来进行哈希,准备数据:

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
}


这个部分比较直观:只需要将 target ,nonce 与 Block 进行合并。这里的 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)
        hashInt.SetBytes(hash[:])
        if hashInt.Cmp(pow.target) == -1 {
            fmt.Printf("\r%x", hash)
            break
        } else {
            nonce++
        }
    }

    fmt.Print("\n\n")
   
    return nonce, hash[:]
}


首先我们对变量进行初始化:
    []HashInt hash 的整形表示;[/][]nonce 是计数器。[/]


然后开始一个 “无限” 循环:maxNonce 对这个循环进行了限制, 它等于 math.MaxInt64。这是为了避免 nonce 可能出现的溢出。尽管我们的 PoW 实现的难度太小了,以至于计数器其实不太可能会溢出,但最好还是以防万一检查一下。

在这个循环中,我们做的事情有:

1、准备数据
2、用 SHA-256 对数据进行哈希
3、将哈希转换成一个大整数
4、将这个大整数与目标进行比较

跟之前所讲的一样简单。现在我们可以移除 Block 的 SetHash 方法,然后修改 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


成功了!你可以看到每个哈希都是 3 个字节的 0 开始,并且获得这些哈希需要花费一些时间。

还剩下一件事情需要做,对工作量证明进行验证:


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


从下图可以看出,这次我们产生三个块花费了一分多钟,比没有工作量证明之前慢了很多(也就是成本高了很多):

5.png

    []总结[/]

我们的区块链离真正的区块链又进了一步:现在需要经过一些困难的工作才能加入新的块,因此挖矿就有可能了。但是,它还缺少一些至关重要的特性:区块链数据库并不是持久化的,没有钱包,地址,交易,也没有共识机制。不过,所有的这些,我们都会在接下来的文章中实现,现在,愉快地挖矿吧!

 

CryptoNight Hash Function

区块链技术lupenglogo 发表了文章 • 2 个评论 • 102 次浏览 • 2018-03-17 17:25 • 来自相关话题

摘要

本文档是CryptoNote标准的一部分,对等匿名支付系统。 它定义了CryptoNote的默认值验证工作量证明哈希函数:CryptoNight。

[]介绍[/]
CryptoNight是一个内存难解哈希函数。它的设计是在GPU,FPGA和ASIC架构上无效可计算。该CryptoNight算法的第一步是初始化大型暂存器与伪随机数据。下一步是大量的读/写在暂存器中包含伪随机地址的操作。最后一步是将整个暂存器的哈希值进行哈希处理产生的值。

[]定义[/]
哈希函数(hash function):映射数据的有效计算函数对于固定大小的数据,任意大小和行为类似于a随机函数

暂存器(scratchpad):用于存储中间值的大面积内存在记忆硬功能的评估过程中

[]便条板初始化[/]
首先,使用参数b =的Keccak [KECCAK]对输入进行散列1600和c = 512.Keccak最终状态的字节0..31解释为AES-256密钥[AES],并扩展为10个循环密钥。一个分配了2097152字节(2 MiB)的暂存器。字节64..191从Keccak最终状态中提取出来并分成8个块每个16个字节。使用以下步骤对每个块进行加密:for i = 0..9 do: block = aes_round(block, round_keys[i])其中aes_round功能执行一轮AES加密,哪些意味着执行SubBytes,ShiftRows和MixColumns步骤块,结果与圆键进行异或运算。注意不同于AES加密算法,第一轮和最后一轮不是特别的得到的块被写入前128个字节的暂存器。然后,这些块再次加密相同的方式,结果写入第二个128个字节便签本每次写入128个字节时,它们代表对先前写入的128字节进行加密的结果。该重复进程,直到暂存器完全初始化为止。

该图表示暂存器初始化:+-----+ |Input| +-----+ | V +--------+ | Keccak | +--------+ | V +-------------------------------------------------------------+ | Final state | +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | | V | +-------------+ V | Round key 0 |------------+---+->+-----+ +-------------+ | | | | | . | | | | | | . | | | | AES | | . | | | | | +-------------+ | | | | | Round key 9 |----------+-|-+-|->+-----+ +---+ +-------------+ | | | | | | | | | | | +------------------->| | | | | | | | | | | | | V | | | | | +->+-----+ | | | | | | | | S | | | | | | | | | | | | AES | | c | | | | | | | | | | | | | | r | | | +--->+-----+ | | | | | | a | | | +------------------->| | | | . | t | | | . | | | | . | c | | | +------------------->| | | | | | h | | | V | | | +----->+-----+ | p | | | | | | | | | | a | | | AES | | | | | | | d | | | | | | +------->+-----+ | | | | | +------------------->| | | | +---+ Figure 3: Scratchpad initialization diagram4. 内存难解循环

在主循环之前,Keccak状态的字节0..31和32..63被异或,所得到的32个字节用于初始化变量a和b,每个16个字节。这些变量用于主循环。主循环迭代524,288次。当一个16字节值需要转换成暂存器中的一个地址解释为小端数整数,21位低位用作字节索引。但是,索引的4个低位是清除以确保16字节对齐。从...读取数据以16字节的块写入暂存器。每次迭代都可以用以下伪代码表示:scratchpad_address = to_scratchpad_address(a) scratchpad[scratchpad_address] = aes_round(scratchpad [scratchpad_address], a) b, scratchpad[scratchpad_address] = scratchpad[scratchpad_address], b xor scratchpad[scratchpad_address] scratchpad_address = to_scratchpad_address(b) a = 8byte_add(a, 8byte_mul(b, scratchpad[scratchpad_address])) a, scratchpad[scratchpad_address] = a xor scratchpad[scratchpad_address], a其中,8byte_add函数将每个参数表示为a一对64位小端值,并将它们添加在一起,分数,模2 ^ 64。结果转换成16字节。

然而,8byte_mul函数仅使用每个字节的前8个字节参数,被解释为无符号64位小端整数并相乘。结果转换为16字节,最后交换结果的两个8字节的一半。

该图说明了内存难解循环:+-------------------------------------------------------------+ | Final state | +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | | | +-----+ | +-->| XOR |<--+ +-----+ | | +----+ +----+ | | V V +---+ +---+ | a | | b | +---+ +---+ | | --------------------- REPEAT 524288 TIMES --------------------- | | address +---+ +-------------|----------------------------------->| | | +-----+ | read | | +-->| AES |<--|------------------------------------| | | +-----+ V | | | | +-----+ | S | | +-->| XOR | | | | | +-----+ write | c | | | | +------------------------------>| | | | +----+ address | r | | +------------------------------------------>| | | | +-----------+ read | a | | +->| 8byte_mul |<--+------------------------| | | | +-----------+ | | t | | | | | | | | | V | | c | | | +-----------+ | | | +------|->| 8byte_add | | | h | | +-----------+ | | | | | | write | p | | +---------|----------------------->| | | | | | a | | V | | | | +-----+ | | d | | | XOR |<-----+ | | | +-----+ | | +------+ | | | +-------------|-+ | | | | +---+ -------------------------- END REPEAT ------------------------- | | Figure 4: Memory-hard loop diagram5. 结果计算

在内存难解部分之后,来自Keccak状态的字节32..63以与第一个相同的方式扩展为10个AES循环密钥部分。

字节64..191从Keccak状态提取并与第一个128字节的暂存器。然后结果被加密与第一部分相同,但使用新的键。该结果与暂存器中的第二个128个字节进行异或运算,再次加密,等等。

在与暂存器的最后128个字节进行异或运算后,结果是加密最后一次,然后在Keccak中的字节64..191状态被替换为结果。然后,凯凯克州通过通过Keccak-f(Keccak排列),b = 1600。

然后,使用状态的第一个字节的2个低位选择一个散列函数:0 = BLAKE-256 [BLAKE],1 = Groestl-256 [GROESTL] 2 = JH-256 [JH],3 = Skein-256 [SKEIN]。所选的哈希函数是然后应用到Keccak状态,并且生成的哈希是输出CryptoNight。

下图说明了结果计算:+-------------------------------------------------------------+ | Final state | +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | | | | | +--------+ | | | V | | | |+-------------+ | | | || Round key 0 |-|---+---+ | | |+-------------+ | | | | | || . | | | | | | || . | | | | | | || . | | | | | | |+-------------+ | | | | | +---+ || Round key 9 |-|-+-|-+ | V | | | |+-------------+ | | | | | +-----+ | | |-|----------------|-|-|-|-|->| XOR | | | | | | | | | | +-----+ | | S | | | | | | | | | | | | | | | | | V | | c | | | | | | +->+-----+ | | | | | | | | | | | | r | | | | | | | | | | | | | | | | | AES | | | a | | | | | | | | | | | | | | | | | | | | t | | | | | +--->+-----+ | | | | | | | | | | c | | | | | V | | | | | | | +-----+ | | h |-|----------------|-|-|----->| XOR | | | | | | | | +-----+ | | p | | | | | | | | | | | | | . | | a | | | | | . | | | | | | | . | | d | | | | | | | | | | | | | V | | | | | | | +-----+ | | |-|----------------|-|-|----->| XOR | | | | | | | | +-----+ | +---+ | | | | | | | | | | V | | | | +----->+-----+ | | | | | | | | | | | | | | | | | AES | | | | | | | | | | | | | | | | +------->+-----+ | | | | | V V V V +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | Modified state | +-------------------------------------------------------------+ | V +----------+ | Keccak-f | +----------+ | | +-----------+ | | | V V +-------------+ +-------------+ | Select hash |->| Chosen hash | +-------------+ +-------------+ | V +--------------+ | Final result | +--------------+ Figure 5: Result calculation diagram哈希示例:Empty string: eb14e8a833fac6fe9a43b57b336789c46ffe93f2868452240720607b14387e11. "This is a test": a084f01d1437a09c6985401b60d43554ae105802c5f5d8a9b3253649c0be6605.6. References

[AES] "Announcing the ADVANCED ENCRYPTION STANDARD", FIPS 197, 2001.

[BLAKE] Aumasson, J.-P., Henzen, L., Meier, W., and R. C.-W. Phan, "SHA-3 proposal BLAKE", 2010.

[GROESTL] Gauravaram, P., Knudsen, L., Matusiewicz, K., Mendel, F., Rechberger, C., Schlaffer, M., and S. Thomsen, "Groestl - a SHA-3 candidate", 2011.

[JH] Wu, H., "The Hash Function JH", 2011.

[KECCAK] Bertoni, G., Daemen, J., Peeters, M., and G. Van Assche, "The Keccak reference", 2011.

[SKEIN] Ferguson, N., Lucks, S., Schneier, B., Whiting, D., Bellare, M., Kohno, T., Callas, J., and J. Walker, "The Skein Hash Function Family", 2008. 查看全部
摘要

本文档是CryptoNote标准的一部分,对等匿名支付系统。 它定义了CryptoNote的默认值验证工作量证明哈希函数:CryptoNight。

    []介绍[/]

CryptoNight是一个内存难解哈希函数。它的设计是在GPU,FPGA和ASIC架构上无效可计算。该CryptoNight算法的第一步是初始化大型暂存器与伪随机数据。下一步是大量的读/写在暂存器中包含伪随机地址的操作。最后一步是将整个暂存器的哈希值进行哈希处理产生的值。

    []定义[/]

哈希函数(hash function):映射数据的有效计算函数对于固定大小的数据,任意大小和行为类似于a随机函数

暂存器(scratchpad):用于存储中间值的大面积内存在记忆硬功能的评估过程中

    []便条板初始化[/]

首先,使用参数b =的Keccak [KECCAK]对输入进行散列1600和c = 512.Keccak最终状态的字节0..31解释为AES-256密钥[AES],并扩展为10个循环密钥。一个分配了2097152字节(2 MiB)的暂存器。字节64..191从Keccak最终状态中提取出来并分成8个块每个16个字节。使用以下步骤对每个块进行加密:for i = 0..9 do: block = aes_round(block, round_keys[i])其中aes_round功能执行一轮AES加密,哪些意味着执行SubBytes,ShiftRows和MixColumns步骤块,结果与圆键进行异或运算。注意不同于AES加密算法,第一轮和最后一轮不是特别的得到的块被写入前128个字节的暂存器。然后,这些块再次加密相同的方式,结果写入第二个128个字节便签本每次写入128个字节时,它们代表对先前写入的128字节进行加密的结果。该重复进程,直到暂存器完全初始化为止。

该图表示暂存器初始化:+-----+ |Input| +-----+ | V +--------+ | Keccak | +--------+ | V +-------------------------------------------------------------+ | Final state | +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | | V | +-------------+ V | Round key 0 |------------+---+->+-----+ +-------------+ | | | | | . | | | | | | . | | | | AES | | . | | | | | +-------------+ | | | | | Round key 9 |----------+-|-+-|->+-----+ +---+ +-------------+ | | | | | | | | | | | +------------------->| | | | | | | | | | | | | V | | | | | +->+-----+ | | | | | | | | S | | | | | | | | | | | | AES | | c | | | | | | | | | | | | | | r | | | +--->+-----+ | | | | | | a | | | +------------------->| | | | . | t | | | . | | | | . | c | | | +------------------->| | | | | | h | | | V | | | +----->+-----+ | p | | | | | | | | | | a | | | AES | | | | | | | d | | | | | | +------->+-----+ | | | | | +------------------->| | | | +---+ Figure 3: Scratchpad initialization diagram4. 内存难解循环

在主循环之前,Keccak状态的字节0..31和32..63被异或,所得到的32个字节用于初始化变量a和b,每个16个字节。这些变量用于主循环。主循环迭代524,288次。当一个16字节值需要转换成暂存器中的一个地址解释为小端数整数,21位低位用作字节索引。但是,索引的4个低位是清除以确保16字节对齐。从...读取数据以16字节的块写入暂存器。每次迭代都可以用以下伪代码表示:scratchpad_address = to_scratchpad_address(a) scratchpad[scratchpad_address] = aes_round(scratchpad [scratchpad_address], a) b, scratchpad[scratchpad_address] = scratchpad[scratchpad_address], b xor scratchpad[scratchpad_address] scratchpad_address = to_scratchpad_address(b) a = 8byte_add(a, 8byte_mul(b, scratchpad[scratchpad_address])) a, scratchpad[scratchpad_address] = a xor scratchpad[scratchpad_address], a其中,8byte_add函数将每个参数表示为a一对64位小端值,并将它们添加在一起,分数,模2 ^ 64。结果转换成16字节。

然而,8byte_mul函数仅使用每个字节的前8个字节参数,被解释为无符号64位小端整数并相乘。结果转换为16字节,最后交换结果的两个8字节的一半。

该图说明了内存难解循环:+-------------------------------------------------------------+ | Final state | +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | | | +-----+ | +-->| XOR |<--+ +-----+ | | +----+ +----+ | | V V +---+ +---+ | a | | b | +---+ +---+ | | --------------------- REPEAT 524288 TIMES --------------------- | | address +---+ +-------------|----------------------------------->| | | +-----+ | read | | +-->| AES |<--|------------------------------------| | | +-----+ V | | | | +-----+ | S | | +-->| XOR | | | | | +-----+ write | c | | | | +------------------------------>| | | | +----+ address | r | | +------------------------------------------>| | | | +-----------+ read | a | | +->| 8byte_mul |<--+------------------------| | | | +-----------+ | | t | | | | | | | | | V | | c | | | +-----------+ | | | +------|->| 8byte_add | | | h | | +-----------+ | | | | | | write | p | | +---------|----------------------->| | | | | | a | | V | | | | +-----+ | | d | | | XOR |<-----+ | | | +-----+ | | +------+ | | | +-------------|-+ | | | | +---+ -------------------------- END REPEAT ------------------------- | | Figure 4: Memory-hard loop diagram5. 结果计算

在内存难解部分之后,来自Keccak状态的字节32..63以与第一个相同的方式扩展为10个AES循环密钥部分。

字节64..191从Keccak状态提取并与第一个128字节的暂存器。然后结果被加密与第一部分相同,但使用新的键。该结果与暂存器中的第二个128个字节进行异或运算,再次加密,等等。

在与暂存器的最后128个字节进行异或运算后,结果是加密最后一次,然后在Keccak中的字节64..191状态被替换为结果。然后,凯凯克州通过通过Keccak-f(Keccak排列),b = 1600。

然后,使用状态的第一个字节的2个低位选择一个散列函数:0 = BLAKE-256 [BLAKE],1 = Groestl-256 [GROESTL] 2 = JH-256 [JH],3 = Skein-256 [SKEIN]。所选的哈希函数是然后应用到Keccak状态,并且生成的哈希是输出CryptoNight。

下图说明了结果计算:+-------------------------------------------------------------+ | Final state | +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | | | | | +--------+ | | | V | | | |+-------------+ | | | || Round key 0 |-|---+---+ | | |+-------------+ | | | | | || . | | | | | | || . | | | | | | || . | | | | | | |+-------------+ | | | | | +---+ || Round key 9 |-|-+-|-+ | V | | | |+-------------+ | | | | | +-----+ | | |-|----------------|-|-|-|-|->| XOR | | | | | | | | | | +-----+ | | S | | | | | | | | | | | | | | | | | V | | c | | | | | | +->+-----+ | | | | | | | | | | | | r | | | | | | | | | | | | | | | | | AES | | | a | | | | | | | | | | | | | | | | | | | | t | | | | | +--->+-----+ | | | | | | | | | | c | | | | | V | | | | | | | +-----+ | | h |-|----------------|-|-|----->| XOR | | | | | | | | +-----+ | | p | | | | | | | | | | | | | . | | a | | | | | . | | | | | | | . | | d | | | | | | | | | | | | | V | | | | | | | +-----+ | | |-|----------------|-|-|----->| XOR | | | | | | | | +-----+ | +---+ | | | | | | | | | | V | | | | +----->+-----+ | | | | | | | | | | | | | | | | | AES | | | | | | | | | | | | | | | | +------->+-----+ | | | | | V V V V +-------------+--------------+---------------+----------------+ | Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 | +-------------+--------------+---------------+----------------+ | Modified state | +-------------------------------------------------------------+ | V +----------+ | Keccak-f | +----------+ | | +-----------+ | | | V V +-------------+ +-------------+ | Select hash |->| Chosen hash | +-------------+ +-------------+ | V +--------------+ | Final result | +--------------+ Figure 5: Result calculation diagram哈希示例:Empty string: eb14e8a833fac6fe9a43b57b336789c46ffe93f2868452240720607b14387e11. "This is a test": a084f01d1437a09c6985401b60d43554ae105802c5f5d8a9b3253649c0be6605.6. References

[AES] "Announcing the ADVANCED ENCRYPTION STANDARD", FIPS 197, 2001.

[BLAKE] Aumasson, J.-P., Henzen, L., Meier, W., and R. C.-W. Phan, "SHA-3 proposal BLAKE", 2010.

[GROESTL] Gauravaram, P., Knudsen, L., Matusiewicz, K., Mendel, F., Rechberger, C., Schlaffer, M., and S. Thomsen, "Groestl - a SHA-3 candidate", 2011.

[JH] Wu, H., "The Hash Function JH", 2011.

[KECCAK] Bertoni, G., Daemen, J., Peeters, M., and G. Van Assche, "The Keccak reference", 2011.

[SKEIN] Ferguson, N., Lucks, S., Schneier, B., Whiting, D., Bellare, M., Kohno, T., Callas, J., and J. Walker, "The Skein Hash Function Family", 2008.

区块链核心技术演进之路-算法演进

区块链技术lupenglogo 发表了文章 • 0 个评论 • 115 次浏览 • 2018-03-17 17:13 • 来自相关话题

回首2008年,由次贷危机引发的金融危机蔓延全球,11月份,一篇名为《Bitcoin:A peer-to-peer electronic cash system》的论文横空出世,当时只是在一小戳圈子里被讨论,大概没几个人知道论文的意义。时间的年轮很快转入新的一年,比特币第一版本代码发布,1月4日,创世块被挖出来,5天之后,第二个块产生,比特币网络正式启动,一个自称“中本聪”的人悄悄在互联网应用这片汪洋大海吹起一片涟漪,时至今日,这片涟漪已形成滔滔大浪。

当年能读懂大神论文的人,大多惊讶于这套系统的简洁和完美,甚至有人断言此物一出,开天辟地。如今近乎8年过去,当年看起来近乎完美的系统理念,在各个方面都有长足探索和发展。接下来我将写一系列文章,回顾区块链核心技术演进之路。包括算法演进,挖矿演进,共识机制演进,代币演进,隐私的演进,以及容量和速率的演进等。题目比较大,抛砖引玉,望读者指正和补充。
算法演进

关于“算法”一词,目前国内用户使用的比较模糊,有时指共识机制,比如POW算法,POS算法;有时指具体的Hash算法,比如SHA256,SCRYPT。应该说这是由于早期从外文资料翻译过来概念模糊导致的错误,后来人云亦云。共识机制(以前一般叫Proof,现在经常使用Consensus)和算法(Algorithm)在英文资料里语义清晰,不能混为一谈,两者都是区块链技术体系里的重要支柱。

因此当我们说“X币使用Y算法”的时候,其实具体指的是采用何种Hash算法,而且隐含的前提条件是这个币使用POW证明方式。只有在POW下讨论选取何种算法才有意义,算法的各种复杂设计才能体现其用处。为什么呢,中本聪在设计比特币的时候其实有很多地方用到Hash函数,比如计算区块ID,计算交易ID,构造代币地址等。我们说的算法具体是指用何种Hash函数计算区块ID,所谓算法创新也就是在这个地方下功夫。此外其他任何用到Hash函数的地方,对计算难度没有要求,而且应该选用可以快速运算的算法,尤其在计算交易ID时候,不然影响区块链同步速度。因此如果选用POS方式,计算区块ID也应该使用容易运算的算法。
Hash函数

如上所言,我们经常说的POW算法本质是一个Hash函数。Hash函数是一个无比神奇的东西,说他替中本聪打下了半壁江山一点不为过,学习比特币应该从学习Hash函数入手,理解了Hash函数再去学比特币原理将事半功倍,不然将处处感觉混沌,难以开窍。而中本聪也将Hash函数的所有特性使用得淋漓尽致:

已经有很多Hash函数被设计出来并广泛应用,不过Hash函数一般安全寿命都不长,被认为安全的算法往往没能使用多久就被成功攻击,新的更安全的算法相继被设计出来,而每一个被公认为安全可靠的算法都有及其严格的审计过程。在币圈中我们经常说某某币发明了某种算法,其实主要都是使用那些被认证过的安全算法,或是单独使用,或是排列组合使用。
SHA256

SHA (Secure Hash Algorithm,译作安全散列算法) 是美国国家安全局 (NSA) 设计,美国国家标准与技术研究院 (NIST) 发布的一系列密码散列函数,经历了SHA-0,SHA-1,SHA-2,SHA-3系列发展。NSA于2007年正式宣布在全球范围内征集新新一代(SHA-3)算法设计,2012年公布评选结果, Keccak算法最终获胜成为唯一官方标准SHA-3算法,但还有四种算法同时进入了第三轮评选,分别是:BLAKE, GrøSTL, JH和SKEIN,这些算法其实也非常安全,而且经受审查,被各种竞争币频繁使用。

比特币采用SHA256算法,该算法属于SHA-2系列,在中本聪发明比特币时(2008)被公认为最安全最先进的算法之一。除了生成地址中有一个环节使用了REPID-160算法,比特币系统中但凡有需要做Hash运算的地方都是用SHA256。随着比特币被更多人了解,大家开始好奇中本聪为何选择了SHA256,同时对SHA256的安全性发表各种意见,SHA256妥妥经受了质疑,到目前为止,没有公开的证据表明SHA256有漏洞,SHA256依然牢牢抗住保卫比特币安全的大旗。当然大家心里都明白,没有永远安全的算法,SHA256被替代是早晚的事,中本聪自己也说明了算法升级的必要和过程。
SCRYPT

后来随着显卡挖矿以及矿池的出现,社区开始担心矿池会导致算力集中,违背中本聪“一CPU一票”的最初设计理念。在那段时间,中心化的焦虑非常严重,讨论很激烈,比特币一次又一次“被死亡”,直到现在,针对矿池是否违背去中心化原则的争论仍在继续。

无论如何,有人将矛头指向SHA256,认为是算法太容易导致矿机和矿池出现,并试图寻找更难的算法。

恰逢其时,使用SCRYPT算法的莱特币(Litecoin)横空出世。据说SCRYPT是由一位著名的黑客开发,由于没有得到诸如SHA系列的严格的安全审查和全面论证,一直没被广泛推广使用。与SHA256算法相比,SCRYPT占用的内存更多,计算时间更长,并行计算异常困难,对硬件要求很高。很显然,SCRYPT算法具有更强的抵御矿机性,莱特币还将区块时间改为2.5分钟,在那个山寨币还凤毛麟角年代,莱特币依靠这两点创新大获成功,长期稳坐山寨币第一宝座位置。

后来有人在SCRYPT的基础上稍作修改形成Scrypt –N算法,改进思路都一样,都是追求更大的内存消耗和计算时间,以有效阻止ASIC专用矿机。

很快,莱特币的成功催生了各种各样的算法创新,2012至2014年间,算法创新一直都是社区讨论的热门话题,每一个使用创新算法的币种出现,都能刮起一阵波澜。
串联算法

重新排列组合是人类一贯以来最常用的创新发明方法。很快,有人不满足于使用单一Hash函数,2013年7月,夸克币(Quark)发布,首创使用多轮Hash算法,看似高大上,其实很简单,就是对输入数据运算了9次hash函数,前一轮运算结果作为后一轮运算的输入。这9轮Hash共使用6种加密算法,分别为BLAKE, BMW, GROESTL, JH, KECCAK和SKEIN,这些都是公认的安全Hash算法,并且早已存在现成的实现代码。

这种多轮Hash一出现就给人造成直观上很安全很强大的感觉,追捧者无数。现今价格依然坚挺的达世币(DASH,前身是暗黑币,Darkcoin,)接过下一棒,率先使用11种加密算法(BLAKE, BMW, GROESTL, JH, KECCAK, SKEIN, LUFFA, CUBEHASH, SHAVITE, SIMD, ECHO),美其名曰X11,紧接着X13,X15这一系列就有人开发出来了。

S系列算法实际是一种串联思路,只要其中一种算法被破解,整个算法就被破解了,好比一根链条,环环相扣,只要其中一环断裂,整个链条就一分为二。
并联算法

有人串联,就有人并联,Heavycoin(HVC)率先做了尝试。HVC如今在国内名不见经传,当时还是名噪一时,首次实现链上游戏,作者是俄罗斯人,后来不幸英年早逝,在币圈引起一阵惋惜。

HVC算法细节:

对输入数据首先运行一次HEFTY1(一种Hash算法)运算,得到结果d1
以d1为输入,依次进行SHA256、KECCAK512、GROESTL512、BLAKE512运算,分别获得输出d2,d3,d4和d5
分别提取d2-d5前64位,混淆后形成最终的256位Hash结果,作为区块ID。



之所以首先进行一轮HEFTY1 哈希,是因为HEFTY1 运算起来极其困难,其抵御矿机性能远超于SCRYPT。但与SCRYPT一样,安全性没有得到某个官方机构论证,于是加入后面的四种安全性已经得到公认的算法增强安全。

对比串联和并联的方法,Quark、X11,X13等虽使用了多种HASH函数,但这些算法都是简单的将多种HASH函数串联在一起,仔细思考,其实没有提高整体的抗碰撞性,其安全性更是因木桶效应而由其中安全最弱的算法支撑,其中任何一种Hash函数遭遇碰撞性攻击,都会危及货币系统的安全性。

HVC从以上每种算法提取64位,经过融合成为最后的结果,实际上是将四种算法并联在一起,其中一种算法被破解只会危及其中64位,四中算法同时被破解才会危及货币系统的安全性。

比特币只使用了一种Hash算法,假如未来某日SHA256被证明不再安全时,虽然可以更该算法,但考虑到如今“硬分叉猛于虎”的局面,届时引发动荡不可避免,但如果使用并联算法,就可以争取平静的硬分叉过渡时间。

PRIMECOIN

正当一部分人在算法探索之路上进行的如火如荼之时,另一部分人的声音也非常刺耳,那就是指责POW浪费能源(彼时POS机制已经实现)。POW党虽极力维护,但也承认耗费能源这一事实。这一指责打开了另一条探索之路,即如果能找到一种算法,既能维护区块链安全,这些Hash运算又能在其他方面产生价值,那简直更完美。

在这条探索之路上最让人振奋人心的成果来自于Sunny King(这大神之前已经开发了Peercoin,点点币)发明的素数币(Primecoin)。素数币算法的核心理念是:在做Hash运算的同时寻找大素数。素数如今已被广泛应用于各个领域,但人类对他的认识还是有限。素数在数轴上不但稀有(相对于偶数而言),而且分布不规律,在数轴上寻找素数只能盲目搜索探测,这正是POW的特征。

POW还有另一个要求是容易验证,这方面人类经过几百年探索已经获得一些成果。素数币使用两种方法测试,首先进行费马测试(Fermat Test),通过则再进行欧拉-拉格朗日-立夫习兹测试(Euler-Lagrange-Lifchitz Test),还通过测试则被视为是素数。需要指出的是,这种方法并不能保证通过测试的数百分百是素数,不过这并不影响系统运行,即便测试结果错误,只要每个节点都认为是素数就行。

素数币其实找的是素数链-坎氏链,存在三个特定类型的坎氏素数链:第一类坎氏链,第二类坎氏链和双坎氏链。

举第一类来说明,规则是:素数链中每个数都是前一个数的两倍减一,比如:

1531,3061,6121,12241,24481

数列的下一个数48961(24481*2-1)不是素数,因而这个坎氏链的长度是5,素数币的目标就是探索更长的坎氏链(以上三类都可以)。

那么现在最重要的问题来了,如何用坎氏链来验证一个区块是否合格呢?素数币实现的细节是这样的:

计算中本聪区块头Hash,hashBlockHeader = SHA256(BlockHeader)
通过变换获得坎氏链的第一个数:originNum = hashBlockHeader * Multiplier


获取originNum之后就可以测试并计算素数链长度的整数部分,小数部分的计算与坎氏链最后一个非素数的跨度相关。

每个区块的乘积因子Multiplier各不相同,计算过程和hashBlockHeader相关,素数币为此对区块头进行修改,专门增加一个字段(bnPrimeChainMultiplier)来存放这个乘积因子。但是以上第一步计算hashBlockHeader时输入数据并不包含这个乘积因子,这也是为啥特别指出中本聪区块头。

由于素数在数轴上分布不均匀,且根据目前掌握的知识来看,数越大,素数越稀有,寻找难度并不是线性递增,耗时也就不可预估,但是区块链要求稳定出块。正因为这点,素数币算法没有得到热捧,但这种探索并非没有意义,利用POW工作量的“幻想”并没有停止,探索还在继续。
ETHASH

以太坊(Ethereum)一开始就打算使用POS方式,但由于POS设计存在一些问题,开发团队决定在以太坊1.0阶段使用POW方式,预计在Serenity阶段转入POS。

以太坊POW算法叫Ethash,虽只是一个过渡算法,但开发团队一点也不含糊,一如既往发扬其“简单问题复杂化,繁琐细节秀智商”的设计风格。Ethash 是最新版本的 Dagger-Hashimoto改良算法,是Hashimoto算法结合Dagger算法产成的一个新变种。Ethash设计时就明确两大目标:

抵御矿机性能(ASIC-resistance),团队希望CPU也能参与挖矿获得收益。
轻客户端可快速验证(Light client verifiability)。


基于以上两个目标,开发团队最后倒腾出来的Ethash挖矿时基本与CPU性能无关,却和内存大小和内存带宽成正相关。不过在实现上还是借鉴了SHA3的设计思路,但是使用的”SHA3_256” ,”SHA3_512”与标准实现很不同。

Ethash基本流程是这样的:对于每一个块,首先计算一个种子(seed),该种子只和当前块的信息有关;然后根据种子生成一个32M的随机数据集(Cache);紧接着根据Cache生成一个1GB大小的数据集合(DAG),DAG可以理解为一个完整的搜索空间,挖矿的过程就是从DAG中随机选择元素(类似于比特币挖矿中查找合适Nonce)再进行哈希运算。可以从Cache快速计算DAG指定位置的元素,进而哈希验证。此外还要求对Cache和DAG进行周期性更新,每1000个块更新一次,并且规定DAG的大小随着时间推移线性增长,从1G开始,每年大约增长7G左右。
EQUIHASH

最近在国内发展势头最猛的莫过于Zcash,该币种最大的特点是使用零知识证明实现隐私交易。距离发布还有几天,但从社区讨论来看,各方矿工都已在磨刀霍霍。Zcash对于算法的选择非常慎重,在先后考量了SHA256D,SCRYPT,CUCKOO HASH以及LYRA2等算法后,最终选择Equihash。

Equihash算法由Alex Biryukov 和 Dmitry Khovratovich联合发明,其理论依据是一个著名的计算法科学及密码学问题——广义生日悖论问题。Equihash是一个内存(ARM)依赖型算法,机器算力大小主要取决于拥有多少内存,根据两位发明者的论文描述,该算法执行至少需要700M内存,1.8 GHz CPU计算30秒,经Zcash项目优化后,目前每个挖矿线程需要1G内存,因此Zcash官方认为该算法在短时间内很难出现矿机(ASIC)。此外,Zcash官方还相信该算法比较公平,他们认为很难有人或者机构能够对算法偷偷进行优化,因为广义生日悖论是一个已经被广泛研究的问题。此外,Equihash算法非常容易验证,这对于未来在受限设备上实现Zcash轻客户端非常重要。

Zcash官方团队选择Equihash完全出于抵御矿机性能的需求,他们在官方博客中也承认并不敢确保Equihash一定是安全的,并表示如果发现Equihash存在问题,或者发现更优算法,Zcash会改变POW算法。
总结

随着比特币、莱特币矿机相继出现,大家已经认识到没有不能开发矿机的算法,想通过改进算法来彻底阻止矿机和矿池的出现是不可能的。另外,从近几年的发展来看,矿池也没有之前想的那么可怕,甚至已经有人论证了矿池并没有破坏去中心化。但除了安全性,POW往往伴随分发代币功能,从这个角度来说,CPU算法更具公平性,用户门槛更低,这也是算法创新的驱动,从Ethash以及Equihash设计来看,目前的算法创新仍然是以追求内存高消耗为主。以此同时,社区在共识机制的探索之路上也取得很多成果。纵观当前区块链核心技术发展全局,算法创新热潮已经有所消退,但也未停止,于比特币,于区块链,于算法探索而言,都还在路上。 查看全部
回首2008年,由次贷危机引发的金融危机蔓延全球,11月份,一篇名为《Bitcoin:A peer-to-peer electronic cash system》的论文横空出世,当时只是在一小戳圈子里被讨论,大概没几个人知道论文的意义。时间的年轮很快转入新的一年,比特币第一版本代码发布,1月4日,创世块被挖出来,5天之后,第二个块产生,比特币网络正式启动,一个自称“中本聪”的人悄悄在互联网应用这片汪洋大海吹起一片涟漪,时至今日,这片涟漪已形成滔滔大浪。

当年能读懂大神论文的人,大多惊讶于这套系统的简洁和完美,甚至有人断言此物一出,开天辟地。如今近乎8年过去,当年看起来近乎完美的系统理念,在各个方面都有长足探索和发展。接下来我将写一系列文章,回顾区块链核心技术演进之路。包括算法演进,挖矿演进,共识机制演进,代币演进,隐私的演进,以及容量和速率的演进等。题目比较大,抛砖引玉,望读者指正和补充。
算法演进

关于“算法”一词,目前国内用户使用的比较模糊,有时指共识机制,比如POW算法,POS算法;有时指具体的Hash算法,比如SHA256,SCRYPT。应该说这是由于早期从外文资料翻译过来概念模糊导致的错误,后来人云亦云。共识机制(以前一般叫Proof,现在经常使用Consensus)和算法(Algorithm)在英文资料里语义清晰,不能混为一谈,两者都是区块链技术体系里的重要支柱。

因此当我们说“X币使用Y算法”的时候,其实具体指的是采用何种Hash算法,而且隐含的前提条件是这个币使用POW证明方式。只有在POW下讨论选取何种算法才有意义,算法的各种复杂设计才能体现其用处。为什么呢,中本聪在设计比特币的时候其实有很多地方用到Hash函数,比如计算区块ID,计算交易ID,构造代币地址等。我们说的算法具体是指用何种Hash函数计算区块ID,所谓算法创新也就是在这个地方下功夫。此外其他任何用到Hash函数的地方,对计算难度没有要求,而且应该选用可以快速运算的算法,尤其在计算交易ID时候,不然影响区块链同步速度。因此如果选用POS方式,计算区块ID也应该使用容易运算的算法。
Hash函数

如上所言,我们经常说的POW算法本质是一个Hash函数。Hash函数是一个无比神奇的东西,说他替中本聪打下了半壁江山一点不为过,学习比特币应该从学习Hash函数入手,理解了Hash函数再去学比特币原理将事半功倍,不然将处处感觉混沌,难以开窍。而中本聪也将Hash函数的所有特性使用得淋漓尽致:

已经有很多Hash函数被设计出来并广泛应用,不过Hash函数一般安全寿命都不长,被认为安全的算法往往没能使用多久就被成功攻击,新的更安全的算法相继被设计出来,而每一个被公认为安全可靠的算法都有及其严格的审计过程。在币圈中我们经常说某某币发明了某种算法,其实主要都是使用那些被认证过的安全算法,或是单独使用,或是排列组合使用。
SHA256

SHA (Secure Hash Algorithm,译作安全散列算法) 是美国国家安全局 (NSA) 设计,美国国家标准与技术研究院 (NIST) 发布的一系列密码散列函数,经历了SHA-0,SHA-1,SHA-2,SHA-3系列发展。NSA于2007年正式宣布在全球范围内征集新新一代(SHA-3)算法设计,2012年公布评选结果, Keccak算法最终获胜成为唯一官方标准SHA-3算法,但还有四种算法同时进入了第三轮评选,分别是:BLAKE, GrøSTL, JH和SKEIN,这些算法其实也非常安全,而且经受审查,被各种竞争币频繁使用。

比特币采用SHA256算法,该算法属于SHA-2系列,在中本聪发明比特币时(2008)被公认为最安全最先进的算法之一。除了生成地址中有一个环节使用了REPID-160算法,比特币系统中但凡有需要做Hash运算的地方都是用SHA256。随着比特币被更多人了解,大家开始好奇中本聪为何选择了SHA256,同时对SHA256的安全性发表各种意见,SHA256妥妥经受了质疑,到目前为止,没有公开的证据表明SHA256有漏洞,SHA256依然牢牢抗住保卫比特币安全的大旗。当然大家心里都明白,没有永远安全的算法,SHA256被替代是早晚的事,中本聪自己也说明了算法升级的必要和过程。
SCRYPT

后来随着显卡挖矿以及矿池的出现,社区开始担心矿池会导致算力集中,违背中本聪“一CPU一票”的最初设计理念。在那段时间,中心化的焦虑非常严重,讨论很激烈,比特币一次又一次“被死亡”,直到现在,针对矿池是否违背去中心化原则的争论仍在继续。

无论如何,有人将矛头指向SHA256,认为是算法太容易导致矿机和矿池出现,并试图寻找更难的算法。

恰逢其时,使用SCRYPT算法的莱特币(Litecoin)横空出世。据说SCRYPT是由一位著名的黑客开发,由于没有得到诸如SHA系列的严格的安全审查和全面论证,一直没被广泛推广使用。与SHA256算法相比,SCRYPT占用的内存更多,计算时间更长,并行计算异常困难,对硬件要求很高。很显然,SCRYPT算法具有更强的抵御矿机性,莱特币还将区块时间改为2.5分钟,在那个山寨币还凤毛麟角年代,莱特币依靠这两点创新大获成功,长期稳坐山寨币第一宝座位置。

后来有人在SCRYPT的基础上稍作修改形成Scrypt –N算法,改进思路都一样,都是追求更大的内存消耗和计算时间,以有效阻止ASIC专用矿机。

很快,莱特币的成功催生了各种各样的算法创新,2012至2014年间,算法创新一直都是社区讨论的热门话题,每一个使用创新算法的币种出现,都能刮起一阵波澜。
串联算法

重新排列组合是人类一贯以来最常用的创新发明方法。很快,有人不满足于使用单一Hash函数,2013年7月,夸克币(Quark)发布,首创使用多轮Hash算法,看似高大上,其实很简单,就是对输入数据运算了9次hash函数,前一轮运算结果作为后一轮运算的输入。这9轮Hash共使用6种加密算法,分别为BLAKE, BMW, GROESTL, JH, KECCAK和SKEIN,这些都是公认的安全Hash算法,并且早已存在现成的实现代码。

这种多轮Hash一出现就给人造成直观上很安全很强大的感觉,追捧者无数。现今价格依然坚挺的达世币(DASH,前身是暗黑币,Darkcoin,)接过下一棒,率先使用11种加密算法(BLAKE, BMW, GROESTL, JH, KECCAK, SKEIN, LUFFA, CUBEHASH, SHAVITE, SIMD, ECHO),美其名曰X11,紧接着X13,X15这一系列就有人开发出来了。

S系列算法实际是一种串联思路,只要其中一种算法被破解,整个算法就被破解了,好比一根链条,环环相扣,只要其中一环断裂,整个链条就一分为二。
并联算法

有人串联,就有人并联,Heavycoin(HVC)率先做了尝试。HVC如今在国内名不见经传,当时还是名噪一时,首次实现链上游戏,作者是俄罗斯人,后来不幸英年早逝,在币圈引起一阵惋惜。

HVC算法细节:

对输入数据首先运行一次HEFTY1(一种Hash算法)运算,得到结果d1
以d1为输入,依次进行SHA256、KECCAK512、GROESTL512、BLAKE512运算,分别获得输出d2,d3,d4和d5
分别提取d2-d5前64位,混淆后形成最终的256位Hash结果,作为区块ID。



之所以首先进行一轮HEFTY1 哈希,是因为HEFTY1 运算起来极其困难,其抵御矿机性能远超于SCRYPT。但与SCRYPT一样,安全性没有得到某个官方机构论证,于是加入后面的四种安全性已经得到公认的算法增强安全。

对比串联和并联的方法,Quark、X11,X13等虽使用了多种HASH函数,但这些算法都是简单的将多种HASH函数串联在一起,仔细思考,其实没有提高整体的抗碰撞性,其安全性更是因木桶效应而由其中安全最弱的算法支撑,其中任何一种Hash函数遭遇碰撞性攻击,都会危及货币系统的安全性。

HVC从以上每种算法提取64位,经过融合成为最后的结果,实际上是将四种算法并联在一起,其中一种算法被破解只会危及其中64位,四中算法同时被破解才会危及货币系统的安全性。

比特币只使用了一种Hash算法,假如未来某日SHA256被证明不再安全时,虽然可以更该算法,但考虑到如今“硬分叉猛于虎”的局面,届时引发动荡不可避免,但如果使用并联算法,就可以争取平静的硬分叉过渡时间。

PRIMECOIN

正当一部分人在算法探索之路上进行的如火如荼之时,另一部分人的声音也非常刺耳,那就是指责POW浪费能源(彼时POS机制已经实现)。POW党虽极力维护,但也承认耗费能源这一事实。这一指责打开了另一条探索之路,即如果能找到一种算法,既能维护区块链安全,这些Hash运算又能在其他方面产生价值,那简直更完美。

在这条探索之路上最让人振奋人心的成果来自于Sunny King(这大神之前已经开发了Peercoin,点点币)发明的素数币(Primecoin)。素数币算法的核心理念是:在做Hash运算的同时寻找大素数。素数如今已被广泛应用于各个领域,但人类对他的认识还是有限。素数在数轴上不但稀有(相对于偶数而言),而且分布不规律,在数轴上寻找素数只能盲目搜索探测,这正是POW的特征。

POW还有另一个要求是容易验证,这方面人类经过几百年探索已经获得一些成果。素数币使用两种方法测试,首先进行费马测试(Fermat Test),通过则再进行欧拉-拉格朗日-立夫习兹测试(Euler-Lagrange-Lifchitz Test),还通过测试则被视为是素数。需要指出的是,这种方法并不能保证通过测试的数百分百是素数,不过这并不影响系统运行,即便测试结果错误,只要每个节点都认为是素数就行。

素数币其实找的是素数链-坎氏链,存在三个特定类型的坎氏素数链:第一类坎氏链,第二类坎氏链和双坎氏链。

举第一类来说明,规则是:素数链中每个数都是前一个数的两倍减一,比如:

1531,3061,6121,12241,24481

数列的下一个数48961(24481*2-1)不是素数,因而这个坎氏链的长度是5,素数币的目标就是探索更长的坎氏链(以上三类都可以)。

那么现在最重要的问题来了,如何用坎氏链来验证一个区块是否合格呢?素数币实现的细节是这样的:

计算中本聪区块头Hash,hashBlockHeader = SHA256(BlockHeader)
通过变换获得坎氏链的第一个数:originNum = hashBlockHeader * Multiplier


获取originNum之后就可以测试并计算素数链长度的整数部分,小数部分的计算与坎氏链最后一个非素数的跨度相关。

每个区块的乘积因子Multiplier各不相同,计算过程和hashBlockHeader相关,素数币为此对区块头进行修改,专门增加一个字段(bnPrimeChainMultiplier)来存放这个乘积因子。但是以上第一步计算hashBlockHeader时输入数据并不包含这个乘积因子,这也是为啥特别指出中本聪区块头。

由于素数在数轴上分布不均匀,且根据目前掌握的知识来看,数越大,素数越稀有,寻找难度并不是线性递增,耗时也就不可预估,但是区块链要求稳定出块。正因为这点,素数币算法没有得到热捧,但这种探索并非没有意义,利用POW工作量的“幻想”并没有停止,探索还在继续。
ETHASH

以太坊(Ethereum)一开始就打算使用POS方式,但由于POS设计存在一些问题,开发团队决定在以太坊1.0阶段使用POW方式,预计在Serenity阶段转入POS。

以太坊POW算法叫Ethash,虽只是一个过渡算法,但开发团队一点也不含糊,一如既往发扬其“简单问题复杂化,繁琐细节秀智商”的设计风格。Ethash 是最新版本的 Dagger-Hashimoto改良算法,是Hashimoto算法结合Dagger算法产成的一个新变种。Ethash设计时就明确两大目标:

抵御矿机性能(ASIC-resistance),团队希望CPU也能参与挖矿获得收益。
轻客户端可快速验证(Light client verifiability)。


基于以上两个目标,开发团队最后倒腾出来的Ethash挖矿时基本与CPU性能无关,却和内存大小和内存带宽成正相关。不过在实现上还是借鉴了SHA3的设计思路,但是使用的”SHA3_256” ,”SHA3_512”与标准实现很不同。

Ethash基本流程是这样的:对于每一个块,首先计算一个种子(seed),该种子只和当前块的信息有关;然后根据种子生成一个32M的随机数据集(Cache);紧接着根据Cache生成一个1GB大小的数据集合(DAG),DAG可以理解为一个完整的搜索空间,挖矿的过程就是从DAG中随机选择元素(类似于比特币挖矿中查找合适Nonce)再进行哈希运算。可以从Cache快速计算DAG指定位置的元素,进而哈希验证。此外还要求对Cache和DAG进行周期性更新,每1000个块更新一次,并且规定DAG的大小随着时间推移线性增长,从1G开始,每年大约增长7G左右。
EQUIHASH

最近在国内发展势头最猛的莫过于Zcash,该币种最大的特点是使用零知识证明实现隐私交易。距离发布还有几天,但从社区讨论来看,各方矿工都已在磨刀霍霍。Zcash对于算法的选择非常慎重,在先后考量了SHA256D,SCRYPT,CUCKOO HASH以及LYRA2等算法后,最终选择Equihash。

Equihash算法由Alex Biryukov 和 Dmitry Khovratovich联合发明,其理论依据是一个著名的计算法科学及密码学问题——广义生日悖论问题。Equihash是一个内存(ARM)依赖型算法,机器算力大小主要取决于拥有多少内存,根据两位发明者的论文描述,该算法执行至少需要700M内存,1.8 GHz CPU计算30秒,经Zcash项目优化后,目前每个挖矿线程需要1G内存,因此Zcash官方认为该算法在短时间内很难出现矿机(ASIC)。此外,Zcash官方还相信该算法比较公平,他们认为很难有人或者机构能够对算法偷偷进行优化,因为广义生日悖论是一个已经被广泛研究的问题。此外,Equihash算法非常容易验证,这对于未来在受限设备上实现Zcash轻客户端非常重要。

Zcash官方团队选择Equihash完全出于抵御矿机性能的需求,他们在官方博客中也承认并不敢确保Equihash一定是安全的,并表示如果发现Equihash存在问题,或者发现更优算法,Zcash会改变POW算法。
总结

随着比特币、莱特币矿机相继出现,大家已经认识到没有不能开发矿机的算法,想通过改进算法来彻底阻止矿机和矿池的出现是不可能的。另外,从近几年的发展来看,矿池也没有之前想的那么可怕,甚至已经有人论证了矿池并没有破坏去中心化。但除了安全性,POW往往伴随分发代币功能,从这个角度来说,CPU算法更具公平性,用户门槛更低,这也是算法创新的驱动,从Ethash以及Equihash设计来看,目前的算法创新仍然是以追求内存高消耗为主。以此同时,社区在共识机制的探索之路上也取得很多成果。纵观当前区块链核心技术发展全局,算法创新热潮已经有所消退,但也未停止,于比特币,于区块链,于算法探索而言,都还在路上。

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]

区块链技术lupenglogo 发表了文章 • 0 个评论 • 104 次浏览 • 2018-03-17 17:08 • 来自相关话题

自动驾驶汽车的发展正处在一个历史转折阶段。这类汽车不仅能够自动驾驶,而且还将拥有一个包含自己的身份和历史的“数字双胞胎”,同时还将拥有自己的数字钱包来控制自己的资产。有些人认为这在遥远的未来才能实现,然而事实是这个产业已经起步,像Bosch和Volkswagen这样的跨国工业集团正在引领这一领域的发展。

机器经济纳入汽车产业

机器将来会控制自己的财务,这听起来是不是有点奇怪?机器将不需要人为干预就能在后台共享数据和价值。一些中间环节会被淘汰,价值创造被提升到一个新的层次。

如果你没有理解上文所述,那么让我们一起来看一下下面的情景:

当你吃完早饭,准备去上班。你只需要按一次按键,一辆(共享)电动自动驾驶汽车就会出现在你的家门外等待你。汽车可识别你的数字身份,并自动在你和汽车之间创建一个安全的一对一交易。在通往目的地的过程中,你的钱包和汽车的钱包会根据行驶里程计算打车费。到达目的地后,汽车会停止并让你下车,你和汽车之间的交易也会自动结束。

如果这辆汽车检测到自己的电量不足时,会自动驶往充电站,在这里,汽车与充电桩之间会自动无缝的进行货币交易并完成充电。最后,汽车驶往最近的公共车库,等待下一位顾客。

这听起来有点科幻,但如果你相信并了解IOTA,你就应该明白它已经离我们不远了。这只是一个小例子,但是你可以想象更多。汽车如何管理自己的经济活动的其他例子:

汽车会监测自身重要安全部件的健康状况,会自动前往维修厂更换问题部件。
汽车自动订购自助服务(例如清洁等),完成后离开工作间。
汽车本身可收集诸如天气、环境、空气质量等方面的数据,并可以放到数据市场上收售。
汽车可以与智能停车场自动交换信息,自动寻找车位,自助泊车并自动支付停车费。

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]

机器经济是IOTA愿景的重要组成部分,这一想法吸引了主要工业集团和跨国公司的关注。欧洲最大的汽车制造商大众集团(Volkswagen Group)是在IOTA技术中看到巨大潜力的参与者之一。大众集团的数字主管约翰•里沃斯(Johann Jungwirth)此前已经加入IOTA基金会,他希望在IOTA基金会担任战略角色,并担任IOTA与大众集团之间未来合作的顾问。

Jungwirth希望在未来五年内担任IOTA基金会顾问,并表示他将在IOTA和大众汽车围绕自动驾驶汽车以及自动支付汽车项目的合作上发挥重要作用。
——IOTA联合创始人David Sønstebø

2月21日至22日,博世在柏林举行了博世联网世界会议。Johann Jungwirth介绍了移动行业的未来。作为未来的一部分,他将IOTA视为一个潜在的具有突破性的分布式账本技术。下面是Johann Jungwirth在会议上的演讲视频:

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]

视频:大众汽车CDO Johann Jungwirth在2018年博世互联世界中谈到IOTA

大众并不是唯一注意到IOTA的企业。 博世也对这项技术表现出浓厚的兴趣。作为一个大型跨国工业集团,博世通过麾下的风险投资公司罗伯特博世风险投资公司(RBVC)购买了大量的IOTA加密货币。

我们已经与IOTA合作了一年多,并且对IOTA的技术非常感兴趣,我们认为这可能成为计算机之间通信的标准。

——蒋洪泉(音)博士,RBVC投资合伙人。

博世解释说,他们将把IOTA开发的技术用于汽车零部件的在线生产上。你可能会问,博世不是做厨电和洗衣机、干洗机的吗? 信不信由你,博世也经营着全球最大的汽车产业。不仅如此,如下面的调查所显示的那样,他们是拥有自动驾驶汽车专利最多的企业。

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]


博世在IOTA中看到了巨大的潜力,在一个汽车可以自动沟通和协商的世界里,它看到了IOTA的多种用途。IOTA技术的架构使机器能够无缝地发送数据流或进行微型交易,而且无需支付交易费用,IOTA几乎就是为博世在汽车物联网和传感器方面的投资量身定制的。 查看全部
自动驾驶汽车的发展正处在一个历史转折阶段。这类汽车不仅能够自动驾驶,而且还将拥有一个包含自己的身份和历史的“数字双胞胎”,同时还将拥有自己的数字钱包来控制自己的资产。有些人认为这在遥远的未来才能实现,然而事实是这个产业已经起步,像Bosch和Volkswagen这样的跨国工业集团正在引领这一领域的发展。

机器经济纳入汽车产业

机器将来会控制自己的财务,这听起来是不是有点奇怪?机器将不需要人为干预就能在后台共享数据和价值。一些中间环节会被淘汰,价值创造被提升到一个新的层次。

如果你没有理解上文所述,那么让我们一起来看一下下面的情景:

当你吃完早饭,准备去上班。你只需要按一次按键,一辆(共享)电动自动驾驶汽车就会出现在你的家门外等待你。汽车可识别你的数字身份,并自动在你和汽车之间创建一个安全的一对一交易。在通往目的地的过程中,你的钱包和汽车的钱包会根据行驶里程计算打车费。到达目的地后,汽车会停止并让你下车,你和汽车之间的交易也会自动结束。

如果这辆汽车检测到自己的电量不足时,会自动驶往充电站,在这里,汽车与充电桩之间会自动无缝的进行货币交易并完成充电。最后,汽车驶往最近的公共车库,等待下一位顾客。

这听起来有点科幻,但如果你相信并了解IOTA,你就应该明白它已经离我们不远了。这只是一个小例子,但是你可以想象更多。汽车如何管理自己的经济活动的其他例子:

汽车会监测自身重要安全部件的健康状况,会自动前往维修厂更换问题部件。
汽车自动订购自助服务(例如清洁等),完成后离开工作间。
汽车本身可收集诸如天气、环境、空气质量等方面的数据,并可以放到数据市场上收售。
汽车可以与智能停车场自动交换信息,自动寻找车位,自助泊车并自动支付停车费。

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]

机器经济是IOTA愿景的重要组成部分,这一想法吸引了主要工业集团和跨国公司的关注。欧洲最大的汽车制造商大众集团(Volkswagen Group)是在IOTA技术中看到巨大潜力的参与者之一。大众集团的数字主管约翰•里沃斯(Johann Jungwirth)此前已经加入IOTA基金会,他希望在IOTA基金会担任战略角色,并担任IOTA与大众集团之间未来合作的顾问。

Jungwirth希望在未来五年内担任IOTA基金会顾问,并表示他将在IOTA和大众汽车围绕自动驾驶汽车以及自动支付汽车项目的合作上发挥重要作用。
——IOTA联合创始人David Sønstebø

2月21日至22日,博世在柏林举行了博世联网世界会议。Johann Jungwirth介绍了移动行业的未来。作为未来的一部分,他将IOTA视为一个潜在的具有突破性的分布式账本技术。下面是Johann Jungwirth在会议上的演讲视频:

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]

视频:大众汽车CDO Johann Jungwirth在2018年博世互联世界中谈到IOTA

大众并不是唯一注意到IOTA的企业。 博世也对这项技术表现出浓厚的兴趣。作为一个大型跨国工业集团,博世通过麾下的风险投资公司罗伯特博世风险投资公司(RBVC)购买了大量的IOTA加密货币。

我们已经与IOTA合作了一年多,并且对IOTA的技术非常感兴趣,我们认为这可能成为计算机之间通信的标准。

——蒋洪泉(音)博士,RBVC投资合伙人。

博世解释说,他们将把IOTA开发的技术用于汽车零部件的在线生产上。你可能会问,博世不是做厨电和洗衣机、干洗机的吗? 信不信由你,博世也经营着全球最大的汽车产业。不仅如此,如下面的调查所显示的那样,他们是拥有自动驾驶汽车专利最多的企业。

[IOTA如何在自动驾驶汽车产业和机器经济中发挥重要作用]


博世在IOTA中看到了巨大的潜力,在一个汽车可以自动沟通和协商的世界里,它看到了IOTA的多种用途。IOTA技术的架构使机器能够无缝地发送数据流或进行微型交易,而且无需支付交易费用,IOTA几乎就是为博世在汽车物联网和传感器方面的投资量身定制的。

掩码认证消息(MAM)详细介绍

区块链技术lupenglogo 发表了文章 • 0 个评论 • 109 次浏览 • 2018-03-17 17:07 • 来自相关话题

掩码认证消息(Masked Authenticated Message),简称MAM,是IOTA最显著的特点之一。让我们来设想一下在这个布满小型物联网设备的世界上,它们的微工作、微观数据流和纳米支付遍布全球的情景。

IOTA的目标是成为未来社会最基本的层面,是当前最有远见的项目,它挑战了即将到来的范式转变。 MAM则是其核心驱动力,它通过使数据流和交易更便宜,更安全和无处不在,而将IOTA与其他分布式账本区分开来。

然而,尽管MAM在IOTA及其未来发展中具有极其重要的意义,但由于它仍处于发展阶段,所以在技术实施方面我们只能获得有限的信息。

这篇文章以一种易于理解的方式来讲解MAM,这样更多的人就可以更轻松的围绕这种下一代技术进行深入研究。

MAM频道(MAM Channel)

像Youtube和其他媒体一样,MAM也有频道。频道所有者可以发布新数据,观看者可订阅频道来获取可用的数据。这种所有权由IOTA通过Seed来实施和保护的。如果你把你的Seed告诉了别人,别人就有了在你的频道上发布任何消息的权限。再次说明,Seed在其81个trytes中保存所有的隐私和产权,切勿将其暴露并妥善保管。

频道模式(Channel Mode)

当在频道上发布新消息时,发布者可以有三种选项。Public(公共模式):每个人都可以查看。Private(私有模式):只有你(即种子所有者)可以查看。Restricted(受限模式):你可以将一个密钥告诉某个人,授权他成为查看者。这个密钥在源代码中被命名为sideKey。所以在这篇文章中,我也会沿用sideKey的称呼。而且,在任何模式下,root可以作为消息标识符被赋予给观看者以便从缠结中找到消息。

Public:掩码消息使用 root 来解密 。
Private:address=hash(root) 。掩码消息使用 root 来解密 。
Restricted:address=hash(root) 。掩码消息使用 sideKey 来解密 。

消息链(Message Chain)

在IOTA协议中,像许多其他的加密技术一样,人们可以在交易上附加任意消息,而且它是免费的。但是,它限制发送者一次只能附加一个消息,并且不能在一个消息流上发布连续的相关消息。

例如,如果您想在不使用MAM的情况下,每15分钟发布一次当前的温度数据,就必须将每个消息发布到同一个地址。因为任何分布式帐本(包括Tangle)都是可以公开访问的,所以对于攻击者来说很容易识别每15分钟更新一次的地址,并用spam交易干扰它。即使你决定每次发布新数据时都要更改地址,你也需要跟踪所有的地址,就在线存储信息而言,监控它们的成本相对较高。

然而,由于MAM的消息链设计,用户可以保护他们的频道免受任何spam的干扰,并让他们免于管理累积地址。

MAM将每条消息发送到不同的地址,但有详细的有用信息来连接它们。在这条消息链上,一代传到下一代,旧消息总是引导新的,它的流动是单向的。 查看全部
掩码认证消息(Masked Authenticated Message),简称MAM,是IOTA最显著的特点之一。让我们来设想一下在这个布满小型物联网设备的世界上,它们的微工作、微观数据流和纳米支付遍布全球的情景。

IOTA的目标是成为未来社会最基本的层面,是当前最有远见的项目,它挑战了即将到来的范式转变。 MAM则是其核心驱动力,它通过使数据流和交易更便宜,更安全和无处不在,而将IOTA与其他分布式账本区分开来。

然而,尽管MAM在IOTA及其未来发展中具有极其重要的意义,但由于它仍处于发展阶段,所以在技术实施方面我们只能获得有限的信息。

这篇文章以一种易于理解的方式来讲解MAM,这样更多的人就可以更轻松的围绕这种下一代技术进行深入研究。

MAM频道(MAM Channel)

像Youtube和其他媒体一样,MAM也有频道。频道所有者可以发布新数据,观看者可订阅频道来获取可用的数据。这种所有权由IOTA通过Seed来实施和保护的。如果你把你的Seed告诉了别人,别人就有了在你的频道上发布任何消息的权限。再次说明,Seed在其81个trytes中保存所有的隐私和产权,切勿将其暴露并妥善保管。

频道模式(Channel Mode)

当在频道上发布新消息时,发布者可以有三种选项。Public(公共模式):每个人都可以查看。Private(私有模式):只有你(即种子所有者)可以查看。Restricted(受限模式):你可以将一个密钥告诉某个人,授权他成为查看者。这个密钥在源代码中被命名为sideKey。所以在这篇文章中,我也会沿用sideKey的称呼。而且,在任何模式下,root可以作为消息标识符被赋予给观看者以便从缠结中找到消息。

Public:掩码消息使用 root 来解密 。
Private:address=hash(root) 。掩码消息使用 root 来解密 。
Restricted:address=hash(root) 。掩码消息使用 sideKey 来解密 。

消息链(Message Chain)

在IOTA协议中,像许多其他的加密技术一样,人们可以在交易上附加任意消息,而且它是免费的。但是,它限制发送者一次只能附加一个消息,并且不能在一个消息流上发布连续的相关消息。

例如,如果您想在不使用MAM的情况下,每15分钟发布一次当前的温度数据,就必须将每个消息发布到同一个地址。因为任何分布式帐本(包括Tangle)都是可以公开访问的,所以对于攻击者来说很容易识别每15分钟更新一次的地址,并用spam交易干扰它。即使你决定每次发布新数据时都要更改地址,你也需要跟踪所有的地址,就在线存储信息而言,监控它们的成本相对较高。

然而,由于MAM的消息链设计,用户可以保护他们的频道免受任何spam的干扰,并让他们免于管理累积地址。

MAM将每条消息发送到不同的地址,但有详细的有用信息来连接它们。在这条消息链上,一代传到下一代,旧消息总是引导新的,它的流动是单向的。