首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

以太坊那些事

随着世界范围内各种ICO项目的持续火爆,最近以太坊紧跟比特币的步伐,开始了飙升之路。而在和朋友、学生交流的时候,往往在一些细节上会出现疑问。特别是在读一些已经发表的论文时,也会看到关于智能合约的过高期望。为了使得技术讨论时,对以太坊有一个基本的认识,我们按照其2017年3月发布的白皮书,来认识一下这位明星加密货币。

~~~~以下大部分内容为翻译稿~~~~

(草草看了一下,内容有121页,太多了,幸好可以任性跳过不关心的内容。另外这白皮书还没章节号,只好自己凑合排了)

1 概述

1.1什么是以太坊?

以太坊是一个开放的区块链平台。任何人都可以通过以太坊运行和使用基于区块链技术的分布式应用。世界范内的许多人维护了以太坊这样一个开源项目。与比特币(单纯用作加密货币)不同,以太坊更为灵活、适用性广。使用目前的以太坊平台家园(Homestead)版本,任何人都能够容易的创建应用,安全的使用应用。

1.2以太坊如何工作?

以太坊的基本单位是账户。以太坊区块链记录了每一个账户的状态。以太坊区块链中的所有状态转换都是关于账户之间的价值和信息的变化(注意这种说法其实暗含了交易之间不再明显的形成交易链)。以太坊中有两类账户:

外部账户(EOAs),由私钥控制的账户

合约账户,由合约代码控制且只能被EOA“激活”的账户。

对于大部分用户而言,这两类账户的区别在于人控制EOA账户,代码控制合约账户。如果一定要说是人控制合约账户的话,也是因为合约代码明确了某个EOA可以操作该合约代码(例如调用智能合约的函数)。普通用户可以通过部署代码到区块链生成新的智能合约。

以太坊中用户也需要支付合约费用。这使得以太坊区块链可以对抗诸如DDOS或者无限循环之类的攻击。交易的发送方需要为其激活的每一行代码支付费用,包括计算和存储费用。这些费用的支付使用以太币(换算为汽油Gas)。

交易费由执行验证功能的节点收集。这些“矿工”负责收集、传播、验证、和执行交易。矿工负责把交易打包成区块(区块包含对以太坊区块链账户的“状态”更新的信息)。矿工之间竞争把自己的打包区块作为以太坊区块链的区块。一旦竞争成功,会得到以太币奖励。

以太坊中矿工竞争解决一个复杂的数学难题以“挖出”一个区块。这被称为“工作量证明”。以太坊采用了内存困难的计算问题。该问题的解决需要内存和CPU。解决该问题的理想硬件应该是通用计算机。这加强了以太坊的去中心化特点。

(译者:以太坊的工作方式就是由程序员生成合约,然后花费以太币(转化为汽油)部署合约,合约就是传统客户端-服务器架构下的服务器。所谓的部署就是把合约代码放到以太坊区块链上或者是通过该区块链可以找到的公开存储区中。【注意以太坊区块链是公开的,所以合约里面直接放解密密钥的操作显然就是把密钥告诉了所有人】。任何人使用一个客户端都可以调用这个合约中的函数。根据函数内部代码的不同,有些函数调用也不能执行,例如某个函数指定只有合约生成人可以执行,这就是所谓的接入控制。【注意传统上接入控制是在单机上实现隐私性的一种技术,但是这里是不行的,这里的接入控制仅仅是不执行代码而已,至于代码的细节,交易的细节全部是公开的。例如代码中写“如果合约执行人不是Alice就不给她数据A”,那么Alice可以通过阅读部署在区块链中的合约直接读取数据A!】合约执行的主体是矿工。)

(译者:从另外一个角度看以太坊所谓的以账户为基本单位。在比特币中,每一对公私钥对应以太坊的一个外部账户。每一个公钥都有该公钥所对应的UTXO值,该值就是该外部账户的状态。比特币交易所改变的是账户之间的UTXO值的状态。从这个意义上,以太坊的外部账户跟比特币地址没有区别!

所谓合约账户,合约是通过交易由用户部署上链的,部署之后总得知道从哪里去找这个合约代码吧,硬盘里的程序还有个程序名呢,所以这就类似个交易ID,后面会介绍以太坊确实从部署合约的交易ID的收据中能够找到合约账户的地址。

所以,单纯从账户的表达上是看不出来所谓的“账户模式”到底是个什么含义的。深究的话需要从交易的结构上讨论。以太坊交易仅仅包含from和to两个字段,仅明确了受影响的两个账户;而比特币交易是输入输出脚本,交易之间形成链接关系。所以账户模式是交易之间没有直接链接关系的一种模式)

