第三章 自己动手写区块链之交易

  • 概览
  • 非对称加密和签名
  • 私钥和公钥
  • 交易概览
  • 交易outputs
  • 交易inputs
  • 交易数据结构
  • 交易id
  • 交易签名
  • 未消费的交易outputs
  • 未消费交易outputs清单更新
  • 交易有效性验证
  • 原始交易
  • 测试体验
  • 小结

概览

本章我们将引入加密货币中的交易机制。有了交易这个机制之后,我们的区块链将会从一个只有基本功能的区块链华丽转身成一个加密货币系统。 最终我们就能通过指定目标用户的地址,和对应的用户进行加密货币交易。 当然,交易之前你必须能证明你是该货币的持有者。

为了达到这个目标,我们还需要了解不少的一些概念。 比如非对称加密,签名,交易inputs(可以理解成发起者)和交易outputs(可以理解成接收者)等。

非对称加密和签名

在非对称加密中,你会有一个公钥-私钥对,其中公钥是通过私钥演绎生成,但私钥不能通过公钥演绎出来。钥如其名,公钥可以安全的分享给其他人,但是私钥只能自己保存。

任何消息都能通过私钥进行加密而生成一个签名。任何拥有对应公钥的人,都能通过该公钥将信息进行解密,进而验证该签名的有效性。

这里我们将会用到一个叫做elliptic的库来实现非对称加密,该库用的是椭圆曲线加密算法(ECDSA)。

总的来说,我们的加密货币系统中会用到两套不同的加密算法来实现不同的目的:

  • SHA256: 在工作量证明中对区块进行哈希计算,保证数据的完整性和不可篡改性。
  • 非对称加密: 用户身份验证。

私钥和公钥

一个有效的ECDSA中的私钥是一个32字节的字符串,示例如下:

19f128debc1b9122da0635954488b208b829879cf13b3d6cac5d1260c0fd967c

一个有效的公钥是由‘04’开头,紧接着64个字节的字符串,示例如下:

04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534a

公钥从私钥演绎生成。公钥将会被用作交易的接收者(即接收者地址)。

交易概览

在我们编写任何代码之前,我们先对交易的结构进行大致的了解。交易将会由两个部分组成:inputs和outputs。 outputs指定了加密货币交易的接收者(可能会有多个),inputs则是用来证明用来交易的货币是确实存在且是被交易发送者所持有的。每个交易中的inputs下的项都会指向一个已经存在的「未消费output」,本章节后面可以看到系统会拿所指向的output的地址(即代表某用户的公钥)来对本交易的签名进行解密,如果能解开,则代表该output确实属于该用户。

交易outputs

交易output(TxOut)结构由地址和数量两个成员变量组成。 数量代表了交易的虚拟货币的数量。地址就是一个ECDSA的公钥,代表接收者。意味着只有拥有对应地址的私钥的用户才能访问对应的加密货币。


class TxOut {
    public address: string;
    public amount: number;

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

交易inputs

交易input(TxIn)结构提供了加密货币的来源信息。每一个txIn都会通过txOutId指向此前的一次交易(交易id),本次交易的加密货币就是从此前的该次交易解锁出来的,解锁后的货币就能在output中被发送给接收者了。结构中的signature这个属性则是发送者用自己私钥进行加密的签名(签名数据为本次交易的id),通过用指向的前一次交易中对应output中的地址(即公钥)对该签名进行验证,即能证明该发送者是该交易及所指向的前一次交易的拥有者。

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

需要注意的是这里保存的只是通过私钥进行的签名,而不是私钥本身。在区块链的整个系统中,仅仅存在公钥和签名,而不会出现私钥。

总的来说,我们可以认为inputs解锁了对应的加密货币,而outputs重新锁定这些货币并让接收者成为新的持有者。

交易数据结构

当定义好上面的交易inputs和outputs之后,交易本身的数据结构就变得异常简单了。

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

交易id

交易id代表了一次交易的唯一性,是通过对交易数据结构中的内容做哈希计算出来的。这里要注意的是我们并没有包含发起者的签名,这个会在之后添加。

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();
};

