程序员开发实例大全宝库

网站首页 > 编程文章 正文

建立一个加密货币的教程(三)(制作加密货币)

zazugpt 2024-08-23 00:41:21 编程文章 17 ℃ 0 评论

#3:交易

概观

在本章中,我们将介绍交易的概念。通过这种修改,我们实际上从我们的项目从“通用”区块链转移到了一个加密货币。因此,如果我们能够首先证明我们拥有这些地址,我们就可以将硬币发送给地址。

为了实现这一切,必须提出许多新的概念。这包括公钥密码学,签名和交易输入和输出。

本章中将要实现的完整代码可以在这里找到。

公钥密码学和签名

在公钥密码学中,你有一个密钥对:一个密钥和一个公钥。公钥可以从私钥中派生出来,但是私钥不能从公钥中派生出来。公钥(顾名思义)可以安全地共享给任何人。

任何消息都可以使用私钥签名来创建签名。有了这个签名和相应的公钥,任何人都可以验证签名是由与公钥匹配的私钥生成的。

我们将使用一个名为椭圆的库作为使用椭圆曲线的公钥密码。(= ECDSA)

总之,两种不同的密码功能在加密货币中用于不同的目的:

  • 散列函数(SHA256)用于工作证明挖掘(哈希也用于保持块完整性)

  • 用于交易的公钥密码术(ECDSA)(我们将在本章中实现)

私钥和公钥(在ECDSA中)

有效的私钥是任何随机的32字节的字符串,例如。 19f128debc1b9122da0635954488b208b829879cf13b3d6cac5d1260c0fd967c

一个有效的公钥是'04'连接一个64字节的字符串,例如04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534a

公钥可以从私钥导出。公钥将被用作交易中硬币的“接收者”(=地址)。

交易概览

在编写任何代码之前,让我们来概述事务的结构。交易由两部分组成:输入和输出。输出指定了硬币的发送位置,并且输入提供了实际发送的硬币首先存在并由“发送者”拥有的证据。输入总是指现有的(未使用的)输出。

交易输出

交易输出(txOut)由一个地址和一定数量的硬币组成。地址是ECDSA公钥。这意味着拥有引用的公共密钥(=地址)的私钥的用户将能够访问硬币。

class TxOut { public address: string; public amount: number; constructor(address: string, amount: number) { this.address = address; this.amount = amount; } }

交易输入

交易输入(txIn)提供硬币从哪里来的信息。每个txIn指的是一个早期的输出,硬币是“解锁”,与签名。这些解锁的硬币现在可用于txOuts。签名证明只有具有被引用的公钥(=地址)的私钥的用户才能创建交易。

class TxIn { public txOutId: string; public txOutIndex: number; public signature: string; }

应该指出,txIn只包含签名(由私钥创建),而不包含私钥本身。区块链包含公钥和签名,而不是私钥。

作为结论,也可以认为txIns解锁硬币,txOuts'重新锁定'硬币:

交易结构

事务结构本身非常简单,因为我们现在定义了txIns和txOuts。

class Transaction { public id: string; public txIns: TxIn[]; public txOuts: TxOut[]; }

交易ID

交易ID是通过从交易内容中获取散列来计算的。但是,事务哈希中包含txIds的签名,因为后面将添加到事务中。

const getTransactionId = (transaction: Transaction): string => { const txInContent: string = transaction.txIns .map((txIn: TxIn) => txIn.txOutId + txIn.txOutIndex) .reduce((a, b) => a + b, ''); const txOutContent: string = transaction.txOuts .map((txOut: TxOut) => txOut.address + txOut.amount) .reduce((a, b) => a + b, ''); return CryptoJS.SHA256(txInContent + txOutContent).toString(); };

交易签名

交易内容签字后不能改动,这一点很重要。由于交易是公开的,任何人都可以访问交易,甚至在它们被包括在区块链之前。