2. 账户管理(请翻到37页)

2.1账户

账户在以太坊中处于中心地位。(如前所述),有两种账户EOA和合约账户。这里(1.4节)主要讨论EOA账户,简称账户。合约账户简称为合约。之所以还用账户来统称这两个概念是因为这两个实体都有状态:账户(EOA)有余额,合约(合约账户)有余额和合约存储区。以太坊网络的状态就是所有账户(包括EOA和合约)的状态。这些状态是通过每一个区块更新的(UTXO可以理解为比特币的状态),这些状态是网络(节点)需要达成共识的。

如果我们限制以太坊仅使用EOA,并且只允许在EOA之间发生交易,那么以太坊就成了比特币的一个“子币”,只能用来传以太币。

账户代表了外部实体,例如人,挖矿节点或者自动代理。账户使用私钥签署交易,所以EVM才能安全的验证交易发送方的身份(公钥)(例如某个用户部署了一个合约,这个部署合约的交易是签了名的;然后用户调用这个合约的函数,调用这个交易同样是签了名的。根据这两个签名可以判断是不是合约的Owner在调用合约;并且明确了账户模型的实质之后,可以提出交易重放的问题,并进而注意到账户中计数器的作用)。

(在66页还有一部分账户相关的内容)

EOA账户(1)包括以太币余额;(2)发送交易;(3)由私钥控制;(4)没有代码

合约(1)有以太币余额;(2)有代码;(3)交易或者消息(来自其它合同代码)可以触发代码执行;(4)执行的操作可以任意复杂(图灵完备),操作该合约的永久存储区,即合约可以有自己的长期状态,还可以调用其它合约。

以太坊区块链上的所有行为都由交易激活,而交易由EOA发起。每一次,当一个合约收到一个交易的时候,合约代码按照交易中的参数运行。合约代码在每个节点的以太坊虚拟机中执行,执行的时机是验证新区块时。

2.2 密钥文件

以太坊的密钥文件存储在keystore子目录中,是一个JSON文件。私钥用口令加密的。地址是公钥的后20字节。

2.3 生成账户

生成多签名账户(请翻到41页)

Mist 以太坊钱包可以生成多签名钱包(注意在账户模型下这个并不容易,素以才用了合约)。首先需要创建多个账户。然后确保一个账户有以太币,至少1.02个以太币,Mist需要确保多钱包合约具有足够的“汽油”来创建。接下来找到多签名钱包合约,设置初始化参数,包括“共同账户由X用户拥有,每天消费以太币的上限是多少,超出上限的需要多少个用户确认”。接下来把多个账户的地址也作为初始化参数写入合约。然后发出部署合约交易。等待该交易被矿工验证,就生成了一个钱包。客户端可以得到一个合约地址。(译者:这显然是个坑,创建钱包要花钱,花钱还要花花钱的钱)

3. 汽油和以太币(请翻到49页)

汽油的用途是衡量使用的网络资源的代价。发送一个交易的时候,人们希望每次这个交易执行的费用是一样的。(正如相同的距离相同的车两次出发油耗应该出不多一样)。所以汽油是不会作为币来发行的,否则会使得价格浮动,代价不一。

在以太坊中,以太币是变动的。如果以太币价格上升(例如从3000到6000),那么汽油价格应该下降(例如从1:1000到1:2000),(这样相同的法币所购买的汽油量基本是不变的(例如3000时买了1个以太币,换成了1000个汽油;在6000时买了0.5个以太币,还是能够买1000个汽油;如果一个加法操作10个油,那么在这两个时间里面一个加法操作都是30))。