交易签名

通过签名来保证交易内容不被修改是非常重要的。因为交易都是被公开的,任何人都能够访问所有的交易,就算这些交易还没有来得及加入到区块链当中去。

当对交易进行签名时,事实上我们只会对交易id进行签名。也就是说,参考前面的交易id的生成,只要交易中任何的一项内容发生变化,交易id都会发生变化, 然后相应的对交易id的签名也就会改变,导致整个交易无效。

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地址中发送10个币到BBB,交易id为0x555...
  • 攻击者随后将接收者地址修改成CCC, 然后把这个交易继续发送到网络中。现在这个广播内容就会变成:从AAA地址中发送10个币到CCC
  • 但是,因为交易数据中的接收者地址被修改了,对应的交易id就不再有效。新的交易id应该会改变,比如变成0x567...。 当其他用户收到该交易时,首先会验证交易id,立即就会发现这个数据被篡改了。
  • 如果攻击者修改接收地址的同时,把交易id也修改了呢?因为AAA只是对交易原始id 0x555...进行签名,并没有用私钥对0x567...进行签名, 其他节点验证时即能识破
  • 所以最终该篡改的交易都不会被其他节点接受,因为无论怎么修改,它都是无效的。

未消费的交易outputs

一笔交易中,发起者必须在input中指定还没有被消费的交易output。 你在区块链中拥有的加密货币, 指的其实就是在未消费交易outputs中,接受者地址为自己的公钥的一系列outputs。

当对交易进行有效性验证时,我们只需要关注未消费交易outputs这份清单。当然,这份独立的清单也可以从我们的区块链中演绎出来。这样子实现的话,当我们把交易纳入到区块链中时,我们将同时也会更新未消费交易outputs这份清单。

未消费交易output的数据结构大致如下所示:

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[] = [];

未消费交易outputs清单更新

每当一个新的区块加入到区块链中,我们都必须对我们的未消费outputs进行更新。 因为新的区块将可能会消费掉「未消费outputs」中的一些outputs,并肯定会引入新的outputs。

为了对此进行处理,我们需要在新区块加入时,将区块的交易数据中的的未消费交易outputs给解析出来:

 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), []);

同时,我们还要找出这个新增区块将会消耗掉哪些未交易outputs。我们通过检验交易数据的inputs下的项,即可以将这些数据找出来:

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

最终我们通过删除已经消费的并且加上新的未消费的,从而产生了新的未消费交易outputs,具体代码如下:

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

以上代码片段都是在updateUnspentTxOuts这个方法中实现的。 需要注意的是,这个方法只有在区块含有的交易数据(以及这个块本身)都被验证没有问题的时候才会被调用。

交易有效性验证

现在我们可以制定一些规则来检查交易是否有效:

  • 交易数据结构有效性验证
    对数据结构的类型等进行验证:
const isValidTransactionStructure = (transaction: Transaction) => {
        if (typeof transaction.id !== 'string') {
            console.log('transactionId missing');
            return false;
        }
        ...
       //check also the other members of class
    }
  • 交易id有效性验证
if (getTransactionId(transaction) !== transaction.id) {
        console.log('invalid tx id: ' + transaction.id);
        return false;
    }
  • inputs有效性验证

交易数据结构中的inputs中的签名必须有效,且指向的交易来源outputs必须还没有被消费掉。

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);
};
  • outputs有效性验证
    交易数据结构中outputs所指定的交易总额之和,必须与inputs中指向的交易来源的outputs总额之和一致。比如,指向的交易来源output有50个币,然后你需要给别人发送30个币,最终outputs中将会有两条记录,一条是发送30个币给对方,另外一条是发送20个币给自己,总共加起来就是50个币(当然,最终在未消费交易outputs中,交易来源的这条output将会被删除掉,而新的两个outputs将会被加进去)。
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;
    }

原始交易

如前面谈及的,交易的inputs所指向的交易来源总是来自「未消费交易outputs」, 但是,最原始的一条交易记录的inputs将会没地方指向。因为这时候还根本没有任何未消费交易outputs。为了解决这个问题,我们需要引入一个特殊的交易类型:「原始交易」

