首页 文章 精选 留言 我的

精选列表

搜索[Vite构建],共10000篇文章
优秀的个人博客,低调大师

Python爬虫(二)——豆瓣图书决策树构建

前文参考:https://www.cnblogs.com/LexMoon/p/douban1.html Matplotlib绘制决策树代码: 1 # coding=utf-8 2 import matplotlib.pyplot as plt 3 4 decisionNode = dict(boxstyle='sawtooth', fc='10') 5 leafNode = dict(boxstyle='round4',fc='0.8') 6 arrow_args = dict(arrowstyle='<-') 7 8 9 10 def plotNode(nodeTxt, centerPt, parentPt, nodeType): 11 createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',\ 12 xytext=centerPt,textcoords='axes fraction',\ 13 va='center', ha='center',bbox=nodeType,arrowprops\ 14 =arrow_args) 15 16 17 def getNumLeafs(myTree): 18 numLeafs = 0 19 firstStr = list(myTree.keys())[0] 20 secondDict = myTree[firstStr] 21 for key in secondDict: 22 if(type(secondDict[key]).__name__ == 'dict'): 23 numLeafs += getNumLeafs(secondDict[key]) 24 else: 25 numLeafs += 1 26 return numLeafs 27 28 def getTreeDepth(myTree): 29 maxDepth = 0 30 firstStr = list(myTree.keys())[0] 31 secondDict = myTree[firstStr] 32 for key in secondDict: 33 if(type(secondDict[key]).__name__ == 'dict'): 34 thisDepth = 1+getTreeDepth((secondDict[key])) 35 else: 36 thisDepth = 1 37 if thisDepth > maxDepth: maxDepth = thisDepth 38 return maxDepth 39 40 def retrieveTree(i): 41 #预先设置树的信息 42 listOfTree = [{'no surfacing':{0:'no',1:{'flipper':{0:'no',1:'yes'}}}}, 43 {'no surfacing':{0:'no',1:{'flipper':{0:{'head':{0:'no',1:'yes'}},1:'no'}}}}, 44 {'Comment score greater than 8.0':{0:{'Comment score greater than 9.5':{0:'Yes',1:{'More than 45,000 people commented': { 45 0: 'Yes',1: 'No'}}}},1:'No'}}] 46 return listOfTree[i] 47 48 def createPlot(inTree): 49 fig = plt.figure(1,facecolor='white') 50 fig.clf() 51 axprops = dict(xticks = [], yticks=[]) 52 createPlot.ax1 = plt.subplot(111,frameon = False,**axprops) 53 plotTree.totalW = float(getNumLeafs(inTree)) 54 plotTree.totalD = float(getTreeDepth(inTree)) 55 plotTree.xOff = -0.5/plotTree.totalW;plotTree.yOff = 1.0 56 plotTree(inTree,(0.5,1.0), '') 57 plt.title('Douban reading Decision Tree\n') 58 plt.show() 59 60 def plotMidText(cntrPt, parentPt,txtString): 61 xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] 62 yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1] 63 createPlot.ax1.text(xMid, yMid, txtString) 64 65 def plotTree(myTree, parentPt, nodeTxt): 66 numLeafs = getNumLeafs(myTree) 67 depth = getTreeDepth(myTree) 68 firstStr = list(myTree.keys())[0] 69 cntrPt = (plotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,\ 70 plotTree.yOff) 71 plotMidText(cntrPt,parentPt,nodeTxt) 72 plotNode(firstStr,cntrPt,parentPt,decisionNode) 73 secondDict = myTree[firstStr] 74 plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD 75 for key in secondDict: 76 if type(secondDict[key]).__name__ == 'dict': 77 plotTree(secondDict[key],cntrPt,str(key)) 78 else: 79 plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW 80 plotNode(secondDict[key],(plotTree.xOff,plotTree.yOff),\ 81 cntrPt,leafNode) 82 plotMidText((plotTree.xOff,plotTree.yOff),cntrPt,str(key)) 83 plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD 84 85 if __name__ == '__main__': 86 myTree = retrieveTree(2) 87 createPlot(myTree) 运行结果:

优秀的个人博客,低调大师

《高性能Linux服务器构建实战》笔记

系统安全、故障排查、自动化运维、集群架构 1、账号安全,安装完后可以删除的账号: adm,lp,sync,shutdown,halt,news,uucp,operator,games,gopher 禁用登录功能:usermod -s /sbin/nologin nagios 2、关闭不需要的服务: anacron auditd autofs avahi-daemon avahi-dnsconfd bluetooth cpuspeed firstboot gpm haldaemon hidd ip6tables ipsec isdn lpd mcstrans messagebus netfs nfs nfslock nscd pcscd portmap readahead_early restorecond rpcgssd rpcidmapd rstatd sendmail setroubleshoot yppasswdd ypserv 系统必须运行的服务:

优秀的个人博客,低调大师

1.kotlin安卓实践课程-构建application