汽油的相关概念包括油价,油价值,油上限,油费。其中油价值是静态的,用于衡量操作的花费,例如加法10个油。油价是汽油和以太币的兑换比例,是浮动的。油上限是一个以太坊区块中最大的油量,限制了一个区块中的交易数量,或者说确定了一个区块最大允许的计算开销,间接的限定了区块大小。油费是运行交易或者合约的时候需要支付的汽油量。区块的汽油量支付给竞争成功的矿工,或者是PoS选出来的打包实体。

(在后面的67页还在讲汽油的事情)

以太坊在区块链上实现的合约代码执行环境是以太坊虚拟机(EVM)。每一个节点都在验证区块时执行EVM。每一个节点对他们要验证的区块中的每一个交易在EVM中运行该交易所触发的合约代码。每一个节点进行相同的计算,存储相同的值。

当人们运行分布式应用dapp时,该应用与区块链交互,读取和修改它的状态。当合约被消息或者交易触发执行的时候,网络上每个节点都是执行每一个指令。对于每一个被执行的操作,有一个明确的代价,用汽油量表示。

油价尽管是由交易的发送方任意指定的,但是它本质上是由矿工确定的,因为油价过低,矿工可以拒绝处理交易(类似比特币交易费)。为了获得汽油,人们只需要向账户中充值以太币。以太坊客户端自动用账户中的以太币购买汽油,购买的油量就是人们指定的用于该交易的最大开销(交易中的STARTGAS字段)。

以太坊协议的每个计算步骤都收费。每一个交易都有一个汽油上限和油价。矿工可以根据这两个值决定是否接受一个交易。如果一个交易所引发的计算(包含交易中的消息和任意后续触发的消息)使用的汽油小于等于汽油上限,那么一个交易就是可以被接受的。否则,该交易及后续的消息所引发的状态改变全部取消(状态回到该交易执行前的上一个区块的状态),但是交易本身还是有效的,并且交易费也要交给矿工(赔了夫人又折兵)。如果汽油上限超过了实际的开销,交易执行完后剩余的汽油会重新转化为以太币返回账户中。交易的发起方不用担心汽油总量过高,毕竟(那么多节点看着呢)人们只会扣除交易实际消耗的汽油。这意味着汽油上限可以高过实际开销。

使用gasUsed表示交易所需要的交易;gasPrice是油价;总消耗Total cost=gasUsed*gasPrice。下面这个表是白皮书中给出的一些EVM操作所需要的油量。除了终止和自杀之外,没有不要钱的。

4. 以太坊网络

4.1 公有链,私有链和联盟链(翻到50页)。

目前大部分以太坊项目是公有链性质的。但是有时候私有链或者联盟链也是需要的。例如,一些水平的公司,像各银行,尝试把以太坊作为他们私有链的基础。

下面是从一个博客上抄下来的,它从授权的角度描述三者的不同。

公有链:任何人可读,任何人可写,任何人可以参与共识。作为取代可信中心或者半可信中心的存在,公有链由“密码经济学”来保障安全,即经济激励和密码验证的结合。使用工作量证明或者股权证明,一个实体对共识的贡献取决于该实体所拥有的经济资源。公有链是“完全去中心化”。

联盟链:(这里联盟用的是Consortium这个词)部分预先选择的节点才能参与共识。例如一个联盟有15个经济体,每个运行一个节点,其中10个签名才能生成确认一个区块有效。读的权限可以是公开的,也可能是限定的,或者仅头部根哈希是公开的,公众只能通过API有限的访问数据。联盟链通常称为“部分去中心化”。

私有链:写权限属于一个机构。读权限是公开的或者限定的。例如公众要求审计。

