在区块链应用中,最重要的就是所谓的交易。通俗地说,交易就是将比特币或某种数字货币从一个人转移到另一个人。从数据结构上看,一个事务包含4个组成部分,分别是版本、输入、输出和锁定时间。该版本用于确定交易可以使用哪些附加功能。输入是一个复杂的概念,后面会解释。输出对应接收方,锁定时间对应交易有效期。让我们先从代码定义事务:fromEllipticCurvesimporthash256classTransation:def__init__(self,version,inputs,outputs,lock_time,test_net=False):self.version=versionself.inputs=inputsself.outputs=outputsself。lock_time=lock_timeself.test_net=test_netdef__repr__(self):inputs=''forinputinself.inputs:inputs+=input.__repr__()+'\n'outputs=''foroutputinself.outputs:输出+=output.__repr__()+"\n"returnf"tx:{self.id()}\nversion:{self.version}\ninputs:{inputs}\noutputs:{outputs}\nlock_time:{self.lock_time}"defid(self):returnself.hash().hex()defhash(self):returnhash256(self.serialize())[::-1]上面代码只定义了事务对应的4同时,还有一个函数还没有实现,那就是serialize(),它的作用是序列化Transation类的实例,后面我们会处理。让我们仔细看看每个字段的含义。version字段用于表示当前事务可以支持哪些功能。例如Windows操作系统有不同的版本,对应的版本有不同的功能或API。知道操作系统的版本,就可以知道系统是否有特定的功能或接口可以调用,事务版本也是类似的目的。通常交易的版本号是1,在某些特定情况下会是2。从代码来看,inputs是一个包含多个元素的数组。这些元素中的每一个都类似于一个指针,指向上一个交易的输出。如果输出的意思是我们向别人支付数字货币,那么首先我们需要从别人或某处获得相应的货币,毕竟你必须有钱才能花。对于比特币应用来说,每一次输入对应两点,一是表明你之前收到的货币,二是证明该货币确实属于你。第二部分对应上一节描述的ECDSA,即我们需要使用自己的私钥生成数字签名,证明自己拥有相应的币种。输入字段之所以要对应一个数组,是因为某个币值可能会通过多次支付来花费。比如你有100元,你可能用20买早餐,20买午餐,60买晚餐,对应3个输入,或者一顿饭吃100元,那么对应1个输入.从二进制数据的角度来看,版本字段后面的输入字段需要分多部分进行解释。版本字段固定为1个字节,后跟可变数量的字节以指示输入的数量。为了节省篇幅,version字段后面用来表示输入数量的信息遵循以下解释规则:1.如果输入数量小于253,则用一个字节来表示。2.如果输入个数在253到2^16-1之间,即输入个数可以用2个字节表示。然后version字段后面是值253,占一个字节,然后两个字节用来表示输入的个数。3.如果输入个数在2^16到2^32-1之间,即输入个数需要用4个字节来表示。然后值254后面是version字段,然后用4个字节表示输入数量。3、如果输入的个数在2^32和2^64-1之间,即输入的个数需要用8个字节来表示,那么取值255后面是version字段,然后是8个字节用来表示输入量。只有通过具体的代码才能更好的理解上面说的:defread_variant(s):#我们假设S是一个Stream类型的对象,它支持接口read读取每个字节#s.read(i)的意思是从输入流读取文件当前开头的第i个字符,read的作用与文件读取的作用相同i=s.read(1)[0]#firstpass1byte,即version字段,然后读取以下version字段后的1个字节ifi==0xfd:#如果字节值为253,则读取接下来的2个字节以获得输入编号returnlittle_endian_to_int(s.read(2))elifi==0xfe:#如果字节为254,则表示接下来的4个字节用来表示输入的数量returnlittle_endian_to_int(s.read(4))elifi==0xff:#如果this的值byte为255,表示接收读取8个字节代表输入数量returnlittle_endian_to_int(s.read(8))else:returni#输入数量小于253,直接读取值这个字节代表输入的数量defencode_variant(i):#i代表输入的个数,在这里编码ifi<0xfd:#如果值小于253,直接写入交易数据returnbytes([i])elifi<0x1000:#0x1000对于2^16,如果取值在253和2^16之间-1,则后面跟一个取值254,然后用两个字节编码ireturnb'\xfd'+int_to_little_endian(i,2)elifi<0x100000000:#0x100000000for2^32,编码时先设置值254,然后用4个字节编码ireturnb'\xfe'+int_to_little_endian(i,4)elifi<0x10000000000000000:#对于2^64,先设置值255,然后使用8个字符编码ireturnb'\ff'+int_to_little_endian(i,8)else:raiseValueError('integertoolarge:{}'.format(i))通过上面的代码,或许我们可以更好的理解所描述的编码规则上面知道了输入的个数,我们就可以解析输入的数据结构了。输入由4部分组成:最后一笔交易的ID、最后一笔交易的索引、ScriptSig和Squence。后两者不好翻译成中文,后面我们会用代码来解释。最后一笔交易ID其实就是最后一笔交易数据经过二进制序列化后计算hash256的结果。因此,该字段的长度固定为32字节,最后的交易索引用4字节表示,均采用小端编码。ScriptSig与比特币智能合约的脚本语言有关。这是一个变长字段,sequence是一个固定的4字节字段。sequence和lock_time这两个字段最初是中本聪用来实现“高频交易”的。意思是如果小明给了小华x个比特币作为报酬,因为小明帮了小华一个忙,小华要求付给小明y个比特币作为报酬。如果x>y,那么小明可以直接付给小花x-y个比特币。没有必要让小明先付给小花x个比特币,然后小花给小明付y个比特币比特币,也就是中本聪希望把多笔交易组合成一笔交易,但是这个思路漏洞严重,所以不能用在比特币。我们看看代码中是如何定义交易输入的:script_sig=Script()#先不考虑它的具体内容else:self.script_sig=script_sigself.sequence=sequencedef__repr__(self):returnf"{self.prev_tx.hex()}:{self.prev_index}"@classmethoddefparse(cls,s):prev_tx=s.read(32)[::-1]#固定32字节,因为对应sha256hashprev_index=little_endian_to_int(s.read(4))#固定4字节script_sig=Script.parse(s)#我们暂时忽略它,因为我们不知道Script是什么在上面的代码中,我们有一个我们还没有理解的Script对象。我们假设它已经存在,我们可以给出输入解析逻辑代码。输入解析后,还可以实现交易的解析:classTransation:@classmethoddefparse(cls,s,testnet=False):version=little_endian_to_int(s.read(4))num_inputs=read_variant(s)inputs=[]for_inrange(num_inputs):inputs.append(TxIn.parse(s))returncls(version,inputs,None,None,testnet=testnet)然后我们看输出field,输出表示谁将得到这笔交易的比特币输出。输出也是多个对象,因为一笔交易可能需要支付给多方。输出对象有两个字段,分别是amount和ScriptPubKey。金额是要支付的比特币数量。它的单位是1/100,000,000比特币,这个字段占8个字节。ScriptPubKey也和比特币的智能合约脚本有关。它可以看作是ATM机的钥匙,每个人都可以往里面存钱,只有有钥匙的人才能打开取款。该字段的含义需要在后面的章节中理解。它也是一个可变长度字段。我们需要先解析几个字节,得到它的具体长度,才能得到它的二进制内容。我们先从代码上简化一下。的定义,让这个概念变得更加具体:self.script_pubkey}'@classmethoddefparse(cls,s):#s对应输出二进制流amount=little_endian_to_int(s.read(8))script_pubkey=Script.parse(s)#暂时忽略returncls(amount,script_pubkey)现在我们在交易对象中也加入了输出的分析:Transation:@classmethoddefparse(cls,s,testnet=False):version=little_endian_to_int(s.read(4))num_inputs=read_variant(s)inputs=[]for_inrange(num_inputs):inputs.append(TxIn.parse(s))outputs=[]num_outputs=read_variant(s)for_inrange(num_outputs):outputs.append(TxOut.parse(s))返回cls(版本、输入、输出、无、测试网=测试网)接下来自然就是lock_time字段的分析了。该字段根据其值有两种不同的解释。如果其值小于500,000,000,则表示公链的区块数。例如lock_time=600,000,表示交易必须等到公链上出现第600,001个区块后才会生效,如果大于500,000,000,则表示unix时间戳。这里需要注意的是,如果所有输入对象中的sequence取值为ffffffff,则会被忽略,因为它对应的是4个字节,所以在交易对象的解析中,读取二进制流的最后4个字节即可输出:转换:@classmethoddefparse(cls,s,testnet=False):version=little_endian_to_int(s.read(4))num_inputs=read_variant(s)inputs=[]for_inrange(num_inputs):inputs.append(TxIn.parse(s))outputs=[]num_outputs=read_variant(s)for_inrange(num_outputs):outputs.append(TxOut.parse(s))lock_time=little_endian_to_int(s.read(4))#Thelast4bytesarelocktimereturncls(version,inputs,outputs,lock_time,testnet=testnet)我们来看输入和输出的序列化操作,就是我们前面介绍的解析操作的逆操作:classTxOut:defserialize(self):defserialize(self):#序列化输出result=int_to_little_endian(self.amount,8)result+=self.script_pubkey.serialize()#这里我们不关心classTxIn:defserialize(self):result=self.prev_tx[::-1]result+=int_to_little_endian(self.prev_index,4)result+=self.script_sig.serialize()#暂时忽略其实现细节result+=int_to_little_endian(self.sequence,4)classTransation:defserialize(self):result=little_endian_to_int(self.version,4)result+=encode_variant(len(self.inputs))fortx_ininself.inputs:result+=tx_in.serialize()result+=encode_variant(len(self.outputs))fortx_outinself.outputs:result+=tx_out.serialize()result+=int_to_little_endian(self.lock_time,4)对于一笔交易,输入的收入和输出对应的是支付。比特币规定输入必须大于等于输出,多出的部分作为对矿工的奖励或奖励输入和输出的差值也叫交易手续费,但是问题是我们上面的代码定义中,只有输出有金额,输入没有,所以需要去公链上找对应的金额到输入,但我们不需要进入公链。因为比特币的模拟链有很多,也就是有人开发了类似比特币的区块链。这些链主要用于测试,因此也被称为比特币测试链。让我们看看如何进入给定的测试链:else:return'http://mainnet.programmingbitcoin.com'@classmethoddeffetch(cls,tx_id,testnet=False,fresh=False):如果新鲜或(tx_idnotincls.cache):url='{}/tx/{}.hex'.format(cls.get_url(testnet),tx_id)response=requests.get(url)try:raw=bytes.fromhex(response.text.strip())exceptValueError:raiseValueError("不正确的响应:{}".format(response.text))ifraw[4]==0:raw=raw[:4]+raw[6:]tx=Transation.parse(BytesIO(raw),test_net=测试网)tx.lock_time=little_endian_to_int(raw[-4:])else:tx=Transation.parse(BytesIO(raw),test_net=testnet)iftx.id()!=tx_id:raiseValueError(f"ids不同:{tx.id()}with{tx_id}")cls.cache[tx_id]=txcls.cache[tx_id].test_net=testnetreturncls.cache[tx_id]上面的代码还不能运行,因为我们还有脚本对象在没有实现的情况下模拟另一个以太坊区块链并没有那么复杂。市面上有大量的测试链,有时间我们自己实现一个。有了测试链,我们可以让输入对象从测试链读取交易的输入金额:=testnet)defvalue(self,testnet=False):tx=self.fetch_tx(testnet=testnet)#本次交易的输入等于上次交易的输出returntx.tx_outs[self.prev_index].amo`在这里插入代码片段`unt目前的代码还不能运行,但是我们可以通过代码更好的理解相应的概念。在下一节中,我们可以在处理完Script对象后让当前代码运行。代码地址:https://github.com/wycl16514/python_blockchain_transation.git