原始交易的数据结构中只会有一个output,且input不会指向任何交易来源。也就是说这种交易只是为了增加新币进行流通用的,并不是一个用户和另外一个用户进行的交易。每一次挖矿成功,都会产生一个原始交易。

我们对原始交易中产生的货币量定义为50个币:

const COINBASE_AMOUNT: number = 50;

每个区块的第一个交易记录都是原始交易,且该第一个交易中的output中指向的接收者地址都是挖出该区块的矿工的公钥。所以,原始交易可以堪称是对挖矿的一种激励机制:一旦你挖出了一个区块,你就会获得50个币的激励。

同时,我们会将区块的高度信息(可以理解为区块的序号)加入到原始交易的input当中,这样做的目的是为了保证每笔原始交易id都是不一样的。因为交易id是通过对交易的内容做哈希算出来的,多条‘给地址0x5cc发放50个币‘记录将不至于会生成同一个交易id。

对原始交易的有效性验证将会和对普通交易的有效性验证有所不同:

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;
};

测试体验

因为当前还没有引入钱包机制,手动测试将会非常困难,所以不建议现在进行测试体验。 在下一章节中实现了钱包之后,体验起来就会方便很多了。

小结

这个章节中我们将交易引入到我们的区块链中来。基本思路是很简单的:我们通过在交易数据结构中的inputs中指定「未消费outputs」来作为交易货币来源,并通过「未消费outputs」中的接收者地址来对本交易的签名进行验证,来证明该交易的发起者是该未消费outputs持有者,然后通过outputs中的接收者的地址来将该未消费outputs重新分配给指定的接收者,最终交易完成。

但是,到现在为止,去创建一笔交易还是相当的麻烦的。我们必须手动去创建交易的inputs和outputs,然后用我们的私钥去对交易进行加密签名。在我们下一章节中,我们将引入钱包机制,这些困局将会被一一击破。

本章节完整代码请查看这里

第四章

本文由天地会珠海分舵编译,转载需授权,喜欢点个赞,吐槽请评论,如能给Github上的项目给个星,将不胜感激.

原文地址:https://www.cnblogs.com/techgogogo/p/11072550.html

时间: 2024-10-31 14:29:37

第三章 自己动手写区块链之交易的相关文章

第三章 C#循环与方法