(译者:私有链通常认为是没有必要的。即使对外宣称是私有链,并且让公众可读,但是自己家的数据库随时可以搞一套假的出来,无非就是删除重建。一个机构多个部门那也属于多部门之间的联盟链,因为多个部门要达成共识(互相校验)才能写入。联盟链的数据是多个联盟节点共享的,联盟节点数量越多,数据保密的希望越小,所以最好不要假设联盟链的数据是私密的,最好是假设公开!

4.2 以太坊网络的连接(请翻到51页)

以太坊程序Geth会不停的尝试连接到网络中的其它节点,直到找到对等体(另一个客户端)。如果程序所运行的主机具有公有IP或者路由器上运行了UPnP协议,这个程序也会像服务器一样接收其它节点的连接。

Geth通过发现协议找到对等体。在发现协议中,节点之间通过微语(八卦)协议找到网络中的其它节点。在初始化时,Geth通过一些启动节点来感知网络,启动节点是硬编码到程序中的。(译者:换言之,把启动节点改一改,就是一个新的以太坊)

5. 挖矿

获取金币的过程和获取密码货币的过程有些类似,所有才有了挖矿这种说法。(其相似性表现在)黄金或者贵金属是稀有的,密码货币也是稀有的,增加密码货币总量的方法只有挖矿。在以太坊中这是类似的。以太坊发行之后(ICO之后),新的以太币来自挖矿。与贵金属挖矿不同的是,加密货币的挖矿过程也是维护该网络安全的过程,包含了生成、验证、发布和传播区块的全过程。(因此有下面的等式)

挖以太币 = 加固以太坊网络 = 验证

以太坊,与其它区块链技术一样,使用基于激励的安全模型。共识基于选择总难度最高的区块。一个矿工生成区块,其它矿工验证区块。验证的时候,除了一些正确性检查(例如语法、交易、区块)之外,区块还要符合一个给定难度的工作量证明。在以太坊的Serenity阶段,工作量证明有望替换为所有权证明。

以太坊工作量证明算法是Ethash,改自Dagger-Hashimoto算法。包含寻找随机数是的算法输出小于一个难度门限的过程。工作量证明算法的关键在于没有比枚举更好的策略去寻找那样一个随机数,同时验证又是平凡和方便的。Ethash算法的输出是均匀分布的,所以一般来说,找到一个合适的随机数的时间是与难度相关的。这样通过调整难度就可以调整出块的时间。

以太坊通过调整难度,确保出块的平均时间是15秒。以太坊的“晶振”时间基本是15秒。(译者:交易列表i+状态i------矿工验证交易,执行合约,出块--------交易列表i+1,状态i+1------;状态i你有一个变量赋值是“小明”。通过执行合约,在状态i+1这个变量赋值变成了“小白”;注意在这个过程过中,并不是“修改了”区块链,而是生成了新区块,区块链可修改何必叫做区块链呢?

6.交易

交易在以太坊中指一个有签名的数据包,里面包含从EOA账户发送给区块链其它账户的消息。

一个交易包含(1)接收方地址(2)发送方的数字签名(3)以“微”做单位的以太币“值”字段(4)可选的数据字段,包含发送给合约的消息(5)STARTGAS字段,代表该交易执行时允许的最大计算步骤(6)GASPRICE字段,代表发送方愿意为汽油支付的费用。

(上述交易中的消息在以太坊中是一个单独的概念)。合约具有向其它合约发送消息的能力。消息是虚对象,不进行序列化,仅存在于以太坊执行环境下。消息可以看成是函数调用。

消息包含(1)发送方(2)接收方(3)以太币值字段(4)可选的数据字段,包含合约的实际输入数据(5)STARTGAS字段,代表该消息所触发的代码执行时最大可花费的汽油量。

本质上,消息类似交易,只不过消息由合约生成。当合约执行CALL或者DELEGATECALL代码时,就会生成一个消息。消息的接收方会根据消息,运行合约代码。这样,合约之间可以相互调用。

7. 合约

账户交互示例——赌约(请翻到69页)

(合约本质上就是程序,程序设计里面最基本的一个方法是模块设计,合约自然也可以划分为多个子合约,通过合约间交互完成一定的功能)

在这个例子中,考虑Alice和Bob赌100Gav币,赌约是旧金山的天气会不会在未来一年超过35度。如果在任意时间超过了35度,Bob赢得赌约,获得Gav币。Alice非常关心安全性,她使用了一个多签名的合约账户,她的合约账户仅在有2个签名时才会转移账户中的钱。Bob则非常在意量子密码技术,他使用的合约账户确保只有在交易中的消息具有量子签名时才会转发该消息。这里假设用的签名算法是Lamport签名。

图1展示的是上述赌约的架构图。其中包括七个合约账户和Bob的一个外部账户,Alice的三个外部账户。

图1:天气赌约架构图

(考虑在未来一年内的某一天,旧金山的温度超过了35度),Bob就可以结束这个赌约。

1)Bob用其EOA生成一个交易。交易中包含一个消息。消息使用Lamport签名。交易发给Bob的转发合约账户;

2)合约账户调用Lamport签名验证合约验证消息的签名;

