#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; };
让我们尝试了解如果有人试图修改交易会发生什么:
攻击者运行一个节点,并收到一个内容
AAA
为BBB
“用地址发送10个硬币”与txId的交易0x555..
攻击者将接收者地址改变为
CCC
在网络中转发。现在,交易的内容是“从地址发送10个硬币AAA
来CCC
”然而,随着接收者地址改变,txId不再有效。一个新的有效txId将是
0x567...
如果txId设置为新值,则签名无效。签名只与原始的txId匹配
0x555..
修改后的事务不会被其他节点接受,因为无论哪种方式,都是无效的。
未使用的交易输出
事务输入必须始终引用未使用的事务输出(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
方法中。应该注意的是,只有在块中的事务(和块本身)已经被验证之后才调用该方法。
交易验证
现在我们终于可以规定交易有效的规则了:
正确的交易结构
该交易必须与定义的类符合Transaction
,TxIn
和TxOut
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找到
本文暂时没有评论,来添加一个吧(●'◡'●)