简介 主要会通过安卓实战来讲解kotlin语法和实际应用,本教程设及知识点包括框架模式mvp+mvvm, Databinding(数据绑定框架),Dagger2(依赖注入框架),DeepLink(页面路由框架),Rxjava,RxAndroid(异步操作框架),Retrofit,Okhtttp等,不过本教程重点在kotlin所以这些框架需要了解可自行百度。 针对人群:Android中级以上,kotlin入门 正式开始 都是程序员,直接看代码 /** * create by bigman * 知识点: 继承 ,init ,lateinit */ class App : Application() { //因为kotlin中的类定义同时也是构造函数,这个时候是不能进行操作的, // 所以kotlin增加了一个新的关键字init用来处理类的初始化问题,init模块中的内容可以直接使用构造函数的参数。 init { instance = this } //加入注解,标注这个apiComponent是需要注入的 //lateinit 则用于只能生命周期流程中进行获取或者初始化的变量,比如 Android 的 onCreate() @Inject lateinit var apiComponent: ApiComponent override fun onCreate() { super.onCreate() //使用组件进行构造,component注射器注入 DaggerApiComponent.builder() .apiModule(ApiModule()) .appModule(AppModule(this)) .build() .inject(this) } //Companion Object中定义的成员类似于Java中的静态成员,因为Kotlin中没有static成员 companion object { lateinit var instance: App } } 语法点1:kotlin继承 class App : Application() {} kotlin的继承就是在类名后直接调用父类的构造方法,这里的我的的应用入口对象App继承于父类Application 语法点2:kotlin init函数 //因为kotlin中的类定义同时也是构造函数,这个时候是不能进行操作的, // 所以kotlin增加了一个新的关键字init用来处理类的初始化问题,init模块中的内容可以直接使用构造函数的参数。 init { instance = this } 上面代码注释已经说明,因为kotlin的类在定义的同时调用了主构造函数,这个时候不能进行任何操作,所以增加了一个init方法可以对构造函数传入的一些参数进行一些初始化操作,比如我们这里是把当前对象赋值给了instance变量 语法点3:kotlin lateinit 语法点4:定义常量与变量var,val和变量类型ApiComponent //加入注解,标注这个apiComponent是需要注入的 //lateinit 则用于只能生命周期流程中进行获取或者初始化的变量,比如 Android 的 onCreate() @Inject lateinit var apiComponent: ApiComponent lateinit可以延迟加载变量,对于非空类型在声明的时候不能赋值的,可以用lateinit修饰,这样可以有效避免空指针,对比学习!!这个符号,这个符号表示当变量为空时会强制抛出空指针,这样显然不是我们想见到的,因为用户是不管你空指针的,他们只知道你的程序异常了。 var用于定义变量,val用于定义常量,代码中apiComponent是一个ApiComponent的变量,:这里的冒号和继承区分一下 语法点5:函数声明 override fun onCreate() { super.onCreate() } 函数定义使用关键字 fun,参数格式为:参数 : 类型注意:kotlin语句不需要分号结束 语法点6:新建对象 //使用组件进行构造,component注射器注入 DaggerApiComponent.builder() .apiModule(ApiModule()) .appModule(AppModule(this)) .build() .inject(this) kotlin新建对象不需要像java那样调用new,直接调用构造方法就行,比如这里的ApiModule()和AppModule(this) 语法点7:伴生对象 //Companion Object中定义的成员类似于Java中的静态成员,因为Kotlin中没有static成员 companion object { lateinit var instance: App } Companion Object中定义的成员类似于Java中的静态成员,因为Kotlin中没有static成员,可用来定义一些常量或者工具方法 欢迎一起交流,有问题加群交流 kotlin实战交流群群二维码.png

优秀的个人博客,低调大师

构建你的Office 365开发环境 - 其他版本

博客地址:http://blog.csdn.net/FoxDave本文介绍如何搭建Android版、Angular和ASP.NET版本的Office 365开发环境,其跟IOS版的区别主要在第一部分下载安装开发工具上,Office 365端的配置都是一样的。 Android 对于安卓开发你有两个选择,Android Studio或者Android Developer Tools(包含Eclipse)。这两个其实是类似的,但是Android Studio通过Gradle支持管理依赖库,但是你必须在Eclipse中手动处理。 Android Studio 访问https://developer.android.com/sdk/index.html去按照说明下载和安装Android Studio。 在需要依赖库时,可以参照向Android Studio中添加依赖库的介绍去向你的项目添加依赖库。 Android Developer Tools 访问http://developer.android.com/tools/help/adt.html去按照说明下载和安装Android Developer Tools。 在需要依赖库时,可以参照向Eclipse中添加依赖库的介绍去向你的项目添加依赖库。 Angular 你可以在很多IDE中编写Angular应用程序,由于目前没有针对Office 365的编程智能提示,尽管用你喜欢的一款。你不需要其他工具去使用Office 365 API开发Angular应用程序。但是需要安装一些套件,包括Git和Node.js。 ASP.NET Office Developer Tools for Visual Studio包含了客户端库和UI加强组件。客户端库使你更容易跟Office 365 REST API进行交互。Visual Studio UI加强组件使你能够轻松添加Office 365服务到你的应用。安装地址:http://aka.ms/officedevtoolsatvscom

优秀的个人博客,低调大师

基于Java语言构建区块链(五)—— 地址(钱包)

最终内容请以原文为准:https://wangwei.one/posts/f9088e0f.html 引言 在 上一篇文章当中,我们开始了交易机制的实现。你已经了解到交易的一些非个人特征:没有用户账户,您的个人数据(例如:姓名、护照号码以及SSN(美国社会安全卡(Social Security Card)上的9 位数字))不是必需的,并且不存储在比特币的任何地方。但仍然必须有一些东西能够识别你是这些交易输出的所有者(例如:锁定在这些输出上的币的所有者)。这就是比特币地址的作用所在。到目前为止,我们只是使用了任意的用户定义的字符串当做地址,现在是时候来实现真正的地址了,就像它们在比特币中实现的一样。 比特币地址 这里有一个比特币地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是一个非常早期的比特币地址,据称是属于中本聪的比特币地址。比特币地址是公开的。如果你想要给某人发送比特币,你需要知道对方的比特币地址。但是地址(尽管它是唯一的)并不能作为你是一个钱包所有者的凭证。事实上,这样的地址是公钥的一种可读性更好的表示 。在比特币中,你的身份是存储在你计算机上(或存储在你有权访问的其他位置)的一对(或多对)私钥和公钥。比特币依靠加密算法的组合来创建这些密钥,并保证世界上没有其他人任何人可以在没有物理访问密钥的情况下访问您的比特币。 比特币地址与公钥不同。比特币地址是由公钥经过单向的哈希函数生成的 接下来,让我们来讨论一下这些加密算法。 注意:不要向本篇文章中的代码所生成的任何比特币地址发送真实的比特币来进行测试,否则后果自负…… 公钥密码学 公钥加密算法(public-key cryptography)使用的是密钥对:公钥和私钥。公钥属于非敏感信息,可以向任何人透露。相比之下,私钥不能公开披露:除了所有者之外,任何人都不能拥有私钥的权限,因为它是用作所有者标识的私钥。你的私钥代表就是你(当然是在加密货币世界里的)。 本质上,比特币钱包就是一对这样的密钥。当你安装一个钱包应用程序或者使用比特币客户端去生成一个新的地址时,它们就为你创建好了一个密钥对。在比特币种,谁控制了私钥,谁就掌握了所有发往对应公钥地址上所有比特币的控制权。 私钥和公钥只是随机的字节序列,因此它们不能被打印在屏幕上供人读取。这就是为什么比特币会用一种算法将公钥的字节序列转化为人类可读的字符串形式。 如果你曾今使用过比特币钱包的应用程序,它可能会为你生成助记词密码短语。这些助记词可以用来替代私钥,并且能够生成私钥。这种机制是通过 BIP-039 来实现的。 好了,现在我们已经知道在比特币中由什么来决定用户的标识了。但是,比特币是如何校验交易输出(和它里面存储的一些币)的所有权的呢? 数字签名 在数学和密码学中,有个数字签名的概念,这套算法保证了以下几点: 保证数据从发送端传递到接收端的过程中不会被篡改; 数据由某个发送者创建; 发送者不能否认发送的数据; 通过对数据应用签名算法(即签署数据),可以得到一个签名,以后可以对其进行验证。数字签名需要使用私钥,而验证则需要公钥。 为了能够签署数据我们需要: 用于被签名的数据; 私钥。 签名操作会产生一个存储在交易输入中的签名。为了能够验证一个签名,我们需要: 签名之后的数据; 签名; 公钥。 简单来讲,这个验证的过程可以被描述为:检查签名是由被签名数据加上私钥得来,并且这个公钥也是由该私钥生成。 数字签名并不是一种加密方法,你无法从签名反向构造出源数据。这个和我们 前面 提到过的Hash算法有点类似:通过对一个数据使用Hash算法,你可以得到该数据的唯一表示。它们两者的不同之处在于,签名算法多了一个密钥对:它让数字签名得以验证成为可能。 但是密钥对也能够用于去加密数据:私钥用于加密数据,公钥用于解密数据。不过比特币并没有使用加密算法。 在比特币中,每一笔交易输入都会被该笔交易的创建者进行签名。比特币中的每一笔交易在放入区块之前都必须得到验证。验证的意思就是: 检查交易输入是否拥有引用前一笔交易中交易输出的权限 检查交易的签名是否正确 数据签名以及签名验证的过程如下图所示: 让我们来回顾一下交易的完整生命周期: 最开始,会有一个包含了Coinbase交易的创世区块。由于在Coinbase交易中没有真正的交易输入,所以它不需要签名。Coinbase交易的交易输出会包含一个Hashing之后的公钥(使用的算法为 RIPEMD16(SHA256(PubKey)) ) 当一个人发送比特币时,会创建一笔交易。这笔交易的交易输入会引用前一笔或多笔交易的交易输出。每一个交易输入将会存储未经Hashing处理的公钥以及整个交易的签名信息。 当比特币网络中的其他节点收到其他节点广播的交易数据之后将,将会对其进行验证。其他的事情除外,他们将会验证: 检查交易输入中公钥的Hash值是否与它所引用的交易输出的Hash值想匹配,这是确保发送方只能发送属于他们自己的比特币。 检查签名是否正确,这是为了确保这笔交易是由比特币的真正所有者创建的。 当一个矿工准备开始开采一个新的区块时,他会将交易信息放入区块中,然后开始挖矿。 当一个区块完成挖矿之后,网络中的其他节点将会收到一条区块已挖矿完毕的消息,并且他们会把这个区块添加到区块链中去。 当一个区块被添加到区块链之后,就标志着这笔交易已经完成,它所产生的交易输出将会在新的交易中被引用。 椭圆曲线密码学 正如前面所提到的那样,公钥和私钥是一串随机的字符序列。由于私钥是用来识别比特币所有者身份的缘故,因此有一个必要的条件:这个随机算法必须产生真正的随机序列。我们不希望意外地生成其他人所拥有的私钥。也就是要保证随机序列的绝对唯一性。 比特币是使用的椭圆曲线来生成的私钥。椭圆曲线是一个非常复杂的数学概念,这里我们不做详细的介绍(如果你对此非常好奇,可以点击 this gentle introduction to elliptic curves 进行详细的 了解,警告:数学公式)。我们需要知道的是,这些曲线可以用来生成真正大而随机的数字。比特币所采用的曲线算法能够随机生成一个介于0到 2^2^56之间的数字(这是一个非常大的数字,用十进制表示的话,大约是10^77, 而整个可见的宇宙中,原子数在 10^78 到 10^82 之间) 。这么巨大的上限意味着产生两个一样的私钥是几乎不可能的事情。 另外,我们将会使用比特币中所使用的 ECDSA (椭圆曲线数字签名算法)去签署交易信息。 Base58和Base58Check编码 现在让我们回到上面提到的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa . 现在我们知道这个地址其实是公钥的一种可读高的表示方式。如果我们对他进行解码,我们会看到公钥看起来是这样子的(字节序列的十六进制的表示方式): 0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93 Base58 Base64使用了26个小写字母、26个大写字母、10个数字以及两个符号(例如“+”和“/”),用于在电子邮件这样的基于文本的媒介中传输二进制数据。Base64通常用于编码邮件中的附件。Base58是一种基于文本的二进制编码格式,用在比特币和其它的加密货币中。这种编码格式不仅实现了数据压缩,保持了易读性,还具有错误诊断功能。Base58是Base64编码格式的子集,同样使用大小写字母和10个数字,但舍弃了一些容易错读和在特定字体中容易混淆的字符。具体地,Base58不含Base64中的0(数字0)、O(大写字母o)、l(小写字母L)、I(大写字母i),以及“+”和“/”两个字符。简而言之,Base58就是由不包括(0,O,l,I)的大小写字母和数字组成。 比特币的Base58字母表: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz Base58Check Base58Check是一种常用在比特币中的Base58编码格式,增加了错误校验码来检查数据在转录中出现的错误。校验码长4个字节,添加到需要编码的数据之后。校验码是从需要编码的数据的哈希值中得到的,所以可以用来检测并避免转录和输入中产生的错误。使用Base58check编码格式时,编码软件会计算原始数据的校验码并和结果数据中自带的校验码进行对比。二者不匹配则表明有错误产生,那么这个Base58Check格式的数据就是无效的。例如,一个错误比特币地址就不会被钱包认为是有效的地址,否则这种错误会造成资金的丢失。 为了使用Base58Check编码格式对数据(数字)进行编码,首先我们要对数据添加一个称作“版本字节”的前缀,这个前缀用来明确需要编码的数据的类型。例如,比特币地址的前缀是0(十六进制是0x00),而对私钥编码时前缀是128(十六进制是0x80)。 让我们以示意图的形式展示一下从公钥得到地址的过程: 因此,上述解码的公钥由三部分组成: Version Public key hash Checksum 00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93 由于哈希函数是单向的(也就说无法逆转回去),所以不可能从一个哈希中提取公钥。不过通过执行哈希函数并进行哈希比较,我们可以检查一个公钥是否被用于哈希的生成。 OK,现在我们有了所有的东西,让我们来编写一些代码。 当一些概念被写成代码时,我们会对此理解的更加清晰和深刻。 地址实现 让我们从 Wallet 的构成开始,这里我们需要先引入一个maven包: <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.59</version> </dependency> 钱包结构 /** * 钱包 * * @author wangwei * @date 2018/03/14 */ @Data @AllArgsConstructor public class Wallet { // 校验码长度 private static final int ADDRESS_CHECKSUM_LEN = 4; /** * 私钥 */ private BCECPrivateKey privateKey; /** * 公钥 */ private byte[] publicKey; public Wallet() { initWallet(); } /** * 初始化钱包 */ private void initWallet() { try { KeyPair keyPair = newECKeyPair(); BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate(); BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic(); byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); this.setPrivateKey(privateKey); this.setPublicKey(publicKeyBytes); } catch (Exception e) { e.printStackTrace(); } } /** * 创建新的密钥对 * * @return * @throws Exception */ private KeyPair newKeyPair() throws Exception { // 注册 BC Provider Security.addProvider(new BouncyCastleProvider()); // 创建椭圆曲线算法的密钥对生成器,算法为 ECDSA KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); // 椭圆曲线(EC)域参数设定 // bitcoin 为什么会选择 secp256k1,详见:https://bitcointalk.org/index.php?topic=151120.0 ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); g.initialize(ecSpec, new SecureRandom()); return g.generateKeyPair(); } } 所谓的钱包,其实本质上就是一个密钥对。这里我们需要借助 KeyPairGenerator 生成密钥对。 接着,我们来生成比特币的钱包地址: public class Wallet { ... /** * 获取钱包地址 * * @return */ public String getAddress() throws Exception { // 1. 获取 ripemdHashedKey byte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey(); // 2. 添加版本 0x00 ByteArrayOutputStream addrStream = new ByteArrayOutputStream(); addrStream.write((byte) 0); addrStream.write(ripemdHashedKey); byte[] versionedPayload = addrStream.toByteArray(); // 3. 计算校验码 byte[] checksum = BtcAddressUtils.checksum(versionedPayload); // 4. 得到 version + paylod + checksum 的组合 addrStream.write(checksum); byte[] binaryAddress = addrStream.toByteArray(); // 5. 执行Base58转换处理 return Base58Check.rawBytesToBase58(binaryAddress); } ... } 这个时候,你就可以得到 真实的比特币地址 了,并且你可以到 blockchain.info 上去检查这个地址的余额。 例如,通过 getAddress 方法,得到了一个比特币地址为:1rZ9SjXMRwnbW3Pu8itC1HtNBVHERSQhaACbL16 我敢保证,无论你生成多少次比特币地址,它的余额始终为0.这就是为什么选择适当的公钥密码算法如此重要:考虑到私钥是随机数字,产生相同数字的机会必须尽可能低。 理想情况下,它必须低至“永不”。 另外,需要注意的是你不需要连接到比特币的节点上去获取比特币的地址。有关地址生成的开源算法工具包已经有很多编程语言和库实现了。 现在,我们需要去修改交易输入与输出,让他们开始使用真实的地址: 交易输入 /** * 交易输入 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易输出索引 */ private int txOutputIndex; /** * 签名 */ private byte[] signature; /** * 公钥 */ private byte[] pubKey; /** * 检查公钥hash是否用于交易输入 * * @param pubKeyHash * @return */ public boolean usesKey(byte[] pubKeyHash) { byte[] lockingHash = BtcAddressUtils.ripeMD160Hash(this.getPubKey()); return Arrays.equals(lockingHash, pubKeyHash); } } 交易输出 /** * 交易输出 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXOutput { /** * 数值 */ private int value; /** * 公钥Hash */ private byte[] pubKeyHash; /** * 创建交易输出 * * @param value * @param address * @return */ public static TXOutput newTXOutput(int value, String address) { // 反向转化为 byte 数组 byte[] versionedPayload = Base58Check.base58ToBytes(address); byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length); return new TXOutput(value, pubKeyHash); } /** * 检查交易输出是否能够使用指定的公钥 * * @param pubKeyHash * @return */ public boolean isLockedWithKey(byte[] pubKeyHash) { return Arrays.equals(this.getPubKeyHash(), pubKeyHash); } } 代码中还有很多其他的地方需要变动,这里不一一指出,详见文末的源码连接。 注意,由于我们不会去实现脚本语言特性,所以我们不再使用 scriptPubKey 和 scriptSig 字段。取而代之的是,我们将 scriptSig 拆分为了 signature 和 pubKey 字段,scriptPubKey 重命名为了 pubKeyHash 。我们将会实现类似于比特币中的交易输出锁定/解锁逻辑和交易输入的签名逻辑,但是我们会在方法中执行此操作。 usesKey 用于检查交易输入中的公钥是否能够解锁交易输出。需要注意的是,交易输入中存储的是未经hash过的公钥,但是方法实现中对它做了一步 ripeMD160Hash 转化。 isLockedWithKey 用于检查提供的公钥Hash是否能够用于解锁交易输出,这个方法是 usesKey 的补充。usesKey 被用于 getAllSpentTXOs 方法中,isLockedWithKey 被用于 findUnspentTransactions 方法中,这样使得在前后两笔交易之间建立起了连接。 newTXOutput 方法中,将 value 锁定到了 address 上。当我们向别人发送比特币时,我们只知道他们的地址,因此函数将地址作为唯一的参数。然后解码地址,并从中提取公钥哈希并保存在PubKeyHash字段中。 现在,让我们一起来检查一下是否能够正常运行: $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1 $ java -jar blockchain-java-jar-with-dependencies.jar createblockchain -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Elapsed Time: 6.77 seconds correct hash Hex: 00000e44be0c94c39a4fef24c67d85c428e8bfbd227e292d75c0f4d398e2e81c Done ! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Balance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 10 $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e -to 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVd -amount 5 java.lang.Exception: ERROR: Not enough funds $ java -jar blockchain-java-jar-with-dependencies.jar send -from 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh -to 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e-amount 5 Elapsed Time: 4.477 seconds correct hash Hex: 00000da41dfacc8032a553ed5b1aa5e24318d5d89ca14a16c4f70129609c8365 Success! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Balance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e Balance of '1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1 Balance of '19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1': 0 Nice! 现在让我们一起来实现交易签名部分的内容。 签名实现 交易数据必须被签名,因为这是比特币中能够保证不能花费属于他人比特币的唯一方法。如果一个签名是无效的,那么这笔交易也是无效的,这样的话,这笔交易就不能被添加到区块链中去。 我们已经有了实现交易签名的所有片段,还有一个事情除外:用于签名的数据。交易数据中哪一部分是真正用于签名的呢?难道是全部数据?选择用于签名的数据相当的重要。用于签名的数据必须包含以独特且唯一的方式标识数据的信息。例如,仅对交易输出签名是没有意义的,因为此签名不会考虑发送发与接收方。 考虑到交易数据要解锁前面的交易输出,重新分配交易输出中的 value 值,并且锁定新的交易输出,因此下面这些数据是必须被签名的: 存储在解锁了的交易输出中的公钥Hash。它标识了交易的发送方。 存储在新的、锁定的交易输出中的公钥Hash。它标识了交易的接收方。 新的交易输出中包含的 value 值。 在比特币中,锁定/解锁逻辑存储在脚本中,解锁脚本存储在交易输入的 ScriptSig 字段中,而锁定脚本存储在交易输出的 ScriptPubKey 的字段中。 由于比特币允许不同类型的脚本,因此它会对ScriptPubKey的全部内容进行签名。 如你所见,我们不需要去对存储在交易输入中的公钥进行签名。正因为如此,在比特币中,所签名的并不是一个交易,而是一个去除部分内容的交易输入副本,交易输入里面存储了被引用交易输出的 ScriptPubKey 。 获取修剪后的交易副本的详细过程在这里. 虽然它可能已经过时了,但是我并没有找到另一个更可靠的来源。 OK,它看起来有点复杂,因此让我们来开始coding吧。我们将从 Sign 方法开始: public class Transaction { ... /** * 签名 * * @param privateKey 私钥 * @param prevTxMap 前面多笔交易集合 */ public void sign(BCECPrivateKey privateKey, Map<String, Transaction> prevTxMap) throws Exception { // coinbase 交易信息不需要签名,因为它不存在交易输入信息 if (this.isCoinbase()) { return; } // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据 for (TXInput txInput : this.getInputs()) { if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { throw new Exception("ERROR: Previous transaction is not correct"); } } // 创建用于签名的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); for (int i = 0; i < txCopy.getInputs().length; i++) { TXInput txInputCopy = txCopy.getInputs()[i]; // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null); // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); // 对整个交易信息仅进行签名,即对交易ID进行签名 ecdsaSign.update(txCopy.getTxId()); byte[] signature = ecdsaSign.sign(); // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名 // 注意是将得到的签名赋值给原交易信息中的交易输入 this.getInputs()[i].setSignature(signature); } } ... } 这个方法需要私钥和前面多笔交易集合作为参数。正如前面所提到的那样,为了能够对交易信息进行签名,我们需要能够访问到被交易数据中的交易输入所引用的交易输出,因此我们需要得到存储这些交易输出的交易信息。 让我们来一步一步review这个方法: if (this.isCoinbase()) { return; } 由于 coinbase 交易信息不存在交易输入信息,因此它不需要签名,直接return. Transaction txCopy = this.trimmedCopy(); 创建交易的副本 public class Transaction { ... /** * 创建用于签名的交易数据副本 * * @return */ public Transaction trimmedCopy() { TXInput[] tmpTXInputs = new TXInput[this.getInputs().length]; for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null); } TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length]; for (int i = 0; i < this.getOutputs().length; i++) { TXOutput txOutput = this.getOutputs()[i]; tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash()); } return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs); } ... } 这个交易数据的副本包含了交易输入与交易输出,但是交易输入的 Signature 与 PubKey 需要设置为null。 使用私钥初始化 SHA256withECDSA 签名算法: Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); 接下来,我们迭代交易副本中的交易输入: for (TXInput txInput : txCopy.getInputs()) { // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null); 在每一个 txInput中,signature 都需要设置为null(仅仅是为了二次确认检查),并且 pubKey 设置为它所引用的交易输出的 pubKeyHash 字段。在此刻,除了当前的正在循环的交易输入(txInput)外,其他所有的交易输入都是"空的",也就是说他们的 Signature 和 PubKey 字段被设置为 null。因此,交易输入是被分开签名的,尽管这对于我们的应用并不十分紧要,但是比特币允许交易包含引用了不同地址的输入。 Hash 方法对交易进行序列化,并使用 SHA-256 算法进行哈希。哈希后的结果就是我们要签名的数据。在获取完哈希,我们应该重置 PubKey 字段,以便于它不会影响后面的迭代。 // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInput.setPubKey(null); 现在,最关键的部分来了: // 对整个交易信息仅进行签名,即对交易ID进行签名 Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); ecdsaSign.update(txCopy.getTxId()); byte[] signature = ecdsaSign.sign(); // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名 // 注意是将得到的签名赋值给原交易信息中的交易输入 this.getInputs()[i].setSignature(signature); 使用 SHA256withECDSA 签名算法加上私钥,来对交易ID进行签名,从而得到了交易输入所要设置的交易签名。 现在,让我们来实现交易的验证功能: public class Transaction { ... /** * 验证交易信息 * * @param prevTxMap 前面多笔交易集合 * @return */ public boolean verify(Map<String, Transaction> prevTxMap) throws Exception { // coinbase 交易信息不需要签名,也就无需验证 if (this.isCoinbase()) { return true; } // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据 for (TXInput txInput : this.getInputs()) { if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { throw new Exception("ERROR: Previous transaction is not correct"); } } // 创建用于签名验证的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); // 使用椭圆曲线 x,y 点去生成公钥Key BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); PublicKey publicKey = keyFactory.generatePublic(keySpec); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(txCopy.getTxId()); if (!ecdsaVerify.verify(txInput.getSignature())) { return false; } } return true; } ... } 首选,同前面签名一样,我们先获取交易的拷贝数据: Transaction txCopy = this.trimmedCopy(); 获取椭圆曲线参数和签名类: Security.addProvider(new BouncyCastleProvider()); ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); 接下来,我们来检查每一个交易输入的签名是否正确: for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); } 这部分与Sign方法中的相同,因为在验证过程中我们需要签署相同的数据。 // 使用椭圆曲线 x,y 点去生成公钥Key BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); PublicKey publicKey = keyFactory.generatePublic(keySpec); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(txCopy.getTxId()); if (!ecdsaVerify.verify(txInput.getSignature())) { return false; } 由于交易输入中存储的 pubkey ,实际上是椭圆曲线上的一对 x,y 坐标,所以我们可以从 pubKey 得到公钥PublicKey,然后在用公钥去签名进行验证。如果验证成功,则返回true,否则,返回false。 现在,我们需要一个方法来获取以前的交易。 由于这需要与区块链互动,我们将使其成为 blockchain 的一种方法: public class Blockchain { ... /** * 依据交易ID查询交易信息 * * @param txId 交易ID * @return */ private Transaction findTransaction(byte[] txId) throws Exception { for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) { Block block = iterator.next(); for (Transaction tx : block.getTransactions()) { if (Arrays.equals(tx.getTxId(), txId)) { return tx; } } } throw new Exception("ERROR: Can not found tx by txId ! "); } /** * 进行交易签名 * * @param tx 交易数据 * @param privateKey 私钥 */ public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception { // 先来找到这笔新的交易中,交易输入所引用的前面的多笔交易的数据 Map<String, Transaction> prevTxMap = new HashMap<>(); for (TXInput txInput : tx.getInputs()) { Transaction prevTx = this.findTransaction(txInput.getTxId()); prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx); } tx.sign(privateKey, prevTxMap); } /** * 交易签名验证 * * @param tx */ private boolean verifyTransactions(Transaction tx) throws Exception { Map<String, Transaction> prevTx = new HashMap<>(); for (TXInput txInput : tx.getInputs()) { Transaction transaction = this.findTransaction(txInput.getTxId()); prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction); } return tx.verify(prevTx); } } 现在,我们需要对我们的交易进行真正的签名和验证了,交易的签名发生在 newUTXOTransaction 中: public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception { ... Transaction newTx = new Transaction(null, txInputs, txOutput); newTx.setTxId(newTx.hash()); // 进行交易签名 blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); return newTx; } 交易的验证发生在一笔交易被放入区块之前: public void mineBlock(Transaction[] transactions) throws Exception { // 挖矿前,先验证交易记录 for (Transaction tx : transactions) { if (!this.verifyTransactions(tx)) { throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! "); } } ... } OK,让我们再一次对整个工程的代码做一个测试,测试结果: $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f $ java -jar blockchain-java-jar-with-dependencies.jar createblockchain -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Elapsed Time: 164.961 seconds correct hash Hex: 00000524231ae1832c49957848d2d1871cc35ff4d113c23be1937c6dff5cdf2a Done ! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 10 $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -to 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f -amount 5 java.lang.Exception: ERROR: Not enough funds $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 -to 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -amount 5 Elapsed Time: 54.92 seconds correct hash Hex: 00000354f86cde369d4c39d2b3016ac9a74956425f1348b4c26b2cddb98c100b Success! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB Balance of '1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f Balance of '13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f': 0 Good!没有任何错误! 让我们注释掉 NewUTXOTransaction 方法中的一行代码,确保未被签名的交易不能被添加到区块中: ... // blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); ... 测试结果: java.lang.Exception: Fail to verify transaction ! transaction invalid ! at one.wangwei.blockchain.block.Blockchain.verifyTransactions(Blockchain.java:334) at one.wangwei.blockchain.block.Blockchain.mineBlock(Blockchain.java:76) at one.wangwei.blockchain.cli.CLI.send(CLI.java:202) at one.wangwei.blockchain.cli.CLI.parse(CLI.java:79) at one.wangwei.blockchain.BlockchainTest.main(BlockchainTest.java:23) 总结 这一节,我们学到了: 使用椭圆曲线加密算法,如何去创建钱包; 了解到了如何去生成比特币地址; 如何去对交易信息进行签名并对签名进行验证; 到目前为止,我们已经实现了比特币的许多关键特性! 我们已经实现了除外网络外的几乎所有功能,并且在下一篇文章中,我们将继续完善交易这一环节机制。 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part5-wallet 《精通比特币(第二版)》—— 第四章 Elliptic Curve Key Pair Generation and Key Factories How to create public key objects with x and y coordinates? Public-key cryptography Digital signatures Elliptic curve Elliptic curve cryptography ECDSA Technical background of Bitcoin addresses Address Base58 A gentle introduction to elliptic curve cryptography ​

优秀的个人博客,低调大师

基于Java语言构建区块链(四)—— 交易(UTXO)

最终内容请以原文为准:https://wangwei.one/posts/9cf9e42a.html 引言 上一篇 文章,我们实现了区块数据的持久化,本篇开始交易环节的实现。交易这一环节是整个比特币系统当中最为关键的一环,并且区块链唯一的目的就是通过安全的、可信的方式来存储交易信息,防止它们创建之后被人恶意篡改。今天我们开始实现交易这一环节,但由于这是一个很大的话题,所以我们分为两部分:第一部分我们将实现区块链交易的基本机制,到第二部分,我们再来研究它的细节。 比特币交易 如果你开发过Web应用程序,为了实现支付系统,你可能会在数据库中创建一些数据库表:账户 和 交易记录。账户用于存储用户的个人信息以及账户余额等信息,交易记录用于存储资金从一个账户转移到另一个账户的记录。但是在比特币中,支付系统是以一种完全不一样的方式实现的,在这里: 没有账户 没有余额 没有地址 没有 Coins(币) 没有发送者和接受者 由于区块链是一个公开的数据库,我们不希望存储有关钱包所有者的敏感信息。Coins 不会汇总到钱包中。交易不会将资金从一个地址转移到另一个地址。没有可保存帐户余额的字段或属性。只有交易信息。那比特币的交易信息里面到底存储的是什么呢? 交易组成 一笔比特币的交易由 交易输入 和 交易输出 组成,数据结构如下: /** * 交易 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class Transaction { /** * 交易的Hash */ private byte[] txId; /** * 交易输入 */ private TXInput[] inputs; /** * 交易输出 */ private TXOutput[] outputs; } 一笔交易的 交易输入 其实是指向上一笔交易的交易输出 (这个后面详细说明)。我们钱包里面的 Coin(币)实际是存储在这些 交易输出 里面。下图表示了区块链交易系统里面各个交易相互引用的关系: 注意: 有些 交易输出 并不是由 交易输入 产生,而是凭空产生的(后面会详细介绍)。 但,交易输入 必须指向某个 交易输出,它不能凭空产生。 在一笔交易里面,交易输入 可能会来自多笔交易所产生的 交易输出。 在整篇文章中,我们将使用诸如“钱”,“硬币”,“花费”,“发送”,“账户”等词语。但比特币中没有这样的概念,在比特币交易中,交易信息是由 锁定脚本 锁定一个数值,并且只能被所有者的 解锁脚本 解锁。(解铃还须系铃人) 交易输出 让我们先从交易输出开始,他的数据结构如下: /** * 交易输出 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXOutput { /** * 数值 */ private int value; /** * 锁定脚本 */ private String scriptPubKey; } 实际上,它表示的是能够存储 "coins(币)"的交易输出(注意 value 字段)。并且这里所谓的 value 实际上是由存储在 ScriptPubKey (锁定脚本)中的一个puzzle(难题) 所锁定。在内部,比特币使用称为脚本的脚本语言,用于定义输出锁定和解锁逻辑。这个语言很原始(这是故意的,以避免可能的黑客和滥用),但我们不会详细讨论它。 你可以在这里找到它的详细解释。here 在比特币中,value 字段存储着 satoshis 的任意倍的数值,而不是BTC的数量。satoshis 是比特币的百万分之一(0.00000001 BTC),因此这是比特币中最小的货币单位(如1美分)。 satoshis:聪 锁定脚本是一个放在一个输出值上的“障碍”,同时它明确了今后花费这笔输出的条件。由于锁定脚本往往含有一个公钥(即比特币地址),在历史上它曾被称作一个脚本公钥代码。在大多数比特币应用源代码中,脚本公钥代码便是我们所说的锁定脚本。 由于我们还没有实现钱包地址的逻辑,所以这里先暂且忽略锁定脚本相关的逻辑。ScriptPubKey 将会存储任意的字符串(用户定义的钱包地址) 顺便说一句,拥有这样的脚本语言意味着比特币也可以用作智能合约平台。 关于 交易输出 的一个重要的事情是它们是不可分割的,这意味着你不能将它所存储的数值拆开来使用。当这个交易输出在新的交易中被交易输入所引用时,它将作为一个整体被花费掉。 如果其值大于所需值,那么剩余的部分则会作为零钱返回给付款方。 这与真实世界的情况类似,例如,您支付5美元的钞票用于购买1美元的东西,那么你将会得到4美元的零钱。 交易输入 /** * 交易输入 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易输出索引 */ private int txOutputIndex; /** * 解锁脚本 */ private String scriptSig; } 前面提到过,一个交易输入指向的是某一笔交易的交易输出: txId 存储的是某笔交易的ID值 txOutputIndex 存储的是交易中这个交易输出的索引位置(因为一笔交易可能包含多个交易输出) scriptSig 主要是提供用于交易输出中 ScriptPubKey 所需的验证数据。 如果这个数据被验证正确,那么相应的交易输出将被解锁,并且其中的 value 能够生成新的交易输出; 如果不正确,那么相应的交易输出将不能被交易输入所引用; 通过锁定脚本与解锁脚本这种机制,保证了某个用户不能花费属于他人的Coins。 同样,由于我们尚未实现钱包地址功能,ScriptSig 将会存储任意的用户所定义的钱包地址。我们将会在下一章节实现公钥和数字签名验证。 说了这么多,我们来总结一下。交易输出是"Coins"实际存储的地方。每一个交易输出都带有一个锁定脚本,它决定了解锁的逻辑。每一笔新的交易必须至少有一个交易输入与交易输出。一笔交易的交易输入指向前一笔交易的交易输出,并且提供用于锁定脚本解锁需要的数据(ScriptSig 字段),然后利用交易输出中的 value 去创建新的交易输出。 注意,这段话的原文如下,但是里面有表述错误的地方,交易输出带有的是锁定脚本,而不是解锁脚本。 Let’s sum it up. Outputs are where “coins” are stored. Each output comes with an unlocking script, which determines the logic of unlocking the output. Every new transaction must have at least one input and output. An input references an output from a previous transaction and provides data (the ScriptSig field) that is used in the output’s unlocking script to unlock it and use its value to create new outputs. 那到底是先有交易输入还是先有交易输出呢? 鸡与蛋的问题 在比特币中,鸡蛋先于鸡出现。交易输入源自于交易输出的逻辑是典型的"先有鸡还是先有蛋"的问题:交易输入产生交易输出,交易输出又会被交易输入所引用。在比特币中,交易输出先于交易输入出现。 当矿工开始开采区块时,区块中会被添加一个 coinbase 交易(创币交易)。coinbase 交易是一种特殊的交易,它不需要以前已经存在的交易输出。它会凭空创建出交易输出(i.e: Coins)。也即,鸡蛋的出现并不需要母鸡,这笔交易是作为矿工成功挖出新的区块后的一笔奖励。 正如你所知道的那样,在区块链的最前端,即第一个区块,有一个创世区块。他产生了区块链中有史以来的第一个交易输出,并且由于没有前一笔交易,也就没有相应的输出,因此不需要前一笔交易的交易输出。 让我们来创建 coinbase 交易: /** * 创建CoinBase交易 * * @param to 收账的钱包地址 * @param data 解锁脚本数据 * @return */ public Transaction newCoinbaseTX(String to, String data) { if (StringUtils.isBlank(data)) { data = String.format("Reward to '%s'", to); } // 创建交易输入 TXInput txInput = new TXInput(new byte[]{}, -1, data); // 创建交易输出 TXOutput txOutput = new TXOutput(SUBSIDY, to); // 创建交易 Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}); // 设置交易ID tx.setTxId(); return tx; } coinbase交易只有一个交易输入。在我们的代码实现中,txId 是空数组,txOutputIndex 设置为了 -1。另外,coinbase交易不会在 ScriptSig 字段上存储解锁脚本,相反,存了一个任意的数据。 在比特币中,第一个 coinbase 交易报刊了如下的信息:"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks". 点击查看 SUBSIDY 是挖矿奖励数量。在比特币中,这个奖励数量没有存储在任何地方,而是依据现有区块的总数进行计算而得到:区块总数 除以 210000。开采创世区块得到的奖励为50BTC,每过 210000 个区块,奖励会减半。在我们的实现中,我们暂且将挖矿奖励设置为常数。(至少目前是这样) 在区块链中存储交易信息 从现在开始,每一个区块必须存储至少一个交易信息,并且尽可能地避免在没有交易数据的情况下进行挖矿。这意味着我们必须移除 Block 对象中的 date 字段,取而代之的是 transactions: /** * 区块 * * @author wangwei * @date 2018/02/02 */ @Data @AllArgsConstructor @NoArgsConstructor public class Block { /** * 区块hash值 */ private String hash; /** * 前一个区块的hash值 */ private String previousHash; /** * 交易信息 */ private Transaction[] transactions; /** * 区块创建时间(单位:秒) */ private long timeStamp; } 相应地,newGenesisBlock 与 newBlock 也都需要做改变: /** * <p> 创建创世区块 </p> * * @param coinbase * @return */ public static Block newGenesisBlock(Transaction coinbase) { return Block.newBlock("", new Transaction[]{coinbase}); } /** * <p> 创建新区块 </p> * * @param previousHash * @param transactions * @return */ public static Block newBlock(String previousHash, Transaction[] transactions) { Block block = new Block("", previousHash, transactions, Instant.now().getEpochSecond(), 0); ProofOfWork pow = ProofOfWork.newProofOfWork(block); PowResult powResult = pow.run(); block.setHash(powResult.getHash()); block.setNonce(powResult.getNonce()); return block; } 接下来,修改 newBlockchain 方法: /** * <p> 创建区块链 </p> * * @param address 钱包地址 * @return */ public static Blockchain newBlockchain(String address) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (StringUtils.isBlank(lastBlockHash)) { // 创建 coinBase 交易 Transaction coinbaseTX = Transaction.newCoinbaseTX(address, ""); Block genesisBlock = Block.newGenesisBlock(coinbaseTX); lastBlockHash = genesisBlock.getHash(); RocksDBUtils.getInstance().putBlock(genesisBlock); RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash); } return new Blockchain(lastBlockHash); } 现在,代码有钱包地址的接口,将会收到开采创世区块的奖励。 工作量证明(Pow) Pow算法必须将存储在区块中的交易信息考虑在内,以保存交易信息存储的一致性和可靠性。因此,我们必须修改 ProofOfWork.prepareData 接口代码逻辑: /** * 准备数据 * <p> * 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换 * @param nonce * @return */ private String prepareData(long nonce) { byte[] prevBlockHashBytes = {}; if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) { prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray(); } return ByteUtils.merge( prevBlockHashBytes, this.getBlock().hashTransaction(), ByteUtils.toBytes(this.getBlock().getTimeStamp()), ByteUtils.toBytes(TARGET_BITS), ByteUtils.toBytes(nonce) ); } 其中 hashTransaction 代码如下: /** * 对区块中的交易信息进行Hash计算 * * @return */ public byte[] hashTransaction() { byte[][] txIdArrays = new byte[this.getTransactions().length][]; for (int i = 0; i < this.getTransactions().length; i++) { txIdArrays[i] = this.getTransactions()[i].getTxId(); } return DigestUtils.sha256(ByteUtils.merge(txIds)); } 同样,我们使用哈希值来作为数据的唯一标识。我们希望区块中的所有交易数据都能通过一个哈希值来定义它的唯一标识。为了达到这个目的,我们计算了每一个交易的唯一哈希值,然后将他们串联起来,再对这个串联后的组合进行哈希值计算。 比特币使用更复杂的技术:它将所有包含在块中的交易表示为 Merkle树 ,并在Proof-of-Work系统中使用该树的根散列。 这种方法只需要跟节点的散列值就可以快速检查块是否包含某笔交易,而无需下载所有交易。 UTXO(未花费交易输出) UTXO:unspend transaction output.(未被花费的交易输出) 在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的UTXO. UTXO 是理解比特币交易原理的关键所在,我们先来看一段场景: 场景:假设你过去分别向A、B、C这三个比特币用户购买了BTC,从A手中购买了3.5个BTC,从B手中购买了4.5个BTC,从C手中购买了2个BTC,现在你的比特币钱包里面恰好剩余10个BTC。 问题:这个10个BTC是真正的10个BTC吗?其实不是,这句话可能听起来有点怪。(什么!我钱包里面的BTC不是真正的BTC,你不要吓我……) 解释:前面提到过在比特币的交易系统当中,并不存在账户、余额这些概念,所以,你的钱包里面的10个BTC,并不是说钱包余额为10个BTC。而是说,这10个BTC其实是由你的比特币地址(钱包地址|公钥)锁定了的散落在各个区块和各个交易里面的UTXO的总和。 UTXO 是比特币交易的基本单位,每笔交易都会产生UTXO,一个UTXO可以是一“聪”的任意倍。给某人发送比特币实际上是创造新的UTXO,绑定到那个人的钱包地址,并且能被他用于新的支付。 一般的比特币交易由 交易输入 和 交易输出 两部分组成。A向你支付3.5个BTC这笔交易,实际上产生了一个新的UTXO,这个新的UTXO 等于 3.5个BTC(3.5亿聪),并且锁定到了你的比特币钱包地址上。 假如你要给你女(男)朋友转 1.5 BTC,那么你的钱包会从可用的UTXO中选取一个或多个可用的个体来拼凑出一个大于或等于一笔交易所需的比特币量。比如在这个假设场景里面,你的钱包会选取你和C的交易中的UTXO作为 交易输入,input = 2BTC,这里会生成两个新的交易输出,一个输出(UTXO = 1.5 BTC)会被绑定到你女(男)朋友的钱包地址上,另一个输出(UTXO = 0.5 BTC)会作为找零,重新绑定到你的钱包地址上。 有关比特币交易这部分更详细的内容,请查看:《精通比特币(第二版)》第6章 —— 交易 我们需要找到所有未花费的交易输出(UTXO)。Unspent(未花费) 意味着这些交易输出从未被交易输入所指向。这前面的图片中,UTXO如下: tx0, output 1; tx1, output 0; tx3, output 0; tx4, output 0. 当然,当我们检查余额时,我不需要区块链中所有的UTXO,我只需要能被我们解锁的UTXO(当前,我们还没有实现密钥对,而是替代为用户自定义的钱包地址)。首先,我们在交易输入与交易输出上定义锁定-解锁的方法: 交易输入: public class TXInput { ... /** * 判断解锁数据是否能够解锁交易输出 * * @param unlockingData * @return */ public boolean canUnlockOutputWith(String unlockingData) { return this.getScriptSig().endsWith(unlockingData); } } 交易输出: public class TXOutput { ... /** * 判断解锁数据是否能够解锁交易输出 * * @param unlockingData * @return */ public boolean canBeUnlockedWith(String unlockingData) { return this.getScriptPubKey().endsWith(unlockingData); } } 这里我们暂时用 unlockingData 来与脚本字段进行比较。我们会在后面的文章中来对这部分内容进行优化,我们将会基于私钥来实现用户的钱包地址。 下一步,查询所有与钱包地址绑定的包含UTXO的交易信息,有点复杂(本篇先这样实现,后面我们做一个与钱包地址映射的UTXO池来进行优化): 从与钱包地址对应的交易输入中查询出所有已被花费了的交易输出 再来排除,寻找包含未被花费的交易输出的交易 public class Blockchain { ... /** * 查找钱包地址对应的所有未花费的交易 * * @param address 钱包地址 * @return */ private Transaction[] findUnspentTransactions(String address) throws Exception { Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs(address); Transaction[] unspentTxs = {}; // 再次遍历所有区块中的交易输出 for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next(); for (Transaction transaction : block.getTransactions()) { String txId = Hex.encodeHexString(transaction.getTxId()); int[] spentOutIndexArray = allSpentTXOs.get(txId); for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) { if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) { continue; } // 保存不存在 allSpentTXOs 中的交易 if (transaction.getOutputs()[outIndex].canBeUnlockedWith(address)) { unspentTxs = ArrayUtils.add(unspentTxs, transaction); } } } } return unspentTxs; } /** * 从交易输入中查询区块链中所有已被花费了的交易输出 * * @param address 钱包地址 * @return 交易ID以及对应的交易输出下标地址 * @throws Exception */ private Map<String, int[]> getAllSpentTXOs(String address) throws Exception { // 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值 Map<String, int[]> spentTXOs = new HashMap<>(); for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next(); for (Transaction transaction : block.getTransactions()) { // 如果是 coinbase 交易,直接跳过,因为它不存在引用前一个区块的交易输出 if (transaction.isCoinbase()) { continue; } for (TXInput txInput : transaction.getInputs()) { if (txInput.canUnlockOutputWith(address)) { String inTxId = Hex.encodeHexString(txInput.getTxId()); int[] spentOutIndexArray = spentTXOs.get(inTxId); if (spentOutIndexArray == null) { spentTXOs.put(inTxId, new int[]{txInput.getTxOutputIndex()}); } else { spentOutIndexArray = ArrayUtils.add(spentOutIndexArray, txInput.getTxOutputIndex()); spentTXOs.put(inTxId, spentOutIndexArray); } } } } } return spentTXOs; } ... } 得到了所有包含UTXO的交易数据,接下来,我们就可以得到所有UTXO集合了: public class Blockchain { ... /** * 查找钱包地址对应的所有UTXO * * @param address 钱包地址 * @return */ public TXOutput[] findUTXO(String address) throws Exception { Transaction[] unspentTxs = this.findUnspentTransactions(address); TXOutput[] utxos = {}; if (unspentTxs == null || unspentTxs.length == 0) { return utxos; } for (Transaction tx : unspentTxs) { for (TXOutput txOutput : tx.getOutputs()) { if (txOutput.canBeUnlockedWith(address)) { utxos = ArrayUtils.add(utxos, txOutput); } } } return utxos; } ... } 现在,我们可以实现获取钱包地址余额的接口了: public class CLI { ... /** * 查询钱包余额 * * @param address 钱包地址 */ private void getBalance(String address) throws Exception { Blockchain blockchain = Blockchain.createBlockchain(address); TXOutput[] txOutputs = blockchain.findUTXO(address); int balance = 0; if (txOutputs != null && txOutputs.length > 0) { for (TXOutput txOutput : txOutputs) { balance += txOutput.getValue(); } } System.out.printf("Balance of '%s': %d\n", address, balance); } ... } 查询 wangwei 这个钱包地址的余额: $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei # 输出 Balance of 'wangwei': 10 转账 现在,我们想要给某人发送一些币。因此,我们需要创建一笔新的交易,然后放入区块中,再进行挖矿。到目前为止,我们只是实现了 coinbase 交易,现在我们需要实现常见的创建交易接口: public class Transaction { ... /** * 从 from 向 to 支付一定的 amount 的金额 * * @param from 支付钱包地址 * @param to 收款钱包地址 * @param amount 交易金额 * @param blockchain 区块链 * @return */ public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception { SpendableOutputResult result = blockchain.findSpendableOutputs(from, amount); int accumulated = result.getAccumulated(); Map<String, int[]> unspentOuts = result.getUnspentOuts(); if (accumulated < amount) { throw new Exception("ERROR: Not enough funds"); } Iterator<Map.Entry<String, int[]>> iterator = unspentOuts.entrySet().iterator(); TXInput[] txInputs = {}; while (iterator.hasNext()) { Map.Entry<String, int[]> entry = iterator.next(); String txIdStr = entry.getKey(); int[] outIdxs = entry.getValue(); byte[] txId = Hex.decodeHex(txIdStr); for (int outIndex : outIdxs) { txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, from)); } } TXOutput[] txOutput = {}; txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to)); if (accumulated > amount) { txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from)); } Transaction newTx = new Transaction(null, txInputs, txOutput); newTx.setTxId(); return newTx; } ... } 在创建新的交易输出之前,我们需要事先找到所有的UTXO,并确保有足够的金额。这就是 findSpendableOutputs 要干的事情。之后,为每个找到的输出创建一个引用它的输入。接下来,我们创建两个交易输出: 一个 output 用于锁定到接收者的钱包地址上。这个是真正被转走的coins; 另一个 output 锁定到发送者的钱包地址上。这个就是 找零。只有当用于支付的UTXO总和大于要支付的金额时,才会创建这部分的 交易输出。记住:交易输出是不可分割的 findSpendableOutputs 需要调用我们之前创建的 findUnspentTransactions 接口: public class Blockchain { ... /** * 寻找能够花费的交易 * * @param address 钱包地址 * @param amount 花费金额 */ public SpendableOutputResult findSpendableOutputs(String address, int amount) throws Exception { Transaction[] unspentTXs = this.findUnspentTransactions(address); int accumulated = 0; Map<String, int[]> unspentOuts = new HashMap<>(); for (Transaction tx : unspentTXs) { String txId = Hex.encodeHexString(tx.getTxId()); for (int outId = 0; outId < tx.getOutputs().length; outId++) { TXOutput txOutput = tx.getOutputs()[outId]; if (txOutput.canBeUnlockedWith(address) && accumulated < amount) { accumulated += txOutput.getValue(); int[] outIds = unspentOuts.get(txId); if (outIds == null) { outIds = new int[]{outId}; } else { outIds = ArrayUtils.add(outIds, outId); } unspentOuts.put(txId, outIds); if (accumulated >= amount) { break; } } } } return new SpendableOutputResult(accumulated, unspentOuts); } ... } 这个方法会遍历所有的UTXO并统计他们的总额。当计算的总额恰好大于或者等于需要转账的金额时,方法会停止遍历,然后返回用于支付的总额以及按交易ID分组的交易输出索引值数组。我们不想要花更多的钱。 现在,我们可以修改 Block.mineBlock 接口: public class Block { ... /** * 打包交易,进行挖矿 * * @param transactions */ public void mineBlock(Transaction[] transactions) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (lastBlockHash == null) { throw new Exception("ERROR: Fail to get last block hash ! "); } Block block = Block.newBlock(lastBlockHash, transactions); this.addBlock(block); } ... } 最后,我们来实现转账的接口: public class CLI { ... /** * 转账 * * @param from * @param to * @param amount */ private void send(String from, String to, int amount) throws Exception { Blockchain blockchain = Blockchain.createBlockchain(from); Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain); blockchain.mineBlock(new Transaction[]{transaction}); RocksDBUtils.getInstance().closeDB(); System.out.println("Success!"); } ... } 转账,意味着创建一笔新的交易并且通过挖矿的方式将其存入区块中。但是,比特币不会像我们这样做,它会把新的交易记录先存到内存池中,当一个矿工准备去开采一个区块时,它会把打包内存池中的所有交易信息,并且创建一个候选区块。只有当这个包含所有交易信息的候选区块被成功开采并且被添加到区块链上时,这些交易信息才算被确认。 让我们来测试一下: # 先确认 wangwei 的余额 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 10 # 转账 $ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Pedro -amount 6 Elapsed Time: 0.828 seconds correct hash Hex: 00000c5f50cf72db1f375a5d454f98bc49d07335db921cbef5fa9e58ad34d462 Success! # 查询 wangwei 的余额 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 4 # 查询 Pedro 的余额 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Pedro Balance of 'Pedro': 6 赞!现在让我们来创建更多的交易并且确保从多个交易输出进行转账是正常的: $ java -jar blockchain-java-jar-with-dependencies.jar send -from Pedro -to Helen -amount 2 Elapsed Time: 2.533 seconds correct hash Hex: 00000c81d541ad407a3767ad633d1147602df86fe14e1962ec145ab17b633e88 Success! $ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Helen -amount 2 Elapsed Time: 1.481 seconds correct hash Hex: 00000c3f8b82c2b970438f5f1f39d56bb8a9d66341efc92a02ffcbff91acd84b Success! 现在,Helen 这个钱包地址上有了两笔从 wangwei 和 Pedro 转账中产生的UTXO,让我们将它们再转账给另外一个人: $ java -jar blockchain-java-jar-with-dependencies.jar send -from Helen -to Rachel -amount 3 Elapsed Time: 17.136 seconds correct hash Hex: 000000b1226a947166c2b01a15d1cd3558ddf86fe99bad28a0501a2af60f6a02 Success! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 2 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Pedro Balance of 'Pedro': 4 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Helen Balance of 'Helen': 1 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Rachel Balance of 'Rachel': 3 非常棒!让我们来测试一下失败的场景: $ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Ivan -amount 5 java.lang.Exception: ERROR: Not enough funds at one.wangwei.blockchain.transaction.Transaction.newUTXOTransaction(Transaction.java:104) at one.wangwei.blockchain.cli.CLI.send(CLI.java:138) at one.wangwei.blockchain.cli.CLI.parse(CLI.java:73) at one.wangwei.blockchain.cli.Main.main(Main.java:7) 总结 本篇内容有点难度,但好歹我们现在有了交易信息了。尽管,缺少像比特币这一类加密货币的一些关键特性: 钱包地址。我们还没有基于私钥的真实地址。 奖励。挖矿绝对没有利润。 UTXO池。当我们计算钱包地址的余额时,我们需要遍历所有的区块中的所有交易信息,当有许许多多的区块时,这将花费不少的时间。此外,如果我们想验证以后的交易,可能需要很长时间。 UTXO迟旨在解决这些问题并快速处理交易。 内存池。 这是交易在打包成区块之前存储的地方。 在我们当前的实现中,一个块只包含一笔交易,而且效率很低。 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part4-transaction1 《精通比特币(第二版)》第6章 —— 交易

优秀的个人博客,低调大师

构建可信超融合业态,助力企业IT未来发展

近两年来,云计算超融合市场持续升温,超融合基础架构成为企业级业务市场中的一个新增长点,据统计报告显示,2016年全球超融合市场规模预计将增长94%,预计2019年全球超融合市场的规模将超过1000亿美元。无论是传统IT大牌企业:联想、新华三、深信服、华为、EMC、VMware,还是新型或是国外的虚拟化厂商,甚至是做安全领域的领头羊企业都投入巨资投入到“超融合”领域,于是市场上的超融合一体机、超融合解决方案如雨后春笋般充盈了整个超融合市场。 业界云计算超融合架构以虚拟化为核心,将计算、存储、网络等虚拟资源融合到一台标准X86服务器中形成基准架构单元,并且多套单元设备可以通过网络聚合起来,实现模块化的无缝横向扩展(Scale-Out),形成统一的资源池。在横向扩展能力方面具有传统平台不可比拟的优势。 当前,我们正迎来一个全新的产业发展新机遇——企业云。云计算超融合的发展,正是密切对接企业云发展对产业的需求。超融合将成为下一个重要的IT平台,是云数据中心和企业私有云底层架构的最佳实践。对于企业IT来说,企业需要实现一个软件定义的架构,而超融合架构恰好如此,通过虚拟化计算和分布式存储,企业IT的弹性得到极大释放,能有效降低整体拥有成本(TCO);能大幅缩短交付时间;能够大幅减轻部署和运维的压力;能轻松实现横向扩展,如同使用乐高积木,任意增减组件,形成不同架构……总之,借助超融合,企业可以获得极简、随需应变、向云平滑演进的IT新架构。 但是,目前行业用户在选择超融合产品时往往会遇到不同厂家的业务形态不同、不同产品的功能和性能各不相同,国内外厂商对超融合的定义不同,这一系列的标准缺失,这就导致各类用户在选择超融合时面临极大困扰,这也是目前制约用户放心、可信采购超融合产品的一个重要因素为了增强行业用户采购云计算超融合架构的的信心,同时为设备商提供第三方的评估服务,促进市场健康、快速发展,数据中心联盟开展了云计算超融合架构可信评估工作。 在7月25日-26日举办的“2017可信云大会”上将对《云计算超融合架构可信评估方法》标准进行解读,并通报首批云计算超融可信评估的结果,同时有行业用户分享在使用超融合过程中遇到的问题以及解决方法,更有企业介绍超融合市场最新发展态势。让我们一起期待“2017可信云大会”的到来吧!!! 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册