2021-3-18 1428 0
区块链学习

一、tcp和udp的区别,如何设计可靠udp传输转载自:https://www.cnblogs.com/williamjie/p/11133180.htmlUDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响。传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。1、添加seq/ack机制,确保数据发送到对端2、添加发送和接收缓冲区,主要是用户超时重传。3、添加超时重传机制。详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。二、RSA算法设计三、https过程HTTP的请求过程:1、TCP建立连接后,客户端会发送报文给服务端;2、服务端接收报文并作出响应;3、客户端收到响应后解析给用户;HTTPS的请求过程:1、客户端发送请求到服务端;2、服务器返回证书和公钥;3、客户端验证证书和公钥的有效性,如果有效,则生成对称密钥并使用公钥加密发送到服务端;4、服务端使用私钥解密报文,并使用收到的对称密钥加密报文,发送到客户端;5、客户端使用对称密钥解密报文;6、SSL加密建立四、tendermint共识五、以太坊EVM是否熟悉

区块链学习

一、go语言基础(1)一个包怎么调用另一包的函数,go语言中公有性和私有性怎么表达?函数大写表示公有,小写表示私有(2)简单的介绍一下闭包使用场景,优缺点?首先闭包的特点是当闭包函数引用外部变量的时候,会把这个变量放在堆中,所以在内部函数引用外部函数的变量时,值不会被释放packagemainimport"fmt"funcexternalFunc(countint)func()int{returnfunc()int{count++returncount}}funcmain(){//这时候的external是个函数,externalFunc函数返回了个匿名函数external:=externalFunc(1)//这时候的count==1fmt.Println(external())//输出2fmt.Println(external())//输出3fmt.Println(external())//输出4}用法1.函数计数器//函数计数器funccounter(ffunc())func()int{n:=0returnfunc()int{f()n+=1returnn}}//测试的调用函数funcfoo(){fmt.Println("callfoo")}funcmain(){cnt:=counter(foo)cnt()cnt()cnt()fmt.Println(cnt())}/*输出结果:callfoocallfoocallfoocallfoo4*/2)装饰器和中间件,即函数作为参数传递funcwrapping(ffunc()string){fmt.Println("domywork...")fmt.Println("wrappingfunction:",f())fmt.Println("myworkfinished!")}funcsayHello()string{return"Hello!"}funcsayByeBye()string{return"ByeBye!"}funcmain(){wrapping(sayHello)wrapping(sayByeBye)}/*输出:domywork...wrappingfunction:Hello!myworkfinished!domywork...wrappingfunction:ByeBye!myworkfinished!*/(3)map和arraymake的使用区别?go语言中new和make是内置函数,主要用来创建分配类型内存。new(T)创建一个没有任何数据的类型为T的实例,并返回该实例的指针;make(T,args)只能创建slice,map,channel,并返回一个有初始值args(非零)的T类型的实例,非指针。二者都是内存的分配(堆上),但是make只用于slice、map、channel的初始化,而new用于类型的内存分配,并且内存内置为0.make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。slice和array接近,不过更加的灵活,可以在新的元素加入的时候增加长度。slice总是指向底层的一个array。slice是一个指向array的指针,这是其与array不同的地方;slice是引用类型,这意味着当赋值某个slice到另外一个变量,两个引用会指向同一个array。例如,如果一个函数需要一个slice参数,在其内对slice元素的修改也会体现在函数调用者中,这和传递底层的array指针类似。append:向slice追加零值或者其他的x值,并且返回追加后的新的slice。如果原来的slice没有足够的容量,那么append会分配一个足够大的,新的slice来存放原来的slice和后面追加的值。因此返回的slice可能和原来的不是指向同一个arraySlice所允许申请的最大容量大小,与当前值类型和当前平台位数有直接关系slice扩容机制1.如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。2.如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。make创建make([]T,length,capacity)slice1:=make([]T,length)length表示slice中已经使用的数据长度capacity表示slice中指针执行的数组容量,如果缺省capacity和length大小相等(4)defer的工作模式go的defer语句是用来延迟执行函数的,而且延迟发生在调用函数return之后,同时defer的执行顺序和栈类似,先进后出(5)匿名函数的使用(难点)常用的感觉就是闭包和gofunc协程了,再说吧。。。。。二、面向对象和并发(1)go语言如何表现继承go中没有extends关键字,所以没有原生级别的继承。go本质上是使用interface实现的,是使用组合来实现继承,用组合来代替继承。在go的结构体struct,通过匿名成员的方式实现继承,比如,Student继承了MentypeMenstruct{namestringageint}typeStudentstruct{Menscoreint}对于接口interface,通过直接引入另一接口的方式实现继承,比如,ReadWriter继承了Reader和WritertypeReaderinterface{}typeWriterinterface{}typeReadWriterinterface{ReaderWriter}(2)接口的优点,使用场景。实现代码解耦(3)并发通信采用什么消息机制。goroutine+channel数据共享

2021-3-1 1180 0
区块链学习

一、共识相关1、POW共识工作量证明,按劳分配,算力决定一切,谁的算力多,谁记账的概率就越大。具体:找到一个hash值SHA256(SHA256(Block_Header)),使得新区块头的哈希值小于某一个指定的值,即区块头中的“难度目标”。找到之后,会全网进行广播打包的区块进行验证,验证通过,该区块会被接受,从而记录到账本中。2、POS共识股权证明,根据用户持有货币的多少和时间(币龄),发放利息的制度。纯POS机制的加密货币,只能通过IPO的方式发行,这就导致“少数人”获得大量成本极低的加密货币。信用基础并不牢固。出块方式是通过随机数。3、DPOS权益持有者选举任何数量的见证人来生成区块,类似人民代表大会制度。即将POS中多有可记账改为只有选举出的见证者才可以记账。优点是交易确认时间短。二、HyperLedgerFabric共识算法1、solo模式单一order节点出块2、kafka模式3、拜占庭容错三、Hyperledger工作流程Fabric基本的流程包括四个阶段,分别是模拟(simulate)、排序(order)、验证(validata)、提交(commit)模拟如其名字所说的一样,这一阶段只是模拟进行交易,并不真正更新ledger。client发起交易请求,请求被发送至endorsers(endorsementpeer,这些peer是根据endorsementpolicy选出来的),endorsers根据当前本地的ledger状态并行模拟进行这些交易,虽然不改变ledger状态,但是会产生一个readset和一个writeset记录这个交易的影响,模拟完成后,endorser对readset和writeset进行签名并将其一起返回给client。如果client收到的readset和writeset是一致的(可能存在恶意endorser或者智能合约存在不确定的算法导致出现不一致),那么client就会生成一个真正的交易请求,包含readset、writeset和对应的签名,并将这个请求发送给orderingservice。排序orderingservice对来自client的交易进行排序,需要注意的是这里并不检查交易的内容,默认按照交易到达的顺序进行排序(这种简单的排序可能导致大量的交易冲突,降低性能,如果按某一特定的顺序排序可以极大的较少交易冲突,提高吞吐量,这也是这篇论文提出的最重要的工作,这里就不细说)。orderingservice将交易排序后打包成block,发送给网络中的peers,这里不保证所有的peer同时收到这个block,但保证收到的block的顺序是一致的(使用gossip协议)。验证当peer收到block后,就开始验证阶段。验证阶段主要包括两个检查:EndorsementPolicy检查检查交易是否满足endorsementpolicy以及是否包含有效的签名,否则说明交易可能被client或者恶意peer篡改过,直接丢弃。交易冲突检查检查交易之间是否存在冲突,也就是是否读脏数据的问题(某个交易在读取ledger之前,ledger被前一个交易改变了),如果存在就丢弃该交易。两次检查都通过的话就可以进入commit阶段了。提交peer将block添加到链上,注意这里是所有的交易(有效的和无效的)都加进来了。然后根据有效的交易改变当前的ledger状态。参考链接https://blog.csdn.net/yijiull/article/details/94966044

2021-1-28 1564 0
区块链学习

1.理解记住最重要的一点,Dokcer实际是宿主机的一个普通的进程,这也是Dokcer与传统虚拟化技术的最大不同。Docker能保证运行环境的一致性,不会出现开发、测试、生产由于环境配置不一致导致的各种问题,一次配置多次运行。使用Docker,可更快地打包、测试以及部署应用程序,并可减少从编写到部署运行代码的周期。rootfs----内核空间是kernel,Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。用户空间的文件系统是rootfs,包含我们熟悉的/dev,/proc,/bin等目录。对于容器的镜像来说,底层直接用Host的kernel,自己只需要提供rootfs就行了,容器是共享主机的kernel。先简单理解docker的使用过程,它分为镜像构建与容器启动。镜像构建:即创建一个镜像,它包含安装运行所需的环境、程序代码等。这个创建过程就是使用dockerfile来完成的。容器启动:容器最终运行起来是通过拉取构建好的镜像,通过一系列运行指令(如端口映射、外部数据挂载、环境变量等)来启动服务的。针对单个容器,这可以通过dockerrun来运行。而如果涉及多个容器的运行(如服务编排)就可以通过docker-compose来实现,它可以轻松的将多个容器作为service来运行(当然也可仅运行其中的某个),并且提供了scale(服务扩容)的功能。简单总结:1.dockerfile:构建镜像;2.dockerrun:启动容器;3.docker-compose:启动服务;2.安装更新ubuntu的apt源索引sudoapt-getupdate安装包允许apt通过HTTPS使用仓库sudoapt-getinstall\   apt-transport-https\    ca-certificates\   curl\   software-properties-common添加Docker官方GPGkeycurl-fsSLhttps://download.docker.com/linux/ubuntu/gpg|sudoapt-keyadd-设置Docker稳定版仓库sudoadd-apt-repository\    "deb[arch=amd64]https://download.docker.com/linux/ubuntu\    $(lsb_release-cs)\   stable"添加仓库后,更新apt源索引sudoapt-getupdate安装最新版DockerCE(社区版)sudoapt-getinstalldocker-ce检查DockerCE是否安装正确sudodockerrunhello-world如果终端卡在Unabletofindimage'hello-world:latest'locally位置docker在本地没有找到hello-world镜像,也没有从docker仓库中拉取镜像,出项这个问题的原因:是应为docker服务器再国外,我们在国内无法正常拉取镜像,所以就需要我们为docker设置国内阿里云的镜像加速器;需要修改配置文件/etc/docker/daemon.json如下{"registry-mirrors":["https://alzgoonw.mirror.aliyuncs.com"]}为了避免每次命令都输入sudo,可以设置用户权限,注意执行后须注销重新登录sudousermod-a-Gdocker$USER3.启动与停止安装完成Docker后,默认已经启动了docker服务,如需手动控制docker服务的启停,可执行如下命令#启动dockersudoservicedockerstart#停止dockersudoservicedockerstop#重启dockersudoservicedockerrestart参考文章理解Docker镜像分层

2020-7-14 1699 0
区块链学习

1.Protobuf简介protobuf是google提供的一个开源序列化框架,类似于XML,JSON这样的数据表示语言,其最大的特点是基于二进制,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持php、java、c++、python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。protobuf在google中是一个比较核心的基础库,作为分布式运算涉及到大量的不同业务消息的传递,如何高效简洁的表示、操作这些业务消息在google这样的大规模应用中是至关重要的。而protobuf这样的库正好是在效率、数据大小、易用性之间取得了很好的平衡。官方文档http://code.google.com/p/protobuf/2.Protobuf如何工作你首先需要在一个.proto文件中定义你需要做串行化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。这里有个非常简单的.proto文件定义了个人信息:messagePerson{requiredstringname=1;requiredint32id=2;optionalstringemail=3;enumPhoneType{MOBILE=0;HOME=1;WORK=2;}messagePhoneNumber{requiredstringnumber=1;optionalPhoneTypetype=2[default=HOME];}repeatedPhoneNumberphone=4;}有如你所见,消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以是数字(整数或浮点)、布尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。你可以指定可选字段,必选字段和重复字段。你可以在(http://code.google.com/apis/protocolbuffers/docs/proto.html)找到更多关于如何编写.proto文件的信息。一旦你定义了自己的报文格式(message),你就可以运行ProtocolBuffer编译器,将你的.proto文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是query()和set_query()),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做Person。随后你就可以在应用中使用这个类来串行化的读取报文信息。你可以这么写代码:Personperson;person.set_name("JohnDoe");person.set_id(1234);person.set_email("jdoe@example.com");fstream.output("myfile",ios::out|ios::binary);person.SerializeToOstream(&output);然后,你可以读取报文中的数据:fstreaminput("myfile",ios::in|ios:binary);Personperson;person.ParseFromIstream(&input);cout<<"Name:"<<person.name()<<endl;cout<<"E-mail:"<<person.email()<<endl;你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。你可以在API参考(http://code.google.com/apis/protocolbuffers/docs/reference/overview.html)中找到完整的参考,而关于ProtocolBuffer的报文格式编码则可以在(http://code.google.com/apis/protocolbuffers/docs/encoding.html)中找到。3.Protobuf消息定义要通信,必须有协议,否则双方无法理解对方的码流。在protobuf中,协议是由一系列的消息组成的。因此最重要的就是定义通信时使用到的消息格式。消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式。字段格式:限定修饰符①|数据类型②|字段名称③|=|字段编码值④|[字段默认值⑤]①.限定修饰符包含required\optional\repeatedRequired:表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。②.数据类型Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型.protobuf数据类型描述打包C++语言映射bool布尔类型1字节booldouble64位浮点数Ndoublefloat32为浮点数Nfloatint3232位整数、Nintuin32无符号32位整数Nunsignedintint6464位整数N__int64uint6464为无符号整Nunsigned__int64sint3232位整数,处理负数效率更高Nint32sing6464位整数处理负数效率更高N__int64fixed3232位无符号整数4unsignedint32fixed6464位无符号整数8unsigned__int64sfixed3232位整数、能以更高的效率处理负数4unsignedint32sfixed6464为整数8unsigned__int64string只能处理ASCII字符Nstd::stringbytes用于处理多字节的语言字符、如中文Nstd::stringenum可以包含一个用户自定义的枚举类型uint32N(uint32)enummessage可以包含一个用户自定义的消息类型NobjectofclassN表示打包的字节并不是固定。而是根据数据的大小或者长度。例如int32,如果数值比较小,在0~127时,使用一个字节打包。关于枚举的打包方式和uint32相同。关于message,类似于C语言中的结构包含另外一个结构作为数据成员一样。关于fixed32和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求比较苛刻的环境,可以选择int32.③.字段名称字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的。protobuf建议字段的命名采用以下划线分割的驼峰式。例如first_name而不是firstName.④.字段编码值有了该值,通信双方才能互相识别对方的字段。当然相同的编码值,其限定修饰符和数据类型必须相同。编码值的取值范围为1~2^32(4294967296)。其中1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低(相对于1-15),当然一般情况下相邻的2个值编码效率的是相同的,除非2个值恰好实在4字节,12字节,20字节等的临界区。比如15和16.1900~2000编码值为Googleprotobuf系统内部保留值,建议不要在自己的项目中使用。protobuf还建议把经常要传递的值把其字段编码设置为1-15之间的值。消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。建议:项目投入运营以后涉及到版本升级时的新增消息字段全部使用optional或者repeated,尽量不实用required。如果使用了required,需要全网统一升级,如果使用optional或者repeated可以平滑升级。⑤.默认值。当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。当接受数据是,对于optional字段,如果没有接收到optional字段,则设置为默认值。关于importprotobuf接口文件可以像C语言的h文件一个,分离为多个,在需要的时候通过import导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同。关于package避免名称冲突,可以给每个文件指定一个package名称,对于java解析为java中的包。对于C++则解析为名称空间。关于message支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息。关于enum枚举的定义和C++相同,但是有一些限制。枚举值必须大于等于0的整数。使用分号(;)分隔枚举变量而不是C++语言中的逗号(,)eg.enumVoipProtocol{H323=1;SIP=2;MGCP=3;H248=4;}

2020-4-22 1676 0
区块链学习

https://github.com/tendermint/tendermint/blob/master/docs/introduction/quick-start.md参考以上github官方文档1、部署环境,编译安装tendermint官方快捷脚本(要fq)curl-Lhttps://git.io/fFfOR|bashsource~/.profile方便无法翻墙,复制如下#!/usr/bin/envbash#XXX:thisscriptismeanttobeusedonlyonafreshUbuntu16.04instance#andhasonlybeentestedonDigitalOcean#getandunpackgolangcurl-Ohttps://storage.googleapis.com/golang/go1.10.linux-amd64.tar.gztar-xvfgo1.10.linux-amd64.tar.gzaptinstallmake##movegoandaddbinarytopathmvgo/usr/localecho"exportPATH=\$PATH:/usr/local/go/bin">>~/.profile##createtheGOPATHdirectory,setGOPATHandputonPATHmkdirgoAppsecho"exportGOPATH=/root/goApps">>~/.profileecho"exportPATH=\$PATH:\$GOPATH/bin">>~/.profilesource~/.profile##getthecodeandmoveintoitREPO=github.com/tendermint/tendermintgoget$REPOcd$GOPATH/src/$REPO##buildgitcheckoutmastermakeget_toolsmakeget_vendor_depsmakeinstall2、单节点启动初始化节点,会在~/.tendermint目录下生成节点配置文件,包括公私钥、config文件以及genesis文件,也可以用--home参数自己指定目录tendermintinit启动节点--proxy_app参数指定上层server应用,kvstore是官方自带的键值对存储应用tendermintnode--proxy_app=kvstore发送交易测试发送交易:curl-s'localhost:26657/broadcast_tx_commit?tx="abcd"'交易查询:curl-s'localhost:26657/abci_query?data="abcd"'发起自定义键值对交易:curl-s'localhost:26657/broadcast_tx_commit?tx="name=satoshi"'通过key查询交易:curl-s'localhost:26657/abci_query?data="name"'3、单机多节点启动(疯狂踩坑)1、先总结坑github给出的操作方式是多机的,因为要修改各个节点ip网上有用docker做的,但是我不会用,写完文档就去学习docker去。。。单机情况下各个节点的p2p和rpc监听端口是冲突的,要修改参考了https://blog.csdn.net/weixin_37504041/article/details/92798787我一开始根据它这个做的,但是在添加验证节点时,在kvsore应用等待链接窗口出现验证节点链接后秒退的情况,我是用abci-clikvstore命令手动打开的kvsore应用,然后让节点一个一个链接的,和官方给的文档对比,官方是在启动节点时通过--proxy_app命令链接的,猜想可能是因为这个。另外还有一个问题,可能会碰到不能链接到server的问题,网上看到有人自己写的server,出现端口和配置文件不匹配的问题,我自己遇到的问题是因为kvsore未启动,所以无法链接。然后我手启遇到上面碰到的问题,最后还是参照官方做的,过程如下。2、单机多节点实验步骤总结1、官方给出了测试节点,初始化方法:tenderminttestnet2、获取各个节点id,记到文档里,会用到。tendermintshow_node_id--home./mytestnet/node0tendermintshow_node_id--home./mytestnet/node1tendermintshow_node_id--home./mytestnet/node2tendermintshow_node_id--home./mytestnet/node33、官方文档这里因为是多机,所以执行了以下命令tendermintnode--home./mytestnet/node0--proxy_app=kvstore--p2p.persistent_peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656"tendermintnode--home./mytestnet/node1--proxy_app=kvstore--p2p.persistent_peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656"tendermintnode--home./mytestnet/node2--proxy_app=kvstore--p2p.persistent_peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656"tendermintnode--home./mytestnet/node3--proxy_app=kvstore--p2p.persistent_peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656"如果你多机执行,就把id换成我们记下每个node的对应id,和该node对应的ip即可3、但是我们穷人只有一台电脑重点来了访问~/mytestnet,可以看到4个node的四个配置文档,四个都需要改,我们以一个为例访问./node1/config,编辑config.toml因为node0的rpc端口为26657,p2p端口为26656,所以我们把node1的rpc端口为36657,p2p端口为36656.同理把node2的rpc端口为46657,p2p端口为46656,把node3的rpc端口为56657,p2p端口为56656[rpc]laddr=“tcp://127.0.0.1:36657”[p2p]laddr=“tcp://0.0.0.0:36656”然后我们看一下persistent_peers这个配置选项,它配置的是peer节点,官方给的这个是根据它这个测试忘这4个节点初始化好的,如果是自己搭建是没有初始化的,我们需要自己添加。现在看下官方给的这个,@符号前面的node的id是它初始化好的四个节点id,不需要修改,@后面的node1:26656我们要改成127.0.0.1:36656,对应node1的ip:p2p端口,因为我们是单节点验证,所以ip都是127.0.0.1,P2P端口就分别是26656,36656,46656,56656.至此,实验环境配置完成,tendermintnode--home./mytestnet/node0--proxy_app=kvstoretendermintnode--home./mytestnet/node1--proxy_app=kvstoretendermintnode--home./mytestnet/node2--proxy_app=kvstoretendermintnode--home./mytestnet/node3--proxy_app=kvstore启动4个节点,即可进行测试。根据拜占庭共识,3f+1个节点可以运行f个恶意节点,所以在1个节点恶意情况下,可以完成共识出块。交易命令同上文单节点学习docker去了,告辞。