签署交易输入时,只会签署txId。如果交易中的任何内容被修改,则txId必须改变,使得交易和签名无效。

const signTxIn = (transaction: Transaction, txInIndex: number, privateKey: string, aUnspentTxOuts: UnspentTxOut[]): string => { const txIn: TxIn = transaction.txIns[txInIndex]; const dataToSign = transaction.id; const referencedUnspentTxOut: UnspentTxOut = findUnspentTxOut(txIn.txOutId, txIn.txOutIndex, aUnspentTxOuts); const referencedAddress = referencedUnspentTxOut.address; const key = ec.keyFromPrivate(privateKey, 'hex'); const signature: string = toHexString(key.sign(dataToSign).toDER()); return signature; };

让我们尝试了解如果有人试图修改交易会发生什么:

  1. 攻击者运行一个节点,并收到一个内容AAABBB“用地址发送10个硬币”与txId的交易0x555..

  2. 攻击者将接收者地址改变为CCC在网络中转发。现在,交易的内容是“从地址发送10个硬币AAACCC

  3. 然而,随着接收者地址改变,txId不再有效。一个新的有效txId将是0x567...

  4. 如果txId设置为新值,则签名无效。签名只与原始的txId匹配0x555..

  5. 修改后的事务不会被其他节点接受,因为无论哪种方式,都是无效的。

未使用的交易输出

事务输入必须始终引用未使用的事务输出(uTxO)。因此,当您在区块链中拥有一些硬币时,您实际上拥有的是未使用的交易输出列表,其公钥与您拥有的私钥匹配。

在交易确认方面,我们只能把重点放在没有支出的交易产出清单上,以确定交易是否有效。未使用的事务输出的列表总是可以从当前的区块链导出。在这个实施过程中,我们会在处理过程中更新未使用的交易产出清单,并将交易包含在区块链中。

未使用的事务输出的数据结构如下所示:

class UnspentTxOut { public readonly txOutId: string; public readonly txOutIndex: number; public readonly address: string; public readonly amount: number; constructor(txOutId: string, txOutIndex: number, address: string, amount: number) { this.txOutId = txOutId; this.txOutIndex = txOutIndex; this.address = address; this.amount = amount; } }

数据结构本身如果只是一个列表:

let unspentTxOuts: UnspentTxOut[] = [];

更新未使用的交易输出

每当一个新的块被添加到链中时,我们必须更新未使用的事务输出的列表。这是因为新的交易将花费一些现有的交易产出,并引入新的未支出产出。

为了处理这个问题,我们将首先newUnspentTxOuts从新块中检索所有新的未使用的事务输出():

 const newUnspentTxOuts: UnspentTxOut[] = newTransactions .map((t) => { return t.txOuts.map((txOut, index) => new UnspentTxOut(t.id, index, txOut.address, txOut.amount)); }) .reduce((a, b) => a.concat(b), []);

我们还需要知道block(consumedTxOuts)的新事务消耗了哪些事务输出。这将通过检查新交易的输入来解决:

 const consumedTxOuts: UnspentTxOut[] = newTransactions .map((t) => t.txIns) .reduce((a, b) => a.concat(b), []) .map((txIn) => new UnspentTxOut(txIn.txOutId, txIn.txOutIndex, '', 0));

最后,我们可以通过产生新的未使用的事务处理输出删除consumedTxOuts,并加入newUnspentTxOuts我们现有的交易输出。

 const resultingUnspentTxOuts = aUnspentTxOuts .filter(((uTxO) => !findUnspentTxOut(uTxO.txOutId, uTxO.txOutIndex, consumedTxOuts))) .concat(newUnspentTxOuts);

所描述的代码和功能包含在该updateUnspentTxOuts方法中。应该注意的是,只有块中的事务(和块本身)已经被验证之后才调用该方法。

交易验证

现在我们终于可以规定交易有效的规则了:

正确的交易结构

