大家好,非常感谢大家在百忙之中抽空收听比原链技术入门课程,我是比原链技术运营经理钟立飞。
今天主要给大家介绍一些比原链的基础技术知识,希望能给大家带来一些启发。同时比原链的开发大赛也在进行当中,欢迎大家参加我们的比赛,我们准备了200万BTM的丰厚奖励,同时推荐身边的开发者参赛也能得到推荐奖励。(活动详情见报名网站:http://bytom.io/developers_zh/)
下面进入正题,本次分享主要分为以下几部分:
-
Bytom是什么
-
Bytom有哪些特性
-
Bytom的设计原理和技术架构
-
比原链的UTXO模型以及一些主要的流程
-
比原的智能合约和合约模板
1
比原链介绍
比特币是点对点的点子现金系统,以太坊是一个巨大的分布式计算机,那么比原链是什么?比原链想要连通原子世界和比特世界,促进资产在两个世界间的交互和流转。
我们知道区块链的创新之一就是解决了价值传递问题,传统互联网可以很方便地传递信息,但是并不适合于传递价值,因为不能保证我把某个东西给你,你多了而我少了,信息可以方便地复制和传播,结果往往是你有一份我仍然还有一份。而区块链通过一系列的措施较好地解决了这个问题,但是我们看到不论是比特币还是以太坊,它本身还是在虚拟世界中做价值传递,而比原链想打破这个界限,把数字世界和物理世界中的资产在比原链上登记,以比原链作为连接的桥梁,实现流通、对赌以及其他复杂操作。
当然在这个过程当中会有很多的难题,比如锚定问题、硬链接问题等,锚定和硬链接问题其实是一类问题(只是一个对虚拟资产,一个对实物资产),都是如何证明比原链上的某个资产实际对应某个虚拟或者实物资产。
-
对于其他虚拟资产锚定问题,我们会使用跨链等技术来将虚拟资产锚定到比原链上,同时通过ODIN标识来进行唯一性的标识,防止资产伪造。
-
对于硬链接问题,肯定绕不开监管等,如何将一些实物资产,比如房产、土地、消耗品等引到链上,这里肯定需要区块链的不断发展演进、政策和法律的完善,以及周边应用的完善,路漫漫其修远兮,吾将上下而求索,比原链将先易后难,不断实现自己的愿景和使命。
总之,比原链是想用区块链技术来实现各种资产交互的公链,作为资产专链,它比以太坊领域更垂直,比比特币更扩展。
2
比原链的特性
比原链的特性:
-
采用AI-POW算法作为共识算法
-
多资产的灵活交互能力
-
资产防伪ODIN标识
-
支持国密标识
本文重点分享AI-POW算法和资产防伪ODIN标识。
2.1 AI-POW算法
比特币的POW算法因为其能源消耗和矿机的利用率一直被人诟病。能源消耗作为保证系统的稳定性的代价也许还能说得通,就像为了挖掘矿产也需要消耗电力一样(当然因为竞争的原因导致过量的电力消耗也仍然是一个问题),但是矿机利用率的问题确实是一个纯粹的消耗,比特币的矿机除了做哈希计算,毫无其他利用价值,一旦新一代矿机到来,算力爆涨,那么旧的矿机将直接报废。
和以太坊、EOS等改造共识算法的做法不同,比原链团队在选择共识算法过程中仍然选择使用成熟稳定的POW算法,因为毕竟我们和资产相关,安全可靠是第一位的,(这个我后续也会讲我们的设计准则,包括最近以太坊推迟POS方案,也是考虑到一些风险),比原链通过引入AI友好型的POW算法,使针对比原的矿机除了能够挖比原之外,还能够做AI运算,从而能够给其他AI有需要的地方(比如企业、机构等)提高AI运算能力,比原看到未来AI的巨大潜力,认为跟AI结合也是一条有无限可能的道路。
2.2 ODIN标识
前面提到可以在比原链上登记无数的资产,那么如何保证某一种资产是真实的,不是其他人伪造进行欺诈的呢?我们通过ODIN的方式来进行鉴伪,ODIN的方式有点类似于域名,只不过域名是由中心化的服务器管理,而ODIN是写入到比特币区块上的,任何的资产ID在比原链上是具有唯一性的,但是名称却不一定,我们通过ODIN来做唯一性的映射,将资产的名称和资产ID进行关联,比原链会拥有一个根的ODIN标识,从而能够管理其下的二级ODIN标识,任何想要在比原链上注册唯一资产标识的团队或者个人都需要通过比原链的根标识进行认证,如果考虑到去中心化,那么可以由比原链团队和其他权威机构来共同管理这个权限。
多资产的交互能力我将在后续合约中进行一些描述。
关于支持国密标准主要也是为了能够促进在国内进一步落地,密码算法的供应链安全也是一块很重要的保障。
3
比原链设计准则
接下来介绍一下比原链的设计准则和技术架构。
3.1 比原链的设计准则
**安全性是我们考虑的首要准则。**因为我们是资产专链,将来会有各种各样的资产运行在比原链之上。
-
首先使用较为成熟稳定的关键技术,包括我们之前讲到的POW共识算法,其在比特币上稳定运行这么长时间,也很好地解决了双花的问题,我们沿袭了UTXO模型而没有采用账户模型,牺牲了部分的便捷性但是提升了安全性,我们知道在以太坊产生的诸多安全问题中,大部分是因为其合约账户编程漏洞从而遭到攻击,而且一旦攻击则会影响到全部的资产(token)。
-
比原链在开发过程中,会有详尽的代码审计,多人同时研究一段代码,最后在pr的过程中由架构师进行review再合并,同时具备较为完整的测试用例,从而从开发层保证安全性。
-
比原链还积极和其他的一些著名安全厂商合作,包括360、慢雾等团队,从更加专业的角度来保证比原链的安全。
模块化设计。
-
比原链整体会分为若干层次,每个层次之间功能清晰,分类明确,利于扩展。
-
比如网络层,主要做区块同步、节点广播等网络之间的事情;内核层做共识、验证的事情,其内部比较复杂但仍然具有相对清晰的逻辑,这个后边会详细进行阐述,同时各个模块之前通过一些接口进行关联,实现松耦合。
强扩展性。
- 比原链作为一个基础公链,肯定需要考虑将来面对各种各样的需求,那么如何去满足这样的需求,我们设计了完备的API接口,提供给各类周边和应用,当然这些API仍然还需要进一步扩展,同时我们各个模块之间松耦合,比如国密和ED25519密码库之间可以很方便进行替换,满足不同的业务需求。
3.2 比原链分层模型
比原链可以分为三个模块,从上到下分别为:钱包层、内核层和通信层,上图中的比例是指代码所占的份额。
-
钱包层大家平时接触比较多,收款或者打款的一个操作界面,比原现在有一个全节点钱包可以供大家使用。
-
内核层可以简单理解为分布式系统中大家都认同的一套规则,在通信过程中,两个节点需要有相同的规则,才可以达成共识,如果两个节点规则不同,那么其实是意味着分叉。比原链中内核层占有非常重要的位置,代码量也是最大的,占据了超过半数的容量。
-
通信层主要负责节点之间信息交互,包括区块同步、交易同步等,比如交易同步,你需要把交易发到全网每一个节点,最后由矿工节点打包,比原链的通信建立在比较成熟的点对点网络的技术之上。
3.3 比原链详细分层模型
上图展示的是比原链更加细致的分层模型。分层还是通信层、内核层和钱包层,除此之外内核层还有虚拟机,这个是为了运行智能合约用的,因为比原链支持图灵完备的智能合约,需要虚拟机进行隔离运行;在虚拟机之上是合约编译层,负责将equity语言编译成机器码从而能够在虚拟机中运行,内核层和钱包层都有数据库,比原链使用levelDB作为存储的数据库,根据各个层不同的需求来存储不同的数据。
接下了我们具体讲讲每一层。
内核层
内核层相对来说较为复杂,从最上层看,内核层有五个模块:孤儿块管理、共识层、区块树管理、数据存储层和交易池。
-
孤儿块管理是什么呢?因为比原使用POW共识算法,就是采用挖矿的方式来产生区块,如果我挖到了一个块-块A,你挖到一个块-块B,最后只有一个块会成为主链,另外一个块就成为孤儿块了,在以太坊中还会有叔块的概念,我们这里面统称为孤儿块,就是未成为主链的合法区块。孤儿块管理是一个模块,在某一高度出现多个块时,这个模块就负责收集和存储非主链的块。
-
共识层比较复杂,它也是全节点最核心的部分,当一个区块来的时候,我如何验证和确定这个块是合法的、我是否该认同这个块,这就是共识层的工作。我们把验证分为两部分:
一部分是区块头验证,需要验证父块的信息,父块是否存在,父块的高度,然后是验时间戳,这块比较复杂需要一系列的验证流程,然后就是pow的验证,要证明你有足够的算力计算出难度值,从而有这个出块或者是记账的权利;
另一部分是交易验证,这块我们比原设计中和区块链有点不一样,就是比原有个BC层,BC层是一个map,用户可以在这个map中找到所有跟交易相关的信息,专门设计用来提高交易验证的性能,在BC层我们把交易的常规数据都验证完毕,然后就是智能合约的验证,我们支持图灵完备的智能合约,所以底层有虚拟机层,这个前文讲过。在每个交易进入时,需要验证交易的每个输入,每个输入其实被一个智能合约所守护,把输入传输到虚拟机中,将用户放到隔离验证的参数也放进去,然后验证这个输入是否合法。
-
区块树管理,主要用来记录全网所有的区块,为什么我们说区块树管理而不是区块链管理呢,因为之前讲过会有孤儿块,可能在某个高度出现分叉的情况,所以通过树的形式记录了全网所有的块,我们还称他为block index,因为可以索引到比原所有的区块。
-
数据存储,就是把区块和其他的一些数据落盘做持久化存储。这里列出来两个存储的数据:第一个是区块数据,这个就是在网络上广播的原生区块信息;第二个是UTXO数据。为什么我们会存储UTXO数据呢?如果不存储UTXO数据,当有一笔交易来的时候,如果你要验证这笔交易的UTXO是否被使用了,你需要历遍所有的区块,对性能会有很大影响,如果存储了可用的UTXO数据,那么就相当于有了一个缓冲池,当交易到来时,你只需要查找该UTXO是否存在在数据库中即可。
-
交易池,当你发送了一笔交易到全网,如果这笔交易还未被验证,那么这笔交易是存储在每个节点的交易池中,也就是说交易池维护了全网已经发出但没有被确认的交易,跟它关联最大的是挖矿这个模块,如果我要产生一个新的区块,那么我需要从交易池中拿一系列的交易并把它打包成块,然后进行POW的工作量验算。比原链的交易选择使用FIFO,也就是先进先出的策略,就是按时间顺序,早入池的早打包,防止因为交易费低而迟迟得不到打包的现象,这块也很容易扩展其他的策略,要看具体的场景。
钱包层
钱包层分为四块:私钥管理、账户管理、资产管理和交易管理。
-
私钥管理,比如你用比原的钱包生成一个private key,就是用到这个模块,还有包括如果安全保护这个私钥,一般都需要进行加密存储,以及如何使用私钥进行签名和子私钥的派生。
-
账户层,其实在UTXO模型当中是没有账户的概念的(比特币是这样),只有私钥和地址,账户其实是上层的抽象,我们比原的设计中,在钱包层设计了账户的概念,每个账户可以拥有多把私钥,私钥的不同形式就组成了不同的账户(因为会有多签账户),可以把账户理解成私钥的一种表现形式。每个账户会有无限多的地址,这个地址是由派生公钥生成的,这个可以很好地保护用户的隐私,因为用户的资产分散在不同的地址当中,只追踪某个地址不可能追溯到用户的所有资产。
-
资产模块主要进行资产的创建、展示和管理,比原链支持多资产的交互,任何人都可以发行自己的资产,然后通过资产管理个人或者组织机构下面的资产。
-
交易模块换一种更确切的说法是跟我有关的交易数据,在区块链上每天可能会有无数笔的交易,但其中可能只有几笔是跟你相关的,那么交易模块会在本地把跟你相关的交易筛选出来,维护钱包端的UTXO的数据库,记录你本人所拥有的UTXO,这个和内核中记录的UTXO不同,内核记录了全网的UTXO,但并没有记录UTXO的详细数据,而只是记录了hash,而钱包端记录了跟这个UTXO相关的详细数据,包括跟哪个账户相关、在哪个区块产生出来的、输入输出等等。然后交易模块会进行交易的构建,如果你在钱包端发起一笔交易,那么就需要在这里进行交易构建和签名,相关的后边会有流程介绍。
通信层
通信层架构分为节点发现、交易同步、区块同步和区块广播。
-
节点发现是很独立的一块,在区块链的世界中,其实像一个黑森林一样,你不知道有哪些节点、这些节点分布在哪?你唯一知道的是有几个种子节点,这几个种子节点就像是一个个的灯塔,可以将新进入的节点引入到整个网络。节点发现就是完成如何连接这些种子节点,并获取种子节点连接的其他相邻节点,然后再向其他节点尝试握手,以此循环发现更多的全网节点,从而能够更快地连接入网络并进行相关信息的同步。
-
交易同步,当你把交易发到全网的时候,它会通过这个模块发送给相邻节点,然后再进一步扩散,直到全网都有这笔交易。
-
区块同步,又成为被动区块同步,比如我当前本地区块高度为1万,我重新连接入网,我发现了其他有个节点高度是4万,那么这个模块就会从1万开始不断地向4万高度的节点请求区块,获取区块并更新状态,丢给内核层去处理。
-
区块广播,新区块快速广播是主动将新的区块广播给其他人,如果只有区块同步被动地接受区块,那么比如我挖到一个新块,还需要等别人发现我的高度比他高才会进行同步,会有不少的延迟,特别是网络层数较多时。新区块广播就是直接将新产生的区块强制广播给相邻连接节点,这些节点在验证确实是最新块时他们也会调用这个模块进行扩散,从而提高了区块同步的效率。
3.4 区块的诞生
介绍完了各个层的架构之后,我们用一个例子来串一下。举一个非常有代表性同时也是入门区块链需要掌握的例子——区块的诞生,区块是如何被矿工挖出来,然后再传播到其他各个节点的。
首先就是挖矿相关的,矿工节点从交易池中读取一系列的未确认的交易,验证这些交易合法后打包成区块,然后计算区块头的默克尔树,做完后就形成了一个未进行工作量证明的区块,然后矿工进行工作量证明,也就是计算一个符合条件的难度值。当矿工计算出满足条件的难度值后,mining模块就会将这个区块交给内核共识层。
共识层处理这个新的区块,这个就比较复杂了,首先它会把这个区块重新进行一遍区块头验证,包括父区块、时间戳、默克尔树、POW验证等。然后就是交易BC层验证,比如UTXO是否被使用、输入输出平衡等等。BC层验证之后,把所有输入的智能合约(Control Program)传入虚拟机,让虚拟机做一个合约层的验证,以后大家如果要在比原链上开发Dapp,那么相关的代码都需要在虚拟机内进行一定的控制,比如gas机制等等。
合约层验证完成后,区块的验证已经通过了,证明这个区块已经合法了,我们要将这个区块落盘,不管是主链上的块还是孤儿块,都会落盘,我们会将区块数据落盘,同时如果你在这个区块中的UTXO有变动,那么我们会将UTXO进行更新,把已使用的删除把新的增加,否则就会引起双花了。
然后就是区块树的更新,区块树前面说过记载了全部的区块信息。区块树更新之后其实大部分工作已经完成了,需要做一些善后工作,第一个就是将交易池中把这些已经确认的交易删除,否则下次再去打包这些交易就非法了,然后共识层的工作已经做完,将区块交给通信层。
最后通信层通过两种方式把区块同步出去,第一种是快速同步,它会立马把新区块广播出去,为了让其他节点更好收到这个块,但是快速同步有个问题:如果你我之间的区块高度相差很大,比如你是2万,我是1万,你的第20001块传递给我也是无用的,因为我没有2万的父块无法进行验证,所以还需要正常的区块同步来进行被动同步。对于实践中来说,经常在线的节点,比如矿池,交易所,钱包等都会通过第一种方式来同步区块,而不经常在线的,比如用户偶尔使用的全节点钱包一般会触发第二种机制。
4
UTXO模型
比原链的BUTXO模型沿袭自比特币的UTXO模型,在UTXO模型上进行了革新,这张图是BUTXO模型的一个概览图,了解UTXO的朋友一定觉得很熟悉,每一笔输入和上一笔交易的输出所关联,这个和比特币是相同的,但比原链的一笔交易中可以包含多种不同类型资产的输入输出,同时会有一个MUX交易池结构作为交易输入输出的简化,还有一种issue的特殊交易类型,这种类型用于创建一种全新的资产。
4.1 UTXO和账户模型的比较
我们简要对比一下UTXO和账户模型的区别,以及为什么要选择UTXO。
如上图的表格所示:
-
在UTXO模型中,单个UTXO是没有状态的,整个UTXO组成了一个世界状态;而账户模型中,在合约对象中会关联所有的世界状态。
-
UTXO的交易通过UTXO的产生和消亡来代表,比如我向你转了100个BTM,那么我账户里的BTM UTXO被删除,你账户中多了一笔100面额的UTXO。而账户模型使用账户和合约之间的消息交互,在合约状态中记录某个人的账户余额变动。
-
UTXO主要的优点是隐私性好,你很难追踪一个账户中所有的地址;安全性高,攻击一个UTXO不会影响其他的UTXO;并发性,就是比如你手中有两个UTXO,其实你可以同时花出去,因为两个UTXO之间不会有关联。而账户模型就是易于理解和编码,我们知道现在中心化的世界中,大多数系统都是账户模型,所以账户模型深入人心,更容易被理解;同时账户模型存储了世界状态,更方便进行复杂场景的定制。
-
而相应的UTXO模型的复杂度较高,对于一些在账户模型中较为简单实现的功能就比较麻烦,可编程性较差。而账户模型中,特别是以太坊的合约暴露出来的巨大安全隐患,攻击合约账户将会影响所有的token安全。
4.2 BUTXO的创新
Butxo具备一些创新的特性:
-
引入了资产ID以及一套体系,从而可以方便地支持多资产的交互
-
引入MUX交易池结构,简化多资产交易的交互
-
创始合约,方便在比原链上发布资产
-
实现了BUTXO上的图灵完备的智能合约
4.3 BUTXO结构
上图所示是BUTXO的静态数据结构:
-
SourceID和sourcePos都是用于定位前一笔的输出,我们通过SourceID找到上一笔关联的交易,但是因为比原链是多输出的,所以还需要一个位置信息,标识是属于第几笔交易。
-
资产ID和资产数量不用多说。
-
Control Program也就是智能合约是很关键的一个信息,前面也说过合约层验证需要将合约传入虚拟机运行和验证,每个UTXO的输入都带有Control Program。
-
Address,地址分为账户下面的地址和合约的地址,分别用于锁定资产使用,可以没有Address,但是不能没有合约。
4.4 MUX交易池结构
比原链独创的MUX结构,为了使多资产的交互能够更加安全和方便。
-
定位输出,前面介绍的sourceID就是跟这个MUX的id相关联,通过这个ID找到这笔交易,从而验证这个UTXO确实存在;
-
MUX结构可以进行输入输出平衡的验证,这个跟架构里面讲到的bc层相关,MUX把所有的输入根据不同的资产类型进行归集,再跟所有的输出去比较,防止输出和MUX已经归集的输入不匹配,而在一个个去验证输出时,也不需要根据不同资产去累加输入,直接根据MUX已经归集的输入即可,方便了多资产多输入输出的验证。
-
MUX结构可以防止溢出,如果你再输入中恶意填入一些越界的值,比如你填充了一笔负值的输入或者超过上限的输入,会被MUX监测出来,从而防止溢出攻击
4.5 比原链交易过程详解
穿插一个交易过程的案例:比原链是如何将一笔交易发给别人的。
-
钱包有存储所有的UTXO信息,也就是之前说过的交易模块,这些UTXO就构成了你可以花费的余额。
-
钱包收到你的信息,你要向某人发送一笔,比如20个BTM给某个人,然后我们就会去找所有BTM的UTXO,多说一句,因为比原是多资产的,所以我们除了BTM的UTXO之外也会有其他的UTXO。钱包会去寻找合适的UTXO,但是这里必须了解的是:UTXO模型和账户模型不一样,你想发20个BTM就在余额减去20,不是这样的,你必须去找合适的面额,如果找不到怎么办,比如我只有一个10BTM数量的UTXO和一个30BTM数量的UTXO,我们不会选择10BTM,因为不够,所以只好选择30BTM的UTXO,这个时候因为多余10 BTM,所以我们必须把剩下的10个找回自己,所以就需要一个找零的机制,我们会向钱包申请一个找零地址,输入30BTM,输出有两个:第一个是要给其他人的20BTM,还有一个10个BTM找给自己的UTXO,然后钱包会帮我们构建这样的一个交易,输入30,输出20和一个10的找零,然后找到要花费的UTXO的智能合约,找到账户信息并加入到交易模板中。
-
放到交易模板之后,钱包会让私钥模块给钱包进行签名,代表我可以花费并且要花费这个30BTM的UTXO,签名完成后我们会将交易交给内核,然后由内核验证这笔交易,验证完成后将这笔交易放入交易池,然后再传输给通信层,然后再广播到相邻节点,这样一笔交易的发出也就完成了,当然这是一笔还未确认的交易,需要等待矿工的打包和确认,后边的流程就是区块的诞生流程了。
5
UTXO上的合约
比原链上的合约是构建在UTXO模型之上的,和以太坊的面向账户编程的模型不一样,比原链是面向资产编程,你的Dapp针对的是一个个的资产,对资产锁定、验证、加锁和控制。
比原链的合约是图灵完备,但也是无状态的,无状态的意思是你不能在一个资产合约中寻找到记录这个资产之前发送的事情,这块后续会考虑如何进行完善。对于比原链合约的安全性,因为每个资产都会有自己的合约,所以无法像攻击以太坊合约账户一样,直接影响其世界状态,从而导致大量的威胁,如果黑客攻击了比原链的上的一个UTXO合约,那么也仅仅影响该UTXO,而不会影响到其他UTXO。
5.1 创始合约
之前也说过,因为比原链上需要有各种各样的资产进行上链和锚定,需要一个可以产生资产的方式,就是使用创始合约来创造资产和它的数量,创始合约可以看作类似于ERC20的协议,但是又有很大差别,也是因为创建在UTXO模型之上,创始合约并没有合约账户,也不保存世界状态,你可以认为凭空创造了一个指定数额的UTXO。
5.2 合约模板的例子
最后我们通过两个合约模板的例子来结束本次的分享吧。
币币交易合约
我们先来看第一个币币交易合约,币币交易合约是由一个卖家发起的合约,卖家在这个合约中锁定固定数量的资产,比如将5个BTC放入合约中,它现在有两种解锁方式,对应的两种过程:Trade和Cancel;
对于Trade过程,只要别人发送了trade里面规定的对应资产种类,对应数量的资产给我,那么他就可以拿走我锁定的资产。如果在很长时间内都无法成功交易,那么合约发起人也可以通过Cancel方法取消这个合约,然后拿回自己锁在合约的资产,当然需要验证这笔资产是这个发起人的,不可能所有人都能取消这个合约从而拿到资产。这个合约可以作为一个去中心化交易所的
第三方托管合约
这个合约可以类比于在淘宝中买东西,买家可以类比于淘宝买家,托管方可以认为是支付宝,卖家类比为商家。买卖双方都是无信任的,买家的钱不是直接打给卖家,而是通过一个可信第三方先进行托管,如果卖家不发货或者想要欺诈,那么卖家无法拿到第三方托管的资产。
在第三方托管合约中,买方先将一定的资产托管给可信第三方,然后由第三方根据买卖双方的交易情况,选择approve操作还是reject操作,如果交易正常发生,那么托管第三方将触发approve操作,将买方锁定的资产打给卖方;如果交易异常,卖方欺诈,那么托管第三方触发reject操作,将资产退回给买家,从而保证整个交易过程中各方的利益。如果这个第三方欺诈怎么办?其实合约当中已经考虑这个问题了,买家锁定的资产的接收者只能是买家或者指定的卖家,第三方无法获取这笔资产,从而避免中间人作恶。
内容来源:HiBlock区块链课堂013期 钟立飞老师的线上分享《比原链技术入门》
本文编辑:Cynthia
点击“阅读原文”即可回听课程分享。
Blockathon|48小时极客竞赛,区块链马拉松等你挑战(成都)
时间:2018年9月14-16日
地点:成都高新区天府五街200号菁蓉国际广场2号楼A座12楼中韩互联网+新技术孵化器
-
招募50名开发者(识别下图二维码即可报名)
-
报名费100元为参赛押金,参赛者个人原因不能到场参加活动概不退款;参赛者全程参与活动,待活动结束后现场退还。9月14日18:00开始第一次签到,9月15日和16日每天早上都要记得签到哦。
-
主办方免费提供2天的食物、饮料,并为每一位参会者准备一件文化衫
来源:oschina
链接:https://my.oschina.net/u/3782027/blog/2049698