当前位置: 首页 > 后端技术 > Node.js

Node.js和比特币中的utxo模型

时间:2023-04-03 11:29:54 Node.js

BTCBTC引入了许多创新的概念和技术。区块链、PoW共识、RSA加密、智能合约等处于萌芽阶段的术语经常被业内人士提及,而且,这些创新的实现确实让BTC变成了一个具有可靠性和安全性保障的封闭生态系统,但在这个BTC生态系统,如果没有区块链模式的转账模块,那么货币的流通属性也就无从谈起。实现转账交易模块,“是否使用传统账户模型实现交易;交易信息如何存储在区块链上,如何实现信息压缩;交易信息如何验证;系统最大并发交易量”和其他问题确实值得思考。BTC将这些一一解决。它摒弃了传统的基于账户的交易模型,而是采用基于区块链存储的utxo(未花费交易输出)模型。笔者尝试分析为什么不用传统的账户模型:BTC的存储单位是区块链,而区块链的数据结构本质上是一个单向链表。不是传统的关系型数据库,不可能新建一个账表来存储压力。如果采用传统的方式,随着时间的推移,账户表会不断增长,这会给后续的表分片和备份带来很大的困难,容易导致隐私泄露。account表中的信息会很直观的暴露余额等敏感信息,但是utxo模型非常巧妙的避免了这一点。utxo模型下实现的每一笔交易都不需要显式提供转账地址和接收地址(没有账户,也不需要提供地址),只需要提供交易的交易输入和交易输出,以及什么是交易输入和交易输出?交易输入指向一个交易输出,“这个交易输出可以被转账方消费,所以这个交易输出也叫utxo(unspenttransactionoutput)”,其中包括“某笔交易,指向这个的一个索引值交易的可用交易输出和解锁脚本”。该解锁脚本用于验证提供解锁脚本的人是否可以使用可用的消费输出。交易输出是一个存储BTC“余额”的数据结构,大致包括两部分:BTC数量和锁定脚本。BTC的数量可以理解为余额,代表交易的结果;而锁定脚本是通过一定的算法来锁定BTC余额,直到有人可以提供数据密钥来解锁脚本,BTC数量才会被锁定。被人消费。从这个角度来看,一笔交易会包含若干笔交易输入,同时会产生若干笔交易输出。这些交易输入会指向前一笔交易未被消费的输出(utxo),并提供自己的解锁脚本来证明这些utxo中的BTC属于转账方;同时,转账产生的所有交易输出都会用对应方的公钥进行加密(这里为了更好理解理解为公钥加密,本质上是一个公钥哈希,即一串reversebase58encodedbtcaddresses),锁定这些交易输出,并等待交易输入解锁脚本解锁。因此,BTC没有账户的概念,所有的“余额”都在区块链上,只是这些余额已经加密,只有提供私钥和签名的人才能使用对应的utxo余额,所以这就是为什么BTC的原因持有者必须保留自己的私钥。UTXO的node.js实现交易输入exportclassInput{privatetxId:string;私有输出索引:数字;私人解锁脚本:字符串;publicget$txId():string{返回this.txId;}publicset$txId(value:string){this.txId=value;}publicget$outputIndex():number{returnthis.outputIndex;}publicset$outputIndex(value:number){this.outputIndex=value;}publicget$unlockScript():string{returnthis.unlockScript;}publicset$unlockScript(value:string){this.unlockScript=value;}constructor(txId:string,index:number,unlockScript:string){this.txId=txId;this.outputIndex=索引;this.unlockScript=unlockScript;}//反序列表化,进行类型转换publicstaticcreateInputsFromUnserialize(objs:Array){letins=[];objs.forEach((obj)=>{ins.push(新输入(obj.txId,obj.outputIndex,obj.解锁脚本));});返回插件;}canUnlock(privateKey:string):boolean{if(privateKey==this.unlockScript){returntrue;}else{返回错误;}}}私有属性txId标识“一个可用的utxo所属的交易”是一串sha256编码的字符串;outputIndex表示“可用的utxo对应交易的序号值”;unlockScript即解锁脚本,并没有完全按照BTC的原型实现,只是简单的验证用户的私钥来实现鉴权,原则上还是沿用了BTC的思路交易输出import*asrsaConfigfrom'../../rsa.json';exportclassOutput{privatevalue:number;//锁定脚本,需要使用UTXO所有者用私钥签名Pass//当解锁UTXO成功后,这个UTXO成为下一笔交易的交易输入,同时锁定本次交易的交易输出usingreceiver'saddress(publickey),//等待接收方使用私钥签名来使用UTXO//所以没有btc账户的概念所有“钱”的概念都是自己公钥加密保存key,只有自己的私钥才能使用这笔钱(即解锁UTXO的解锁脚本)privatelockScript:string;//该属性仅在交易期间使用,设置属性privatetxId:string;//该属性仅在交易中使用,设置属性privateindex:number;publicget$index():number{returnthis.index;}publicset$index(value:number){this.index=value;}publicget$txId():string{returnthis.txId;}publicset$txId(value:string){this.txId=value;}publicget$value():number{returnthis.value;}publicset$value(value:number){this.value=value;}/*publicget$lockScript():string{returnthis.lockScript;}publicset$lockScript(value:string){这个。锁定脚本=价值;}*/constructor(value:number,publicKey:string){this.value=value;this.lockScript=publicKey;}//反序列化并进行类型转换publicstaticcreateOnputsFromUnserialize(objs:Array){letouts=[];objs.forEach((obj)=>{outs.push(新输出(obj.value,obj.lockScript));});退货;}publiccanUnlock(privateKey:string):boolean{if(privateKey==rsaConfig[this.lockScript]){返回真;}else{返回错误;}}}交易输出中的value属性标识当前utxo余额,即BTC数量;lockScript属性是锁定脚本,在我们简单的实现中是接收方的公钥,不是BTC中的逆波兰式,但是大体原理是一样的,都需要提供私钥来解密一个交易一笔交易包括若干笔交易输入,交易输出还提供了一个唯一标识本笔交易的txId。从结构的角度来看,它看起来像这样:exportclassTransaction{privatetxId:string;私有inputTxs:数组<输入>;私有outputTxs:Array;构造函数(txId:字符串,输入:Array,输出:Array){this.txId=txId;this.inputTxs=输入;this.outputTxs=输出;}publicget$txId():string{returnthis.txId;}publicset$txId(value:string){这个。txId=值;}publicget$inputTxs():Array{returnthis.inputTxs;}publicset$inputTxs(value:Array){this.inputTxs=value;}publicget$outputTxs():Array{returnthis.outputTxs;}publicset$outputTxs(value:Array){this.outputTxs=value;}/*1.将交易结构的各个字段序列化为字节数组2.将字节数组拼接成支付字符串3.对支付字符串进行两次SHA256计算得到交易哈希*/publicsetTxId(){让sha256=crypto.createHash('sha256');sha256.update(JSON.stringify(this.inputTxs)+JSON.stringify(this.outputTxs)+Date.now(),'utf8');this.txId=sha256.digest('hex');其中,txId的计算并不是严格按照BTC的实现来计算的。相反,它只是执行对象序列化以执行sha256coinbase事务。我们都知道需要挖矿才能获得比特币。其实挖矿也是一种交易,只不过是一种不确定交易输入的交易,也叫coinbase交易。coinbase交易会存在于每个区块中,其总金额包括系统对矿工打包交易过程的奖励以及其他转账方提供的手续费,如下图所示:因此也非常容易创建acoinbasetransaction//coinbasetransaction用于奖励矿工,输入为空,输出为矿工奖励publicstaticcreateCoinbaseTx(pubKey:string,info:string){letinput=newInput('',-1,info);让output=newOutput(AWARD,pubKey);lettx=newTransaction('',[输入],[输出])tx.setTxId();返回交易;}在我们的实现中,我们只需要提供锁定utxo的公钥和一串描述字符串即可。是的,最后设置交易的txId就完成了coinbase交易的创建。还提供了一种识别coinbase交易的方法:publicstaticisCoinbaseTx(tx:Transaction){if(tx.$inputTxs.length==1&&tx.$inputTxs[0].$outputIndex==-1&&tx.$inputTxs[0].$txId==''){返回真;}else{返回错误;}}至此,coinbase交易完成。这是最简单的交易,不涉及转账方,即交易输入。在转账交易中使用BTC无法避免转账。utxo模型中转账交易的实现是在某个区块上增加一个Transaction。每笔交易都需要交易输入和交易输出。因此,在BTC中,转账的核心是找到转账方的utxo进行消费,同时将指定数量的BTC转入指定的消费输出。如果还有剩余,找零自己消费产出。//创建转账交易publicstaticcreateTransaction(from:string,fromPubkey:string,fromKey:string,to:string,toPubkey:string,coin:number){letoutputs=this.findUTXOToTransfer(fromKey,coin);console.log(`UTXOToTransfer:${JSON.stringify(outputs)},from:${from}to${to}transfer${coin}`)letinputTx=[],sum=0,outputTx=[];outputs.forEach((o)=>{sum+=o.$value;inputTx.push(newInput(o.$txId,o.$index,fromKey));});if(sumcoin){outputTx.push(newOutput(sum-coin,fromPubkey));}lettx=newTransaction('',inputTx,outputTx);tx.setTxId();返回交易;}创建交易需要提供转账方地址(公钥哈希)、转账方公钥和私钥、接收方地址、接收方公钥、金额比特币转账。本次交易是由转账发起的,因此需要提供转账方的私钥来解锁脚本。首先使用findUTXOToTransfer找到满足转账金额的可用utxo。需要提供转账方私钥和转账金额;然后根据获取到的可用utxo创建对应的交易输入;然后用接收方的公钥加密交易输出,同时如果有余额,就换成自己的,用自己的公钥加密;最后,根据得到的交易输入和交易输出,创建交易,计算出txId,加入区块(我们的demo是在单机模拟下,多播没有实现),等待挖矿。转账的核心是findUTXOToTransfer。在findUTXOToTransfer中,通过调用getAllUnspentOutputTx获取所有可用的utxo,筛选出满足给定BTC数量的utxo。publicstaticgetAllUnspentOutputTx(secreteKey:string):Array{letoutputIndexHash:Object=this.getAllSpentOutput(secreteKey);让unspentOutputsTx=[];让keys=Object.keys(outputIndexHash);letblock=BlockDao.getSingletonInstance().getBlock(chain.$lastACKHash);while(block&&blockinstanceofBlock){block.$txs&&block.$txs.forEach((tx)=>{if(keys.includes(tx.$txId)){tx.$outputTxs.forEach((输出,i)=>{//经过过滤的输出if(i==outputIndexHash[tx.$txId])return;if(output.canUnlock(secreteKey)){unspentOutputsTx.push(tx);}});}else{for(leti=0,len=tx.$outputTxs.length;i