第一节1-For循环入门 语法: for(条件表达式) { 执行语句 } 练习: 第三章作业1.写一个程序打印100到200的值;2.写一个程序从10打印到1:3.写一个程序打印10到30之间的所有偶数 第二节2-变量的声明和赋值.变量的作用域 第三节For循环语法:while(条件表达式){ 执行语句}等差数列 1+N=n*(n+1)/2 第五节while循环,先判断再执行1.面试时会考到的一道题int i=0;while(i<=5){ Console.WriteLine(i++);//输出0

自己动手写处理器之第三阶段——教学版OpenMIPS处理器蓝图

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第十篇,我尽量每周四篇 从本章开始将一步一步地实现教学版OpenMIPS处理器.本章给出了教学版OpenMIPS的系统蓝图,首先介绍了系统的设计目标,其中详细说明了OpenMIPS处理器计划实现的5级流水线.3.2节给出了OpenMIPS处理器的接口示意图,及各个接口的作用.3.3节简单解释了各个源代码文件的作用.最后描述了OpenMIPS处理器的实现方法,读者将发现本书给出的实现方法与现有书籍的方法完全不同,更加易于理解.便于实践

Orange&#39;s 自己动手写操作系统 第一章 十分钟完成的操作系统 U盘启动 全记录

材料: 1 nasm:编译汇编源代码,网上很多地方有下 2  WinHex:作为windows系统中的写U盘工具,需要是正版(full version)才有写的权限,推荐:http://down.liangchan.net/WinHex_16.7.rar 步骤: 1 编译得到引导程序的机器代码.用命令行编译汇编源代码:name boot.asm -o boot.bin,其中boot.bin文件产生在命令行的当前目录中. 2 将引导程序写入到U盘引导盘的第一个扇区的第一个字节处(后),即主引导区.

【自己动手写神经网络】小白入门连载(三)--神经元的感知

[真实原创,转载务必注明出处] 上一个连载中我们已经了解了神经元模型和其工作方式.单个神经元就可以构成一个最简单的神经网络--感知机.在单层神经元感知机中,网络接收若干过输入,并通过输入函数.传输函数给出一个网络的输出.这个网络已经可以解决苹果和香蕉的分类问题.在本系列中,将具体介绍其内部原理. 首先,我们确定感知机的输入.在此,我们引入形状和颜色两个变量,苹果的形状为圆形记为1,颜色为红色记为1:香蕉的形状为弯形记为-1,颜色为黄色记为-1.则有输入p如表3.1所示. 表3.1 常用传输函数列

自己动手写CPU之第七阶段(2)——简单算术操作指令实现过程

将陆续上传本人写的新书<自己动手写CPU>,今天是第25篇,我尽量每周四篇 亚马逊的预售地址如下,欢迎大家围观呵! http://www.amazon.cn/dp/b00mqkrlg8/ref=cm_sw_r_si_dp_5kq8tb1gyhja4 China-pub的预售地址如下: http://product.china-pub.com/3804025 7.2 简单算术操作指令实现思路 虽然简单算术操作指令的数目比较多,有15条,但实现方式都是相似的,与前几章逻辑.移位操作指令的实现方式也

自己动手写处理器之第一阶段(3)——MIPS32指令集架构简介

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第四篇,我尽量每周四篇 1.4 MIPS32指令集架构简介 本书设计的处理器遵循MIPS32 Release 1架构,所以本节介绍的MIPS32指令集架构指的就是MIPS32 Release 1. 1.4.1 数据类型 指令的主要任务就是对操作数进行运算,操作数有不同的类型和长度,MIPS32提供的基本数据类型如下. 位(b):长度是1bit. 字节(Byte):长度是8bit. 半字(Half Word):长度是16bit. 字(

自己动手写处理器开篇介绍

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是开篇,我尽量每周四篇 内容简介 本书使用Verilog HDL设计实现了一款兼容MIPS32指令集架构的处理器--OpenMIPS.OpenMIPS处理器具有两个版本,分别是教学版和实践版.教学版的主要设想是尽量简单,处理器的运行情况比较理想化,与教科书相似,便于使用其进行教学.学术研究和讨论,也有助于学生理解课堂上讲授的知识.实践版的设计目标是能完成特定功能,发挥实际作用. 全书分为三部分.第一部分是理论篇,介绍了指令集架构.Ve

自己动手写CPU之第九阶段(9)——修改OpenMIPS以实现ll、sc指令

将陆续上传新书<自己动手写CPU>,今天是第48篇. 9.8 修改OpenMIPS以实现ll.sc指令 9.8.1 LLbit寄存器的实现 LLbit寄存器在LLbit模块中实现,模块接口如图9-30所示,各接口描述如表9-8所示. LLbit寄存器的代码如下,源文件是本书光盘Code\Chapter9_2目录下的LLbit_reg.v文件. module LLbit_reg( input wire clk, input wire rst, // 异常是否发生,为1表示异常发生,为0表示没有异

自己动手写CPU之第八阶段(1)——转移指令介绍

将陆续上传本人写的新书<自己动手写CPU>,今天是第34篇,我尽量每周四篇 感兴趣的朋友可以在亚马逊.当当.京东等查找. 另外,开展晒书评送书活动,在亚马逊.京东.当当三大图书网站上,发表<自己动手写CPU>书评的前十名读者,均可获赠<步步惊芯--软核处理器内部设计分析>一书,大家踊跃参与吧!活动时间:2014-9-11至2014-10-20 本章将为OpenMIPS处理器添加转移指令,转移指令包括跳转.分支两种,区别在于前者是绝对转移,后者是相对转移,但实现方法是相似