3)Lamport验证合约调用Hash算法合约验证签名;

4) 签名验证完成后,Lamport验证合约返回Bob的转发合约账户返回值;

5)Bob的转发合约转发消息给赌约;

6)赌约发消息给天气合约,确认曾经大于35度;

7)赌约发消息给Gav币合约,确认转移赌约的Gav币到Bob的转发合约账户中。

上述实现中的Gav币仅在Gav合约的存储区中存储。赌约的Gav币账户在Gav合约中就是以赌约地址为键值,以Gav币余额为值得一对数据。在收到赌约的消息后,Gav币合约仅仅是把赌约的Gav币余额减少,把Bob转发合约的地址对应的Gav账户的余额增加。

图2:Bob结束赌约时各个合约的调用和返回值(红色)关系图;(返回值应该是通过查询本地receipt获取的)

一般意义上,合约就是以太坊区块链中可以用特定地址检索到的一组代码和数据。合约账户之间可以传递消息。合约也可以完成计算任务。合约在以太坊区块链中以EVM字节码的形式存在。高级语言编译的合约,编译成字节码方能部署。

一个具体的实例性合约就是“Hello World”合约,如下所示:

contract HelloWorld{

event Print(string out);

function()

}

该合约每次执行会在区块链中产生一个新的记录(log)表项,该表项类型为Print,参数为“Hello World!”。(注意这里的输出并不是显示在用户的终端,它是在区块链中的。推荐一开始使用Remix尝试智能合约的编写)

部署合约的交易是空的地址(就是没有接收方),交易的数据字段就是合约代码。例如

var primaryAddress = eth.accounts[0]//合约发送方账户var abi = [{ constant: false, inputs: { name: 'a', type: 'uint256' }

}]合约中常量,输入,输出等信息var MyContract = eth.contract(abi)//生成合约对象(感觉更像一个合约类)var contract = MyContract.new(arg1, arg2, ...,

)

//部署合约对象(感觉更像实例化一个合约类的对象)

(部署对象后以太坊区块链会以收据的形式提供返回信息),因此可以如下定义回调函数,部署合约

MyContract.new([arg1,arg2,...,]

{from:primaryAccount,data:evmCode},

function(err,contract){if(!err&&contract.address)//没错且返回了合约地址

console.log(contract.address);//终端显示合约地址

}

);

(当合约部署好之后,可以通过交易或者Call的形式调用)。在调用合约时,通常先使用类似eth.contract()之类的抽象层函数生成一个对象,包含该合约的可用接口。例如:

varMultiply7=eth.contract(contract.info.abiDefinition);//感觉像模板

varmyMultiply7=Multiply7.at(address);//获取该地址的具体合约的接口

(获得接口之后,执行函数就行了,例如对下述合约:

contract test {

function multiply(uint a) returns(uint d)

{ return a * 7; }

}

发送交易或者本地执行都可以执行该合约:)

>myMultiply7.multiply.sendTransaction(3,{from:address})

"0x12345"

>myMultiply7.multiply.call(3)

21

发送交易会返回交易的ID(与比特币类似;参数含义参照部署合约的交易可知,3是参数,后面是地址,没有额外的数据;通常会引起状态或者账户余额的变化)。(通过客户端)执行Call只是在本地执行代码,返回结果,对区块链中的数据没有影响。

(当使用以太坊节点的RPC时,部署合约的交易会得到交易ID,而通过交易ID可以查看到该交易在矿工处执行的收据[receipt],包含合约的地址[说明合约地址与交易ID还是不一样的],收取的Gas,所在的区块等。可以使用函数来获取交易的收据。)

如果给定合约:

contract Multiply7 { event Print(uint);//这一句会导致收据中含有log function multiply(uint input) returns (uint) { Print(input * 7); return input * 7; }}

那么通过 交易调用合约之后,使用交易ID所获得的合约收据如下:

{blockHash:

"0xbf0a347307b8c63dd8c1d3d7cbdc0b463e6e7c9bf0a35be40393588242f01d55",blockNumber:268,contractAddress:null,cumulativeGasUsed:22631,gasUsed:22631,

logs:[{

address:"0x6ff93b4b46b41c0c3c9baee01c255d3b4675963d",blockHash:

"0xbf0a347307b8c63dd8c1d3d7cbdc0b463e6e7c9bf0a35be40393588242f01d55",blockNumber:268,data:

"0x000000000000000000000000000000000000000000000000000000000000002a",logIndex:,topics:

["0x24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da"],transactionHash:

"0x759cf065cbc22e9d779748dc53763854e5376eea07409e590c990eafc0869d74",transactionIndex:}],transactionHash:

"0x759cf065cbc22e9d779748dc53763854e5376eea07409e590c990eafc0869d74",transactionIndex:}

可以看到在该收据中有不少的重复内容,但是其中有用的信息是data,logIndex和topics。其中topics说明是谁输出的log,data是log的具体值。logIndex是索引。其中topics是使用sha3函数Keccak生成的,如下:

> web3.sha3("Print(uint256)")"24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da"

(通常来说,使用web3.js调用以太坊的函数会更为方便。上述实例的大部分内容都是采用了web3.js接口书写的,下面给出一个完成的使用we3.js部署合约使用合约的过程:)

部署合约:

var source = 'contract Multiply7

{ event Print(uint); function multiply(uint input) returns (uint)

{ Print(input * 7); return input * 7; } }';var compiled = web3.eth.compile.solidity(source);var code = compiled.Multiply7.code;var abi = compiled.Multiply7.info.abiDefinition;web3.eth.contract(abi).new(

,

function (err, contract) { if (!err && contract.address) console.log("deployed on:", contract.address); });//下面是返回值deployed on: 0x0ab60714033847ad7f0677cc7514db48313976e2

调用合约:

var source = 'contract Multiply7 { event Print(uint);

function multiply(uint input) returns (uint)

{ Print(input * 7); return input * 7; } }';

//这一部分主要是为了获得ABI,不一定非要有源代码这样获取var compiled = web3.eth.compile.solidity(source);var Multiply7 = web3.eth.contract(compiled.Multiply7.info.abiDefinition);var multi = Multiply7.at("0x0ab60714033847ad7f0677cc7514db48313976e2")multi.multiply.sendTransaction(6,

)

查看合约执行情况:

multi.Print(function(err, data) { console.log(JSON.stringify(data)) }){"address":"0x0ab60714033847ad7f0677cc7514db48313976e2",

"args": {"":"21"},"blockHash":

"0x259c7dc07c99eed9dd884dcaf3e00a81b2a1c83df2d9855ce14c464b59f0c8b3",

"blockNumber":539,"event":"Print","logIndex":0,

"transactionHash":

"0x5c115aaa5418118457e96d3c44a3b66fe9f2bead630d79455d0ecd832dc88d48",

"transactionIndex":0}

最后,在以太坊的账户中包含了一个计数器。这个计数器是在比特币中没有的。以太坊中这个计数器每发送一个交易,账户中的计数器就加1。

~~~~以上大部分是翻译稿~~~~

随笔:

1.本地执行sendTransaction会使得网络节点传播该交易,并进而在每一个节点的EVM中验证该交易。每一个节点执行该交易触发的合约时,该合约对外部合约的调用所引起的状态变化在每个节点都是相同的。本地执行call时,也就在本地了。

2.以太坊账户本来跟比特币地址差不多,但是以太坊的交易形式与比特币相差甚远。

3.以太坊有单独的存储网络(一个大硬盘),有共同的执行环境(EVM),跟我们传统上在计算机里面运行个Java程序差不多。感觉到的不同就在于矿工可以控制代码的执行顺序,交易或者合同可以影响合约的执行流程和输出结果。所以,合约能简单就简单点吧。KISS原则特别适用于合约的编写。

本文难免错漏,欢迎留言指出。

~~~结束~~~

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180113G0AE9200?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券