该交易必须与定义的类符合TransactionTxInTxOut

 const isValidTransactionStructure = (transaction: Transaction) => { if (typeof transaction.id !== 'string') { console.log('transactionId missing'); return false; } ... //check also the other members of class }

有效的交易ID

交易中的id必须正确计算。

 if (getTransactionId(transaction) !== transaction.id) { console.log('invalid tx id: ' + transaction.id); return false; }

有效的txIns

txIns中的签名必须是有效的,并且所引用的输出必须没有被使用。

const validateTxIn = (txIn: TxIn, transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { const referencedUTxOut: UnspentTxOut = aUnspentTxOuts.find((uTxO) => uTxO.txOutId === txIn.txOutId && uTxO.txOutId === txIn.txOutId); if (referencedUTxOut == null) { console.log('referenced txOut not found: ' + JSON.stringify(txIn)); return false; } const address = referencedUTxOut.address; const key = ec.keyFromPublic(address, 'hex'); return key.verify(transaction.id, txIn.signature); };

有效的txOut值

输出中指定的值的总和必须等于输入中指定的值的总和。如果你参考一个包含50个硬币的输出,那么新输出中的数值总和也必须是50个硬币。

 const totalTxInValues: number = transaction.txIns .map((txIn) => getTxInAmount(txIn, aUnspentTxOuts)) .reduce((a, b) => (a + b), 0); const totalTxOutValues: number = transaction.txOuts .map((txOut) => txOut.amount) .reduce((a, b) => (a + b), 0); if (totalTxOutValues !== totalTxInValues) { console.log('totalTxOutValues !== totalTxInValues in tx: ' + transaction.id); return false; }

Coinbase交易

交易投入必须始终指未支付的交易产出,但最初的硬币从哪里进入区块链?为了解决这个问题,引入了一种特殊类型的交易:coinbase交易

coinbase交易只包含一个输出,但没有输入。这意味着一个coinbase交易添加新的流通硬币。我们指定coinbase输出的数量是50个硬币。

const COINBASE_AMOUNT: number = 50;

coinbase交易总是块中的第一笔交易,并被块的矿工包括在内。硬币奖励是对矿工的一种激励:如果你找到了这个地块,你可以收集50个硬币。

我们将把块高添加到coinbase事务的输入中。这是为了确保每个coinbase交易都有一个独特的txId。例如,如果没有这个规则,一个表示“给50个硬币寻址0xabc”的coinbase事务将总是有相同的txId。

coinbase交易的确认与“正常”交易的确认略有不同

const validateCoinbaseTx = (transaction: Transaction, blockIndex: number): boolean => { if (getTransactionId(transaction) !== transaction.id) { console.log('invalid coinbase tx id: ' + transaction.id); return false; } if (transaction.txIns.length !== 1) { console.log('one txIn must be specified in the coinbase transaction'); return; } if (transaction.txIns[0].txOutIndex !== blockIndex) { console.log('the txIn index in coinbase tx must be the block height'); return false; } if (transaction.txOuts.length !== 1) { console.log('invalid number of txOuts in coinbase transaction'); return false; } if (transaction.txOuts[0].amount != COINBASE_AMOUNT) { console.log('invalid coinbase amount in coinbase transaction'); return false; } return true; };

结论

我们将交易的概念包含在区块链中。基本思想很简单:我们引用事务输入中的未使用输出,并使用签名来表示解锁部分是有效的。然后我们使用输出将它们“重新锁定”到一个接收器地址。

但是,创建交易还是非常困难的。我们必须手动创建事务的输入和输出,并使用我们的私钥对它们进行签名。当我们在下一章介绍钱包时,这将会改变。

目前还没有交易中继:要将交易包含在区块链中,您必须自己挖掘。这也是我们还没有引入交易费概念的原因。

本章中实现的完整代码可以在这里https://github.com/lhartikk/naivecoin/tree/chapter3找到

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表