GithubHelp home page GithubHelp logo

blog's Introduction

听闻用issue写博客省事又自带SEO,为了更关注内容本身,尝试迁移至此。

原博客地址:

  1. maxisacoder.github.io (技术blog,一锅炖)
  2. blog.sina.com.cn/u/2647396281 (大学时期遗址)

blog's People

Contributors

maxisacoder avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

Dharma源码阅读笔记

Dharma Source Code Explore

0.目录

  1. 背景简介
  2. 协议简述
  3. 主要repo
  4. 代码流程及调用栈
  5. Dharma.js模块划分
  6. 合约模块划分
  7. Limitations & Opportunities
  8. References

1.背景简介

根据 Nadav Hollander(Dharma协议) 在白皮书中观点:

  • ERC20规范了众筹融资。
  • 债券融资市场很大,但面临着不透明和私人化的问题,作者期望可以 apply the token-sale model to debt fundraising.
  • ERC20发债会面临如下问题:
    • Retrieving machine-readable debt-specific metadata (e.g. principal, interest rates) associated with the assets.
    • Retrieving a history of payments between debtors and creditors in a debt asset's terms.
    • Pricing default risk into the debt asset's value.

故Dharma对自己的定位是:

  • Provide a permission-less, generic,decentrolized mechanism.
  • Debt assets of flexible type can be issued, sold, administered, and priced.

总结下来,Dharma的Scope包括:

  • 0x标准订单和协议 (握手,链下中继链上结算,标准订单格式)
  • 加入termContract协议扩展
  • 将订单拆出Issurance,用来ntf的发行
  • 加入underwriter角色

2.协议简述

包含四个角色,角色模型如下:

在0xProject的基础上加入了Underwritter这个角色,可以用来:

  • 帮助debtor发起订单,协商利率
  • 为debtor提供风险评级,提供一定程度的担保
  • 可能提供催收服务
  • 可能对违约的订单提供抵押和变卖服务

2.1 订单格式

Dbet Order

    struct DebtOrder {
        Issuance issuance;
        uint underwriterFee;
        uint relayerFee;
        uint principalAmount;
        address principalToken;
        uint creditorFee;
        uint debtorFee;
        address relayer;
        uint expirationTimestampInSec;
        bytes32 debtOrderHash;
    }

Issuance Commitment

  • Issuance 是Debt Oder的子集。
  • 表示 debtor 希望发行一个 non-fungible debt token。
  • 这个commitment的hash(下列数据的hash),被称为 agreementId,在整个交易过程中被作为Id使用。
    struct Issuance {
        address version;
        address debtor;
        address underwriter;
        uint underwriterRiskRating;
        address termsContract;
        bytes32 termsContractParameters;
        uint salt;
        bytes32 agreementId;
    }

签名

一个填完的订单应包含如下3个签名

  1. debtor对如下hash签名:agreementId + debtOrder的所有字段
    function getDebtOrderHash(DebtOrder debtOrder)
        internal
        view
        returns (bytes32 _debtorMessageHash)
    {
        return keccak256(
            address(this),
            debtOrder.issuance.agreementId,
            debtOrder.underwriterFee,
            debtOrder.principalAmount,
            debtOrder.principalToken,
            debtOrder.debtorFee,
            debtOrder.creditorFee,
            debtOrder.relayer,
            debtOrder.relayerFee,
            debtOrder.expirationTimestampInSec
        );
    }
  1. creditor最debtOderHash签名, 同debtor
  2. underwriter对如下hash签名:
    function getUnderwriterMessageHash(DebtOrder debtOrder)
        internal
        view
        returns (bytes32 _underwriterMessageHash)
    {
        return keccak256(
            address(this),
            debtOrder.issuance.agreementId,
            debtOrder.underwriterFee,
            debtOrder.principalAmount,
            debtOrder.principalToken,
            debtOrder.expirationTimestampInSec
        );
    }

注意到,underwriter在签名时不包含relayer和creditor的信息,这样设计的目的是:

  • 足够的安全性:underwriter只需要填写并同意上述信息,也即只要underwriter私钥未泄露,事后有人若修改上述信息,可以被核心合约发现。在creditor和debtor私下串通的情况下(如果两者通过订单获得了联系方式,而这种情况应当是被避免的。事实上,如果creditor和debtor互相可以联系,发生两方借贷也是可以被协议支持的。但是在四方借贷的模型下,我们所有考虑安全性的基础在于,应当假设creditor和debtor互相不可以联系。),这样的协议无法避免在两者事后重新签订合约时,underwriter的信息被恶意去除来减少费用的串通情况。然而之前讨论的方案,若每个人只对自己负责的信息签名,虽然可以简化协议,但是最后提交协议上链的一方,拥有所有其余角色信息的彻底删除或完整替代的可能性。
  • 简化协议握手流程:同时,underwriter对relyer信息和creditor信息并不关心。也即发送给relyaer之后不需要underwriter再重新签名

同时注意到:

  • relayer不需要签名:最后的订单一定由creditor提交上链,creditor无法直接私自篡改订单,侵害relayer利益。
  • 但creditor需要签名:因为要防止别人冒充creditor身份。(dharma为了简化,在creaditor提交订单时可省略验签)
  • debtOder中没有creditor的信息,而核心合约存储对应每个order的creditor,并由repay相关的合约完成还款。

具体流程及图示

  1. Debtor Order 握手
  2. Creditor 填单提交
  3. 核心合约校验订单,通过后存入订单Registry
  4. Debtor 还款
  5. 订单结束

握手流程官方图示

整体流程官方图示

2.主要Repo

dharma主要维护的项目包括如下:

  • dharma.js:Dharma合约的js封装
  • charta: Dharma合约和合约测试逻辑
  • dharma-chain:Dharma合约的测试私链,通过ganache --db导入已经部署的Dharma合约和状态
  • dharma-tutorial: 基于React的Dapp,实现了一个Debtor填单dapp,并通过Dharma.js调用合约

生态中,有两个参考relayer实现:

  • plex: 达摩团队官方维护
  • Bloqboard.com: 第三方团队基于达摩协议的relayer

3.代码流程

Dharma.js现阶段只实现了抵押借贷分期还款的接口,儿无抵押分期还款只提供了contract。
下面我们先介绍其基类合约分期还款(SimpleInterestTermsContract),再详述 CollateralizedSimpleInterestTermsContract 流程。

3.1 SimpleInterestTermsContract

3.1.1 协议参数

在分期还款协议中,协议通过debtOder中的termsContractParameters封装了如下参数:

                                        Location (bytes)	Location (bits)
                                        32                  256
        principalTokenIndex	            31	                248
        principalAmount	                19                  152
        interestRate                    16                  128
        amortizationUnitType            15.5                124
        termLengthInAmortizationUnits   13.5                108

其完整参数为:

    struct SimpleInterestParams {
        address principalTokenAddress;
        uint principalAmount;
        uint termStartUnixTimestamp;
        uint termEndUnixTimestamp;
        AmortizationUnitType amortizationUnitType;
        uint termLengthInAmortizationUnits;

        // Given that Solidity does not support floating points, we encode
        // interest rates as percentages scaled up by a factor of 10,000
        // As such, interest rates can, at a maximum, have 4 decimal places
        // of precision.
        uint interestRate;
    }

其中缺少的两个参数有下列逻辑得出. 其中debtRegisty()订单数据库中存储的 issuanceBlockTimestamp,为插入该条订单数据的时间, 也即债权人填单的时间。具体call stack路径是:

  • DebtRegistry: insert()
  • DebtToken: create()
  • DebtKernal: issueDebtAgreement()
  • DebtKernal: fillDebtOrder()
uint issuanceBlockTimestamp = contractRegistry.debtRegistry().getIssuanceBlockTimestamp(agreementId);
uint termEndUnixTimestamp = termLengthInSeconds.add(issuanceBlockTimestamp);

3.2 实例2:CollateralizedSimpleInterestTermsContract

分期抵押借贷是Dharma.js默认实现的借贷合约,值得我们完整研究。

从债务人发单到最后协议终止,完整流程简述如下:

  1. 债务人从客户端dapp上签名发起订单,调用dharma.jsDebtOrder类静态方法create()完成订单创建。
  2. 债务人调用 allowCollateralTransfer(), 同意抵押合约接受债务人的资产抵押。
  3. 债务人调用allowPrincipalTransfer(), 同意抵押合约接受债权人的本金抵押。
  4. 债权人填单:调用Debtor.fill(), 债权人签名填单,将合约上链。
  5. 债务人还款:调用makePayment(), 发起还款
  6. 债务人 return colleteral / 债权人 seize colleteral

接下的内容详述每一个步骤:

3.2.1 dharma.js 订单创建

  1. 准备各种参数,构造相应的参数类。
  2. 构造 CollateralizedSimpleInterestLoanOrder 订单
  3. 调用 CollateralizedSimpleInterestLoanAdapter, 将订单转化为标准的 DebtOrderData
  4. 加载 debtKernelrepaymentRouter, 并将其地址填入标准订单中。
  5. 创建标准订单类,并作为debtor进行签单。调用了Dharma的SignerAPI, 在下一章详述。(await debtOrder.signAsDebtor();

步骤3中,DebtOrderData的定义,也即白皮书中描述的标准订单格式:

export interface DebtOrderData {
    kernelVersion?: string;
    issuanceVersion?: string;
    principalAmount?: BigNumber;
    principalToken?: string;
    debtor?: string;
    debtorFee?: BigNumber;
    creditor?: string;
    creditorFee?: BigNumber;
    relayer?: string;
    relayerFee?: BigNumber;
    underwriter?: string;
    underwriterFee?: BigNumber;
    underwriterRiskRating?: BigNumber;
    termsContract?: string;
    termsContractParameters?: string;
    expirationTimestampInSec?: BigNumber;
    salt?: BigNumber;

    // Signatures
    debtorSignature?: ECDSASignature;
    creditorSignature?: ECDSASignature;
    underwriterSignature?: ECDSASignature;
}

注意到,此处创建 DebtOder还不会将订单发送到区块链。

3.2.2 Debtor同意资产抵押

调用栈及说明:

  • tokenAPI.setUnlimitedProxyAllowanceAsync(): 调用ERC20的approve()接口将代币抵押到tokenTransferProxy
  • debtOder.enableTokenTransfers(address, tokenSymbol): 调用tokenRegisy合约获得ERC20代币的合约地址, 并调用setUnlimitedProxyAllowanceAsync.
  • debtOder.allowCollateralTransfer(debtorAddress)

3.2.3 Creditor同意principle抵押

流程与Debtor同意资产抵押基本相同

3.2.4 债权人填单

调用栈及说明:

  • debtKernel.fillDebtOrder(): 调用核心合约,完成合约确认的填单。
  • OderAPI.fillAsync():先进行相关的订单校验,校验完毕后调用核心合约的填单逻辑。
  • DebtOrder.fill()

Dharma.js提交订单前,通过OrderAPI.assertFillableAsync()校验订单是否可以被填单:

  • 有效性校验:OderAPI.assertValidityInvariantsAsync()
    • 本金是否足够支付 relayer, underwritter,creditor的fee
    • 调用核心合约,查询订单是否已经debtor取消,因为debtor发单之后在fill之前都可以取消订单。
    • 查询订单Issurance是否已经被取消,因为debtorunderwritter在订单fill之前都可以取消订单。
    • 通过查询debtTokenContract合约,判断该订单中对应的IssuranceCommitmentHash没有已被使用。
  • 同意性校验:assertConsensualityInvariants()
    • 分别检验 Debtor, Creditor, UnderWriter 三者的签名
  • 校验债权人本金是否充足: orderAPI.assertCreditorBalanceAndAllowanceInvariantsAsync()
    • 校验债权人balance金额
    • 校验债权人allowance金额
  • 调用Adapter 的 validateAsync() 接口,校验是否creditor可以签单,在抵押借贷中:
    • 校验参数是否合法
    • 校验债务人的balance
    • 校验债务人的allowance
    • 注意,这一步校验不由合约重复保证,而是只发生在dharma.js对各个合约提供的adpater中进行,故债权人如果直接操作合约,也可以成功签单。

核心合约签单后

  • 重复校验上述逻辑
  • 调用 TermContractregisterTermStart,通知其合约开始
  • 核心合约将Creditorallowance作为本金转给 Debtor`
  • 核心合约将Creditorallowance作为手续费转给 Underwritter和Relyer fee

注意到,如果做法币的抵押借贷,在上述流程中:

  • 在第三步中的校验,只需要校验是否有足够的vena用来支付手续费。
  • 如果需要涉及法币交易,需要再添加Debtor确认收款的接口,在约定的时间窗口内要求Debtor确认,否则进入仲裁流程。同时在核心合约中维护Debtor是否已经收款的状态。
  • 如果需要仲裁,由核心合约调用仲裁合约,并维护仲裁结果,以判断资产归属权。

3.2.5 债务人还款

调用栈及说明:

  • RepaymentRouterContract.repay(): RepaymentRouter的作用是,帮助债务人得知还款地址,因为债务人在填单的时候不知道债券人的地址,同时,债券也有可能被转让。(转让债券只需要将NTF转让,并在DebtRgistry中调用modifyBeneficiary()
  • ServicingAPI.makePayment()ServicingAPI中的makePayment(),支持的是对agreementId发起的还款。并不要求一定是是debtorOder中登记得Debtor发起还款,任何人都可以为agreementId发起还款。
  • debtOder.makePayment()

3.2.6 合同终止

合同终止有两种情况,一种为债务人还清债务,赎回抵押物:

  • collateralizerContract.returnCollateral()
  • CollateralizedSimpleInterestLoanAdapter.returnCollateralAsync()
  • DdebtOder.returnCollateral()

一种为债务人还清债务,赎回抵押物:

  • collateralizerContract.returnCollateral()
  • CollateralizedSimpleInterestLoanAdapter.seizeCollateralAsync()
  • DdebtOder.seizeCollateral()

4 Dharma.js中类功能划分

4.1 Dharma

Dharmajs导出的顶层单例类,未按模块粒度导出。

  • 调用者引用时只需引入该类,并调用其构造函数构造其实例即可。
  • 一些基础的interface和贫血的class的类型,在 public static Types = DharmaTypes 保存引用。([]为何要在此保存,而不直接将其各种type直接export?)
  • 其余成员为8个API的类,分别为:
    • sign: SignerAPI;
    • order: OrderAPI;
    • contracts: ContractsAPI;
    • adapters: AdaptersAPI;
    • servicing: ServicingAPI;
    • token: TokenAPI;
    • blockchain: BlockchainAPI;
    • logs: LogsAPI;
  • 特别的,包含一个DebtOrder类,见一下章。
    public static Types = DharmaTypes;

    public sign: SignerAPI;
    public order: OrderAPI;
    public contracts: ContractsAPI;
    public adapters: AdaptersAPI;
    public servicing: ServicingAPI;
    public token: TokenAPI;
    public blockchain: BlockchainAPI;
    public logs: LogsAPI;

    private readonly web3: Web3;

DebtOrder

该类提供了大部分的需要Debtor或者Creditor通过Dapp调用的顶层接口, 该顶层接口典型的调用路径为 DebtOder.someMethod()--> **API.someMethod() --> Contract.someFunction()

Adapter

具有如下接口

export interface Adapter {
    fromDebtOrder: (
        debtOrder: DebtOrderData,
    ) => Promise<SimpleInterestLoanOrder | CollateralizedSimpleInterestLoanOrder>;
    toDebtOrder: (params: object) => Promise<DebtOrderData>;
    fromDebtRegistryEntry: (entry: DebtRegistryEntry) => Promise<object>;
    getRepaymentSchedule: (entry: DebtRegistryEntry) => number[];
    unpackParameters: (packedParams: string) => object;
    validateAsync: (
        order: SimpleInterestLoanOrder | CollateralizedSimpleInterestLoanOrder,
    ) => Promise<void>;
}
  • Dharma在核心协议和DebtOder基础上,实现了SimpleInterestLoanCollateralizedSimpleInterestLoan 两个具体的交易类型
  • 这两个具体的交易类型,分别具有各自的 OderTerm, 以及 TermsContract
  • Adpter 负责 Oder格式的转化

Wrappers

提供了如下合约的js封装,通过abi-gen生成,不对外部暴露。
- BaseContract,
- DebtOrderDataWrapper,
- DebtKernelContract,
- DebtRegistryContract,
- DebtTokenContract,
- DummyTokenContract,
- TokenRegistryContract,
- TokenTransferProxyContract,
- ERC20Contract,
- ERC721ReceiverContract,
- MockERC721ReceiverContract,
- TermsContract,
- SimpleInterestTermsContractContract,
- CollateralizedSimpleInterestTermsContractContract,
- RepaymentRouterContract,
- CollateralizerContract,
特别的,其中还包含了 DebtOrderDataWrapper的封装,是在DebtOrderData的基础上扩展了一些方法。

关于abi-gen, 是由Ox项目提供的工具:

  • 其提供了从ABI file生成 TypeScript Wrapper的命令行工具
  • 支持模板
  • 包含deploy()at()两个接口,主要用来获取已部署的合约Wrapper实例。

SignerAPI

提供了3中角色的签单逻辑,分别为:

  • asDebtor(DebtOrderData)
  • asCreditor
  • asUnderwriter

asDebtor为例,包含以下流程:

  1. 校验订单格式。
  2. 给未填写的字段填写默认值。
  3. 创建 DebtOrderDataWrapper, 并获取getDebtorCommitmentHash.
  4. 调用web3并转化为ECDSASignature.

ContractsAPI

从链上加载达摩协议,返回wrapper中定义的如下协议实例。

export interface DharmaContracts {
    debtKernel: DebtKernelContract;
    debtRegistry: DebtRegistryContract;
    debtToken: DebtTokenContract;
    repaymentRouter: RepaymentRouterContract;
    tokenTransferProxy: TokenTransferProxyContract;
    collateralizer: CollateralizerContract;
}

TokenAPI

包含对ERC20 token的如下操作:

  • 转账
  • 查账
  • 将代币抵押到tokenTransferProxy
  • 查询已抵押的代币余额
  • 查询支持的代币类型

OderAPI

包含对订单操作API,不同于DebtOder实例上的API是,这些API较之更加底层,且会被DebtOder实例调用。而DebtOder上面的API基本上只会被CreditorDebtor直接消费。

5 Charta中协议功能划分

  • DebetKernal: 填单,订单取消,订单验证。
  • DebtToken: ERC721兼容的Token,tokenId为Issurance的hash。
    • 债权 ntf mint。
    • 债权 ntf 转让。
  • DebtRegistry:
    • 管理issurance hash和具体信息。
    • 管理debtor名下的所有debts。
  • ContractRegistry:保存所有已部署的达摩合约的地址,用户编写的TermsContract兼容的合约如 SimpleInterestTermsContract,通过该ContractRegistry调用所有已部署的达摩合约。
  • Collateralizer:用户抵押ERC20的helper。seize或return的条件判断逻辑在此。
  • RepaymentRouter:由于debtor无法得知真正的还款人,debtor调用RepaymentRouter来还款。
  • TokenTransferProxy:用户的ERC20代币抵押到此合约,目前只有DebtKernalRepaymentRouter有权限转移用户抵押的代币。

5.1 如何实现自定义的协议

  1. Charta中,继承TermsContract,实现接口并部署。
  2. Dharma.js中,实现协议的Adpater接口。
  3. 实现该定制协议对应的wrapperschema
  4. Dharma.js最顶层的 DebtOrder与具体的termsContract有较大的耦合,故实现新的自定义合约在现有的状态下无法做到不修改 Dharma.js代码。

6 调试技巧

  • 前端: 可以基于 dharma-turorial,进行已有的dharma API的调用。
  • eth client:如果直接使用geth的client,获取contract引用实例交完所,所以可以直接进入charta的目录,执行truffle console,其中自带合约的引用。
  • dharmajs的sourceMap:前端用 yarn run eject 将webpack配置发射,然后在rule中加入以下配置,会加载dharmajs的sourceMap,但运行速度会相对变慢
      {
        test: /\.js$/,
        use: ["source-map-loader"],
        enforce: "pre"
      },

6.1 dharma.js如何找到已部署的合约地址,如何发起调用?[x]

  • 如何找到已部署的合约: 由合约的wrapper保存已部署的合约地址,并由js发起调用
  • 合约地址保存在 "@dharmaprotocol/dharma.js": "0.1.10"
  • 已部署的合约和状态数据存储在 dharma-chain的data中,由ganache--db 功能导入

7 达摩协议无法解决的问题及思考

基于综上对达摩协议的调研,在 OTC, 法币,仲裁,双向挂单

如何引入法币?

初步想法:
dharma协议过程可以简述如下:

  1. debtor generate issurance --> relayer。
  2. debtor collateralize。
  3. creditor fill order, 触发token的自动打款。
  4. debtor repay, 由tokenTransferProxy自动还款。
  5. debtor return collateral / crediter size collateral

注意到,如果引入法币,只影响3和4步骤的转账逻辑。故拟转账双方可以走如下流程:

  • 转账方打款,通知收款方收款。
  • 收款方确认,通知合约进行下一步操作。
  • 若收款方在一定的时间窗口未收到款项,转账方可以申请仲裁,合同流程挂起,进入仲裁环节。

实际逻辑可能比上述逻辑稍复杂,但主体逻辑应该为上述步骤。故我们可以引入Fiat合约,代理法币转账逻辑。提供相应的方法同时发射相应的事件,供Dapp或者Relayer调用或监听。

如何引入OTC

在上述引入法币的基础上,法币抵押借贷流程可整理为:
dharma协议过程可以简述如下:

  1. tokenSeller generate issurance --> relayer
  2. tokenSeller collateralize
  3. tokenBuyyer fill order, 触发法币支付流程。
  4. tokenBuyyer size collateral.

对比OTC和法币抵押借贷的逻辑,OTC交易可以看成满足一下要求的特殊抵押token借出法币的流程:

  • 一旦tokenBuyyer支付本金成功(未发生仲裁),抵押物便无法撤回,也即tokenSeller无法repay。

由于抵押逻辑发生在 collateral 合约中,我们可以实现另外一 OTC 合约来实现不同于collateral中的抵押物逻辑。

仲裁设计

目前的仲裁,只涉及法币的到账确认问题。故仲裁合约可以设计在所有合约之外,只负责对法币支付是否到账做仲裁,如何处理仲裁结果,由上层合约处理。未来为了支持非智能合约的代币支付,我们可能通过仲裁机制为其提供支持。同时,详细的仲裁细节和vena代币在仲裁中的激励涉及,需要另外专题详述。

如何支持双向挂单?

在dharma协议中,订单格式与协议流程只支持借贷者发起订单,债权人不能发起订单。而对应于OTC交易中,issurance只能由tokenSeller发起。

在达摩协议中,订单是以一下的步骤握手生成的:

  1. creditor/tokenSeller与underwriter协商,生成issurance。
  2. creditor/tokenSeller与relayer协商,获得relayer fee。
  3. relayer负责订单的宣传推广.
  4. debtor/tokenBuyyer负责填单和提交。

在上述流程中,订单的发起有如下限制:

  • underwriter只负责对creditor的担保。
  • 只能由creditor/tokenSeller单方面发起订单

如何做风控

  • 抵押物的价值是动态的。
  • 合约需要能通过抵押物的市场价值,要求补充抵押物,或者变卖一定的抵押物。
  • 现有代码逻辑无法判断抵押物市值。
  • 如果可以从可信预言机获得抵押物市值
    • 如何与已有contract结合
    • 调用者和被调用者分别是谁

References

[WIP] Geth Source Code Deep Dive

一、 可运行的geth源码阅读环境

本文档基于go-etherum v1.9.13,亦是bsc节点fork的eth版本起点。 目标在于以可debug的方式启动geth节点

准备config.toml

# Note: this config doesn't contain the genesis block.

[Eth]
NetworkId = 11133
SyncMode = "fast"
# DiscoveryURLs = ["enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.rinkeby.ethdisco.net"]
NoPruning = false
NoPrefetch = false
LightPeers = 100
UltraLightFraction = 75
DatabaseCache = 512
DatabaseFreezer = ""
TrieCleanCache = 256
TrieDirtyCache = 256
TrieTimeout = 3600000000000
EnablePreimageRecording = false
EWASMInterpreter = ""
EVMInterpreter = ""

[Eth.Miner]
GasFloor = 8000000
GasCeil = 8000000
GasPrice = 1000000000
Recommit = 3000000000
Noverify = false

[Eth.Ethash]
CacheDir = "ethash"
CachesInMem = 2
CachesOnDisk = 3
CachesLockMmap = false
DatasetDir = "/Users/lixianji/Documents/coding/play/go-ethereum/data/ethash"
DatasetsInMem = 1
DatasetsOnDisk = 2
DatasetsLockMmap = false
PowMode = 0

[Eth.TxPool]
Locals = []
NoLocals = false
Journal = "transactions.rlp"
Rejournal = 3600000000000
PriceLimit = 1
PriceBump = 10
AccountSlots = 16
GlobalSlots = 4096
AccountQueue = 64
GlobalQueue = 1024
Lifetime = 10800000000000

[Eth.GPO]
Blocks = 20
Percentile = 60

[Shh]
MaxMessageSize = 1048576
MinimumAcceptedPOW = 2e-01
RestrictConnectionBetweenLightClients = true

[Node]
DataDir = "/Users/lixianji/Documents/coding/play/go-ethereum/data/chain"
omitempty = ""
IPCPath = "geth.ipc"
HTTPPort = 18545
HTTPVirtualHosts = ["127.0.0.1"]
HTTPModules = ["net", "web3", "eth"]
WSPort = 18546
WSModules = ["net", "web3", "eth"]
GraphQLPort = 18547
GraphQLVirtualHosts = ["localhost"]

[Node.P2P]
MaxPeers = 50
NoDiscovery = false
# BootstrapNodes = ["enode://a24ac7c5484ef4ed0c5eb2d36620ba4e4aa13b8c84684e1b4aab0cebea2ae45cb4d375b77eab56516d34bfbd3c1a833fc51296ff084b770b94fb9028c4d25ccf@52.169.42.101:30303", "enode://343149e4feefa15d882d9fe4ac7d88f885bd05ebb735e547f12e12080a9fa07c8014ca6fd7f373123488102fe5e34111f8509cf0b7de3f5b44339c9f25e87cb8@52.3.158.184:30303", "enode://b6b28890b006743680c52e64e0d16db57f28124885595fa03a562be1d2bf0f3a1da297d56b13da25fb992888fd556d4c1a27b1f39d531bde7de1921c90061cc6@159.89.28.211:30303"]
# BootstrapNodesV5 = ["enode://a24ac7c5484ef4ed0c5eb2d36620ba4e4aa13b8c84684e1b4aab0cebea2ae45cb4d375b77eab56516d34bfbd3c1a833fc51296ff084b770b94fb9028c4d25ccf@52.169.42.101:30303", "enode://343149e4feefa15d882d9fe4ac7d88f885bd05ebb735e547f12e12080a9fa07c8014ca6fd7f373123488102fe5e34111f8509cf0b7de3f5b44339c9f25e87cb8@52.3.158.184:30303", "enode://b6b28890b006743680c52e64e0d16db57f28124885595fa03a562be1d2bf0f3a1da297d56b13da25fb992888fd556d4c1a27b1f39d531bde7de1921c90061cc6@159.89.28.211:30303"]
StaticNodes = []
TrustedNodes = []
ListenAddr = ":30303"
EnableMsgEvents = false

[Node.HTTPTimeouts]
ReadTimeout = 30000000000
WriteTimeout = 30000000000
IdleTimeout = 120000000000

准备gensis.json

替换alloc中的地址

{
  "config": {
    "chainId": 11133,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "istanbulBlock": 0
  },
  "alloc": {
    "xx": {
      "balance": "111111111000000000000000000000"
    },
    "xxx": {
      "balance": "22222111111111000000000000000000000"
    }
  },
  "coinbase": "0x0000000000000000000000000000000000000000",
  "difficulty": "0x20000",
  "extraData": "",
  "gasLimit": "0x2fefd8",
  "nonce": "0x0000000000000042",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "timestamp": "0x00"
}

运行

geth init --datadir ./data/chain genesis.json

本地启动geth

在goland中配置启动参数

  • 可以加入--rpc启动rpc端口
  • --debug显示debug输出

尝试发送交易

参考

二、代码主流程分析

cmd/main.go

  1. newApp
  2. 若无subcommand,则进入geth方法
func geth(ctx *cli.Context) error {
	if args := ctx.Args(); len(args) > 0 {
		return fmt.Errorf("invalid command: %q", args[0])
	}
	prepare(ctx) //1
	node := makeFullNode(ctx) //2
	defer node.Close()
	startNode(ctx, node) //3
	node.Wait()
	return nil
}

prepare

  1. 配置默认cache大小
ctx.GlobalSet(utils.CacheFlag.Name, strconv.Itoa(4096))
  1. set up gc
gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024)))
godebug.SetGCPercent(int(gogc))
  1. set up metrics
utils.SetupMetrics(ctx)

make full node

makeFullNode(ctx *cli.Context)

// 1. read config
stack, cfg := makeConfigNode(ctx)

// 2. eable eth service
utils.RegisterEthService(stack, &cfg.Eth)

// 3. enable whisper
// 涉及到 shh: whisper 协议 
// 用作dapp间,借助eth的p2p层进行通信
// ref:https://eth.wiki/concepts/whisper/whisper 
shhEnabled := enableWhisper(ctx)

// 4. config graphql service
utils.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.GraphQLCors, cfg.Node.GraphQLVirtualHosts, cfg.Node.HTTPTimeouts)

// 5. eth stats service
// 节点状态上报,通过websocket
// 配套使用 https://github.com/cubedro/eth-netstats
// https://github.com/cubedro/eth-net-intelligence-api
// https://github.com/ethereum/eth-net-intelligence-api
// https://eth.wiki/concepts/network-status
// 目前官方使用情况不佳,不过可以用作自己的节点监控
utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)

上述流程中,关键点在于RegisterEthService, 展开其流程,如下

func RegisterEthService(stack *node.Node, cfg *eth.Config) {
	var err error
    // light sync只支持启动les节点
	if cfg.SyncMode == downloader.LightSync {
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
			return les.New(ctx, cfg)
		})
	} else {
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
			fullNode, err := eth.New(ctx, cfg)
			if fullNode != nil && cfg.LightServ > 0 {
				ls, _ := les.NewLesServer(fullNode, cfg)
                // 如果开启light client,则在fullnode基础上加入les功能,以支持web3.les
                // les 协议:https://github.com/ethereum/devp2p/blob/master/caps/les.md
                // les 介绍:https://eth.wiki/concepts/light-client-protocol
				fullNode.AddLesServer(ls)
			}
			return fullNode, err
		})
	}
	if err != nil {
		Fatalf("Failed to register the Ethereum service: %v", err)
	}
}

start node

// 1. 启动节点
utils.StartNode(stack)

// 2. 启动wallet相关服务
events := make(chan accounts.WalletEvent, 16)
stack.AccountManager().Subscribe(events)

// 3. 启动geth client
rpcClient, err := stack.Attach()
ethClient := ethclient.NewClient(rpcClient)

// 4.注册lesservice,处理les请求

// 5. 处理POA共识

最终具体的节点启动逻辑来到了node.go 中的start方法。在后续系列中详细解析

node wait

func (n *Node) Wait() {
	n.lock.RLock()
	if n.server == nil {
		n.lock.RUnlock()
		return
	}
	stop := n.stop
	n.lock.RUnlock()

	<-stop
}

三. Node 启动流程分析

主流程

Node is a container on which services can be registered.
node.go中,start方法实现了对节点的启动

  • 构造services
  • 启动services对应的p2p server
  • 启动services对应的rpc server
// 1. create p2p server
running := &p2p.Server{Config: n.serverConfig}

// 2. start services p2p
// 其中、p2p协议会多个版本共存、该版本中eth service会同时启动63、64、65三个版本的p2p服务
// Gather the protocols and start the freshly assembled P2P server
	for _, service := range services {
		running.Protocols = append(running.Protocols, service.Protocols()...)
	}
	if err := running.Start(); err != nil {
		return convertFileLockError(err)
	}
	// Start each of the services
	var started []reflect.Type
	for kind, service := range services {
		// Start the next service, stopping all previous upon failure
		if err := service.Start(running); err != nil {
			for _, kind := range started {
				services[kind].Stop()
			}
			running.Stop()

			return err
		}
		// Mark the service started for potential cleanup
		started = append(started, kind)
	}

// 3. start services RPC
	if err := n.startRPC(services); err != nil {
		for _, service := range services {
			service.Stop()
		}
		running.Stop()
		return err
	}

Services

对于定制化的节点,可以实现Service接口,注入node
service接口如下:

type Service interface {
	// Protocols retrieves the P2P protocols the service wishes to start.
	Protocols() []p2p.Protocol

	// APIs retrieves the list of RPC descriptors the service provides
	APIs() []rpc.API

	// Start is called after all services have been constructed and the networking
	// layer was also initialized to spawn any goroutines required by the service.
	Start(server *p2p.Server) error

	// Stop terminates all goroutines belonging to the service, blocking until they
	// are all terminated.
	Stop() error
}

四、ETH service解析

Package eth implements the Ethereum protocol.

概况

ethereum struct

type Ethereum struct {
	config *Config

	// Handlers
	txPool          *core.TxPool
	blockchain      *core.BlockChain
	protocolManager *ProtocolManager
	lesServer       LesServer
	dialCandiates   enode.Iterator

	// DB interfaces
	chainDb ethdb.Database // Block chain database

	eventMux       *event.TypeMux
	engine         consensus.Engine
	accountManager *accounts.Manager

	bloomRequests     chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
	bloomIndexer      *core.ChainIndexer             // Bloom indexer operating during block imports
	closeBloomHandler chan struct{}

	APIBackend *EthAPIBackend

	miner     *miner.Miner
	gasPrice  *big.Int
	etherbase common.Address

	networkID     uint64
	netRPCService *ethapi.PublicNetAPI

	lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)
}

start 流程

func (s *Ethereum) Start(srvr *p2p.Server) error {

    // EIP778: Ethereum Node Records (ENR)
    // https://eips.ethereum.org/EIPS/eip-778
    // EIP778 提供了一套节点记录的编码规范、用于p2p以及其他类似DNS等协议之上的节点发现
    // EIP-2124:用于识别p2p网络中的协议分叉,可能可以用于快速扫描网络中的节点
    // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md
    // P2P 一节可以详细讨论一下这里
	s.startEthEntryUpdate(srvr.LocalNode())

	// Start the bloom bits servicing goroutines
	s.startBloomHandlers(params.BloomBitsBlocks)

	// Start the RPC service
    // ?? 这个API为什么传入的是p2p的server
	s.netRPCService = ethapi.NewPublicNetAPI(srvr, s.NetVersion())

	// Figure out a max peers count based on the server limits
	maxPeers := srvr.MaxPeers
	if s.config.LightServ > 0 {
		if s.config.LightPeers >= srvr.MaxPeers {
			return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, srvr.MaxPeers)
		}
		maxPeers -= s.config.LightPeers
	}
    // 协议层启动发生在这个方法
	// Start the networking layer and the light server if requested
	s.protocolManager.Start(maxPeers)
	if s.lesServer != nil {
		s.lesServer.Start(srvr)
	}
	return nil
}

ProtocalManager

func (pm *ProtocolManager) Start(maxPeers int) {
	pm.maxPeers = maxPeers

	// broadcast transactions
	pm.wg.Add(1)
    
    //txChanSize默认4096
    //** 这个比较关键、决定了txPool的大小,可以做一定的修改
	pm.txsCh = make(chan core.NewTxsEvent, txChanSize)
    // 订阅pending
	pm.txsSub = pm.txpool.SubscribeNewTxsEvent(pm.txsCh)
    // 广播pending给peer,** 可以只广播自己的交易
	go pm.txBroadcastLoop()

	// broadcast mined blocks
	pm.wg.Add(1)
    // ?? 不知道这个从哪儿订阅上的
	pm.minedBlockSub = pm.eventMux.Subscribe(core.NewMinedBlockEvent{})
	go pm.minedBroadcastLoop()

	// start sync handlers
	pm.wg.Add(2)
    // 开始sync
	go pm.chainSync.loop()
	go pm.txsyncLoop64() // TODO(karalabe): Legacy initial tx echange, drop with eth/64.
}

Sync

五、交易接收流程

上一片儿失联了
然后可能要起两个节点了,通过两个节点,来figure out

  • block的订阅
  • 状态更新

SendTx

我们从交易发送的线索开始挖,主要关注

  • 交易上链的过程
  • 状态更新过程

以sendTx为例,流程如下

// 1. ethapi/backend.go 定义API接口,对接口的创建在 node 包实现
SendTx(ctx context.Context, signedTx *types.Transaction) error

// 2. eth/api_backend.go 实现了 SendTx
func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
	return b.eth.txPool.AddLocal(signedTx)
}

// 3. 进入core/tx_pool.go
func (pool *TxPool) AddLocal(tx *types.Transaction) error {
	...
    pool.addTxs(txs, !pool.config.NoLocals, true)
}

// 4. 过滤已知交易、设置cache
pool.mu.Lock()
newErrs, dirtyAddrs := pool.addTxsLocked(news, local)
pool.mu.Unlock()

// 5. 加入池子 addTxsLocked
func (pool *TxPool) addTxsLocked(txs []*types.Transaction, local bool) ([]error, *accountSet) {
	dirty := newAccountSet(pool.signer)
	errs := make([]error, len(txs))
	for i, tx := range txs {
        // 把新交易加入池子
		replaced, err := pool.add(tx, local)
		errs[i] = err
		if err == nil && !replaced {
			dirty.addTx(tx)
		}
	}

// 6. pool.add 核心处理
// If a newly added transaction is marked as local, its sending account will be
// whitelisted, preventing any associated transaction from being dropped out of the pool
// due to pricing constraints. (*** local账户添加,防止因为价格原因drop)
func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err error)
// a. If the transaction is already known, discard it
// b. If the transaction fails basic validation, discard it
// c. If the transaction pool is full, discard underpriced transactions
// d. pricing bump : Try to replace an existing transaction in the pending pool
// 对同nonce替换的处理,参数决定了交易替换的最小gas比例,本地可能可以逃过,但网络中不太好跳过,看出块节点的配置
	// TxPoolPriceBumpFlag = cli.Uint64Flag{
	// 	Name:  "txpool.pricebump",
	// 	Usage: "Price bump percentage to replace an already existing transaction",
	// 	Value: eth.DefaultConfig.TxPool.PriceBump,
	// }
// e. 送入交易队列
pool.enqueueTx(hash, tx)

func (pool *TxPool) enqueueTx(hash common.Hash, tx *types.Transaction) (bool, error) {
	// Try to insert the transaction into the future queue
	from, _ := types.Sender(pool.signer, tx) // already validated
	if pool.queue[from] == nil {
		pool.queue[from] = newTxList(false)
	}
	inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump)
	if !inserted {
		// An older transaction was better, discard this
		queuedDiscardMeter.Mark(1)
		return false, ErrReplaceUnderpriced
	}
	// Discard any previous transaction and mark this
	if old != nil {
		pool.all.Remove(old.Hash())
		pool.priced.Removed(1)
		queuedReplaceMeter.Mark(1)
	} else {
		// Nothing was replaced, bump the queued counter
		queuedGauge.Inc(1)
	}
	if pool.all.Get(hash) == nil {
		pool.all.Add(tx)
		pool.priced.Put(tx)
	}
	return old != nil, nil
}

接收交易后,如果是miner,就需要进行交易打包了
如果作为普通的全节点,接下来主要是广播交易,
*** 广播交易和节点处理的链路,我们可以专门优化,此处可以测试一下典型的延时,应该链路上优化空间比较大

六、block insert

顶层从InsertChain开始track

// blockchain.go 
func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) 

-->
func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, error)
    // 处理区块
    receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig)
    // 写入区块
    status, err := bc.writeBlockWithState(block, receipts, logs, statedb, false)
-->
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error)

七、同步机制:SYNC

fast sync

An outline of the fast sync algorithm would be:

  • Similarly to classical sync, download the block headers and bodies that make up the blockchain
  • Similarly to classical sync, verify the header chain's consistency (POW, total difficulty, etc)
  • Instead of processing the blocks, download the transaction receipts as defined by the header
  • Store the downloaded blockchain, along with the receipt chain, enabling all historical queries
  • When the chain reaches a recent enough state (head - 1024 blocks), pause for state sync:
    • Retrieve the entire Merkel Patricia state trie defined by the root hash of the pivot point
    • For every account found in the trie, retrieve it's contract code and internal storage state trie
  • Upon successful trie download, mark the pivot point (head - 1024 blocks) as the current head
  • Import all remaining blocks (1024) by fully processing them as in the classical sync

这是相对省资源的一种方式,fast sync 的模式会下载 Header, Body 和 Receipt,插入的过程不会执行交易,因此也不会产生 StateDB 的数据,然后在某一个区块高度(最高的区块高度 - 1024)的时候同步完成所有的 StateDB 中的数据,作为本节点上的初始 StateDB Root 信息,最后的 downloader.fsMinFullBlocks(当前为 64)个区块会采用 full mode 的方式来构建。这种模式会缩小区块的同步时间(不需要执行交易),同时不会产生大量的历史的 StateDB 信息(也就不会产生大量的磁盘空间),但是对于网络的消耗会更高(因为需要下载 Receipt 和 StateDB),与 full sync 相比较,fast sync 是用网络带宽换取 CPU 资源。

SNAP sync

与fast sync对比

In the case of fast sync, the unforeseen bottleneck was latency, caused by Ethereum’s data model. Ethereum’s state trie is a Merkle tree, where the leaves contain the useful data and each node above is the hash of 16 children. Syncing from the root of the tree (the hash embedded in a block header), the only way to download everything is to request each node one-by-one. With 675 million nodes to download, even by batching 384 requests together, it ends up needing 1.75 million round-trips. Assuming an overly generous 50ms RTT to 10 serving peers, fast sync is essentially waiting for over 150 minutes for data to arrive. But network latency is only 1/3rd of the problem.

When a serving peer receives a request for trie nodes, it needs to retrieve them from disk. Ethereum’s Merkle trie doesn’t help here either. Since trie nodes are keyed by hash, there’s no meaningful way to store/retrieve them batched, each requiring it’s own database read. To make matters worse, LevelDB (used by Geth) stores data in 7 levels, so a random read will generally touch as many files. Multiplying it all up, a single network request of 384 nodes - at 7 reads a pop - amounts to 2.7 thousand disk reads. With the fastest SATA SSDs’ speed of 100.000 IOPS, that’s 37ms extra latency. With the same 10 serving peer assumption as above, fast sync just added an extra 108 minutes waiting time. But serving latency is only 1/3 of the problem.

Requesting that many trie nodes individually means actually uploading that many hashes to remote peers to serve. With 675 million nodes to download, that’s 675 million hashes to upload, or 675 * 32 bytes = 21GB. At a global average of 51Mbps upload speed (X Doubt), fast sync just added an extra 56 minutes waiting time. Downloads are a bit more than twice as large, so with global averages of 97Mbps, fast sync popped on a further 63 minutes. Bandwidth delays are the last 1/3 of the problem.

image

原理

yellow paper

  • MPT
  • word state 结构

image

八、P2P 工作流程

eth/backend.go

1. node start
err := n.openEndpoints()
--> server.Start() // server start 即为p2p模块的server

2. p2p server
- setupLocalNode
	- create rplx handshake obj
	- init or load node db: level db
	- initial NAT // ref NAT 网络类型 https://juejin.cn/post/6984996578587050021
- setupListening
	- tcp listen
	- update enr
	- server.listenLoop()
		- set max pending peers
		- accept and check inbound
			- 这里有一个"too many attempts"的控制逻辑为同一个peer每30秒只能尝试一次handshake
			- 这里使用ip控制使用代理池可以解决这个限制
			- `srv.inboundHistory.add(remoteIP.String(), now.Add(inboundThrottleTime))`
		- 记录metric
		- SetupConn
			- doEncHandshake // 秘钥协商
			- srv.checkpoint(c, srv.checkpointPostHandshake) --> 检查max peer 
			// 是否能在未建立连接的情况下获取到hello返回,不能
			- doProtoHandshake // rplx hello mesg,包含client id,即name,客户端版本
			- srv.checkpoint(c, srv.checkpointAddPeer) // 检查协议的兼容性
			

细节见文档 p2p.md

九、mining工作流程

1. cmd/geth 
threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name)
		if err := ethBackend.StartMining(threads); err != nil {
			utils.Fatalf("Failed to start mining: %v", err)
		}
2. eth/backend
// get miner  account
eb, err := s.Etherbase()
if parlia, ok := s.engine.(*parlia.Parlia); ok {
			wallet, err := s.accountManager.Find(accounts.Account{Address: eb})
			if wallet == nil || err != nil {
				log.Error("Etherbase account unavailable locally", "err", err)
				return fmt.Errorf("signer missing: %v", err)
			}
			// sign dat func & sign tx func
			parlia.Authorize(eb, wallet.SignData, wallet.SignTx)
		}
// start miner		
go s.miner.Start(eb)

3. miner/miner
// save start signal
func (miner *Miner) Start(coinbase common.Address) {
	miner.startCh <- coinbase
}

// start signal fire 
case addr := <-miner.startCh:
	miner.SetEtherbase(addr)
	if canStart {
		// start worker
		miner.worker.start()
	}
	shouldStart = true

4. commit to engine
// worker.go/commit: commit seal to task channal 
case w.taskCh <- &task{receipts: receipts, state: s, block: block, createdAt: time.Now()}:

// worker.go/taskLoop: commit to engine
w.engine.Seal(w.chain, task.block, w.resultCh, stopCh);

5. seal in engine

parlia/Seal: 
func (p *Parlia) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{})

val, signFn := p.val, p.signFn // val 是出块的签名地址
snap, err := p.snapshot(chain, number-1, header.ParentHash, nil)
// Bail out if we're unauthorized to sign a block
if _, authorized := snap.Validators[val]; !authorized {
		return errUnauthorizedValidator
}

// Sweet, the protocol permits us to sign the block, wait for our time
delay := p.delayForRamanujanFork(snap, header)

// Sign all the things!
sig, err := signFn(accounts.Account{Address: val}, accounts.MimetypeParlia, ParliaRLP(header, p.chainConfig.ChainID))

// send result
results <- block.WithSeal(header):


6. validators set 中的逻辑


十、共识

十一、level db

科普:web开发者入门区块链开发路径

这几日team里要来几个新同学,对区块链暂时没太多了解。故有了此篇blog,收集一些我看过的高质量的入门资料。这些资料收录的原则是,宁缺毋滥,尽量精简。

区块链基础知识

  • AA的 Mastering Bitcoin: AA是最负盛名的区块链技术传销大师,这本书内容较为精简,可以通过比特币来了解区块链系统。
  • Bitcoin WhitePaper:区块链从业者必读,区块链技术创世paper。
  • 普林斯顿的《Bitcoin and Cryptocurrency Technologies: A Comprehensive Introduction》:如果有时间的话,极力推荐阅读,目前看到的最成体系和学术化的区块链技术入门资料。对于工科生来讲可以零门槛入门的同时对整个事情有一个深入浅出的认识。前几章的基础知识和对比特币机理的讲解非常精彩,后面的一些章节比较长尾,可以跳过。
  • blockchain.info:区块浏览器,在看书过程可以配合玩一下。
  • bitcoin core:bitcoin node,可以在本机跑一个比特币的节点玩一下。

以太坊基础知识

dapp开发核心参考

DIP-3: Partial Fills

dip title author
3 Partial Fills Max Li <max>

Simple Summary

For debtor order with a big value of principleAmount, the debtor may want only fill part of it instead of the whole of it. This feature allows more agility and convenience for both parties of debt. The efficiency of the whole market will also increase.

This proposal introduces a straightforward design for this feature with the overhead of none-backward compatible NFT token. Whether there are more clever solutions for better compatibility is still remained to be explored by our communities.

Specification

Splitting out this issue, we need to deal with the following questions:

  1. Where and when the partial fills happen?
  2. How to record the partial filled?
  3. What the relationship between with NFT and the record?

Inspired by 0x Protocol, which do partial fill when call fillOder with a fillTakerTokenAmount for partial-fill-value for contract caller. A similar method can be considered by adding this parameter to fillOder. For supporting partial fills, DebtOrder struct need a minor revision by adding a minPrincipalAmount field.

    struct Issuance  {
        address version;
        address debtor;
        address underwriter;
        uint underwriterRiskRating;
        address termsContract;
        bytes32 termsContractParameters;
        uint salt;
        bytes32 agreementId;
    }

    struct DebtOrder {
        Issuance issuance;
        uint underwriterFee;
        uint relayerFee;
        uint principalAmount;
        + uint minPrincipalAmount; // value equal to principalAmount means no-partial-fill allowed
        address principalToken;
        uint creditorFee;
        uint debtorFee;
        address relayer;
        uint expirationTimestampInSec;
        bytes32 debtOrderHash;
    }

In original charta design, DebtRegistry contract records every occurred debt behavior on-chain. DebtToken contract get the hash of every record and mint token by the record hash. This proposal will follow this pattern for minimal changes. In DebtKernel contract, we save an orderRemain, which maps debtOder hash to current remaining for every order. This map can determine whether the remaining value is big enough for a fillDebtOder call.

    mapping (byte32 => Remainning) orderRemain;
    struct Remainning {
        uint total;
        uint min;
        uint remain;
    }
    function fillDebtOrder(
        address creditor,
        address[6] orderAddresses,
        uint[8] orderValues,
        + uint partialFillAmount, // for partial fill
        bytes32[1] orderBytes32,
        uint8[3] signaturesV,
        bytes32[3] signaturesR,
        bytes32[3] signaturesS
    )
        public
        whenNotPaused
        returns (bytes32 _agreementId)
    {
        + // check the exisitence of  order for given order hash
       DebtOrder memory debtOrder = getDebtOrder(orderAddresses, orderValues, orderBytes32);
       if (!orderRemain[orderBytes32]) {
            orderRemain[orderBytes32] =  Remainning (
                  debtOrder.principalAmount,
                  debtOrder.minPrincipalAmount,
                  debtOrder.principalAmount,
          }
       }
        
        + // Assert remain is enough for this fill
        require(orderRemain[orderBytes32].remain > partialFillAmount);

        // Assert order's validity & consensuality (some checking allowance value need also change)
         
        + // Mint debt token and finalize debt agreement
        issueDebtAgreement(creditor, debtOrder.issuance, partialFillAmount);
        orderRemain[orderBytes32].remain = orderRemain[orderBytes32].principalAmount - partialFillAmount;

        // Register debt agreement's start with terms contract
        // Transfer principal to debtor
        // Transfer underwriter fee to underwriter
        // Transfer relayer fee to relayer
    }

In the current design of NFT, the ERC-721 tokenId is set to agreementId(Issuance Id). In the context of partial fills, one order is corresponding to one issuance, but possibly multiple fills. Every occured fill event need a NTF for tokenlizing the creditor's right. So, there are avoidless breaking changes in DebtToken contract for minting this new NTF for every fill.

    function create(
        address _version,
        address _beneficiary,
        address _debtor,
        address _underwriter,
        uint _underwriterRiskRating,
        address _termsContract,
        bytes32 _termsContractParameters,
        uint _salt,
        uint _amount;
    )
        public
        whenNotPaused
        returns (uint _tokenId)
    {
        require(tokenCreationPermissions.isAuthorized(msg.sender));

        bytes32 entryHash = registry.insert(
            _version,
            _debtor,
            _underwriter,
            _underwriterRiskRating,
            _termsContract,
            _termsContractParameters,
            _salt
        );

       + // record the fill to registry
          // Fill struct record the related order, beneficiary, and partial-filled amount
              bytes32 fillHash = registry.insertFill(
             entryHash, (issurance hash)
             _beneficiary,
             _amount,
             _salt
        );
       
       + // mint by fillHash
        super._mint(_beneficiary, uint(fillHash));
        return uint(fillHash);
    } 

The meaning of agreementId is the unique key for all off-chain Order or on-chain Entry. Replacing this agreementId with fillHash for NFT minting, the NFT issued and the on-chain Entry can no long keep an one-to-one relationship. In this proposal, Fill is a structure looks like:

strtuct Fill {
   btyte32 agreementId;
   uint amount;
   uint salt;
   btyte32 fillHash;
}

TermsContract will retrieve order paraments from DebtRegisty by fillHash now. For example, querying _termsContractParameters by fillHash, DebtRegisty will get agreementId form Fill, than get _termsContractParameters from Entry.

Backwards Compatibility

This proposal introduce main changes including:

  • DebtKernal: adding partialFillAmount in DebtOrder definition. When this field is ignored, contract can treat the order with no-partial-fill supporting and keep compatibility.
  • NFT tokenId: this change need be considered with prudence. TokenId redifining will make all legacy minted tokens uncompatiable.

Copyright Waiver

Copyright and related rights waived via CC0.

Generator小结

前言

JavaScript异步从最初的callback,到promise,到Observable(RFP,reactive functional programming), 可谓各种范式不断演进,es6提出的Generator是什么玩法?和async/await有什么关系?和现在唱的很火的协程(coroutine) 之间的关系是什么?暂且跟随本文的思路一探究竟。

Generator

定义

Generator是一组可以暂停和继续的process

function* genFunc() {
    // (A)
    console.log('First');
    yield;
    console.log('Second');
}

种类

  1. 函数定义
 function* genFunc() { ··· }
 const genObj = genFunc();
  1. 函数表达式
 const genFunc = function* () { ··· };
 const genObj = genFunc();
  1. 对象字面量中的方法
 const obj = {
     * generatorMethod() {
         ···
     }
 };
 const genObj = obj.generatorMethod();

4 在类定义中的generator:

 class MyClass {
     * generatorMethod() {
         ···
     }
 }
 const myInst = new MyClass();
 const genObj = myInst.generatorMethod();

用途

1. 用来实现Iterables (data producer)

ES6 中 Iterator有如下的接口:

interface Iterator { // data producer
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}

事实上,Generator函数实现了ES6 Iterable接口,故可以用Generator函数实现可迭代的对象

function* objectEntries(obj) {
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        // `yield` returns a value and then pauses
        // the generator. Later, execution continues
        // where it was previously paused.
        yield [propKey, obj[propKey]];
    }
}

objectEntries() is used like this:
const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

这里需要注意的是,我们用(...)操作符,亦可以用来迭代这个Genrator生成的迭代器。事实上,spread
操作符的实际作用便是将可迭代的序列转化成数组。

function* genFunc() {
    yield 'a';
    yield 'b';
}
const arr = [...genFunc()]; // ['a', 'b']

2. 用来实现异步操作 (observer, data consumption)

广义的Observerr具有如下接口

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

接下来我们将列举分别用promise, Generator, 和es6 2017的async方法实现相同的异步逻辑:

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}
const fetchJson = co.wrap(function* (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});
async function fetchJson(url) {
    try {
        let request = await fetch(url);
        let text = await request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
}

3. 用作协程 (cooperative multitasking)

根据维基百科,协程有如下定义:

Coroutines are computer-program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations.
我们可以粗略得到以下总结:

  • 协程是在非抢占式的多任务系统中运作的子程序。
  • 因为费抢占式系统中需要子程序协同工作互让cpu时间,所以协程需要支持在特定位置的暂停和继续。

具体而言, ES6的结合了Iterator和Observer,具有如下接口:

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

为了更好地理解协程,我们先回顾一下JavaScript的并行哲学。运行时,js只会run一个process,但我们有一下两种方式用来处理并行程序逻辑:

  • Multiprocessing: Service Worker, 不同worker之间不会共享数据,除非主动发送数据拷贝。
  • Cooperative multitasking: 为了支持多任务,在任务切换时,协程需要支持挂起(explicitly suspend),并将全部控制权交出。

利用co库,我们可以实现自执行的generator,也即coroutine

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

利用generator实现coroutine,有以下的局限性:

coroutine 本来要求在一个coroutine中的任何函数都可以挂起整个coroutine(这个函数本身,或者这个函数的调用者,甚至函数调用者的调用者),但用generator实现的coroutine只能挂起generator内的的函数,即这个函数本身。故这种coroutine被称为shadow coroutine。

但同时,伴随局限性,带来的好处是:

  • 因为和JavaScript的event loop模型兼容性较好,这种基于generator的coroutine非常容易在浏览器上实现。(关于event loop,如果有时间,可以专门再开一篇issue来详述,可以参考 Exploring ES6 的 asynchronous programming 一章)
  • 同时,这种方式的multitasking保证了JavaScript的一个重要特性,也即 run to completion。所有的非Generator的函数都是事务性的,在完成前不会被任何其他任务打断。
实例:用Generator实现一个字符串流

有如下要求:

  • 将char流转化成words:/^[A-Za-z0-9]+$/
  • 提取数字:/^[0-9]+$/
  • 对目前收到的所有数字,输出当前所有收到的数字之和

短文:合格Blockchain Researcher的自我修养

如题所属,分享给立志成为Blockchain Researcher的要求

  • Write high quality and secure solidity contracts

  • Fluent in Python, GOlang, Java, or Node.js

  • Experience analyzing data structures and algorithms and issues related to scale, security and availability

  • Proficient with Merkle Trees, Fraud Proofs, RLP, UTXO, Priority Queues

  • Understanding of crypto-economic incentivisation

  • Database experience: preferably SQL, but noSQL is fine too

  • Familiarity with Ethereum/blockchains

  • Understanding we are a globally distributed, remote-first team; Comfortable with handling uncertainty and ambiguity

What we need to know about "Oracle" in the context of blockchain?

1. What is Oracle? Why concern?

'Oracle', not that database 'Oracle', is a term come from complexity theory and computability theory. According to Wikipedia [8], an oracle machine is an abstract machine used to study decision problems. It can be visualized as a Turing machine with a black box, called an oracle, which is able to solve certain decision problems in a single operation. The problem can be of any complexity class, which means that the problem does not have to be computable. Even undecidable problems, such as the halting problem, can be used.

In the context of blockchain technology, blockchain is defined by all the information that is publically available on the decentralized network. However, there come certain situations where the blockchain does not have access to information that is off of the chain.
Oracles provide the data that is required for these smart contracts to execute. These are external data feeds that are provided by third-party services and are designed to be used with these smart contracts. They will provide information to the smart contract such as whether a payment has succeeded, a price has reached some limit or even other external factors like the weather.
Oracles are essential to the functioning of the smart contracts. They provide essential inputs for all of these smart contracts and allow for the legitimate interaction of these contracts with real world and external factors.

2. Oracle research in the academic community.

There are centralized oracles, which pipe Internet information into Ethereum. For example, Oraclize and Real Keys. These oracles substantially suffer from central failure points. Shapeshift built a custom centralized price feed for their exchange Prism. MakerDAO implemented an Oracle controlled by the team’s multisig for their first stable coin Sai. The oracle is often the only centralized unit of such systems.

Prediction markets, such as Augur and Gnosis, can theoretically be used as price feed providers. In practice, however, it’s unlikely to be feasible soon due to the overhead added by the generic nature of these projects.

We will review the following oracle project in this chapter:

  • SchellingCoin
  • Augur
  • Oraclize
  • Real keys
  • Casper

SchellingCoin

SchellingCoin is posted by Vitalik, a mechanism that allows you to create a decentralized data feed.

This mechanism is how SchellingCoin works. The basic protocol is as follows:

  1. During an even-numbered block, all users can submit a hash of the ETH/USD price together with their Ethereum address

  2. During the block after, users can submit the value whose hash they provided in the previous block.

  3. Define the “correctly submitted values” as all values N where H(N+ADDR) was submitted in the first block and N was submitted in the second block, both messages were signed/sent by the account with address ADDR and ADDR is one of the allowed participants in the system.

  4. Sort the correctly submitted values (if many values are the same, have a secondary sort by H(N+PREVHASH+ADDR) wherePREVHASH is the hash of the last block)

  5. Every user who submitted a correctly submitted value between the 25th and 75th percentile gains a reward of N tokens (which we’ll call “schells”)

Potential attack for this SchellingCoin system are: 1. 49% coalition. 2. Micro-cheating. The interesting part about SchellingCoin is that it can be used for more than just price feeds. SchellingCoin can tell you the temperature in Berlin, the world’s GDP or, most interestingly of all, the result of a computation. Some computations can be efficiently verified; for example, if I wanted a number N such that the last twelve digits of 3N are 737543007707, that’s hard to compute, but if you submit the value then it’s very easy for a contract or mining algorithm to verify it and automatically provide a reward. Other computations, however, cannot be efficiently verified, and most useful computation falls into the latter category. SchellingCoin provides a way of using the network as an actual distributed cloud computing system by copying the work among N parties instead of every computer in the network and rewarding only those who provide the most common result.

Oraclize

One solution is to accept data inputs from more than one untrusted or partially trusted party and then execute the data-dependent action only after a number of them have provided the same answer or an answer within some constrains. This type of system can be considered a decentralized oracle system. Unfortunately, this approach has severe limitations:

  • It requires a predefined standard on data format
  • It is inherently inefficient: all the parties participating will require a fee and, for every request, it will take time before reaching a sufficient number of answers.

The solution developed by Oraclize is instead to demonstrate that the data fetched from the original data-source is genuine and untampered. This is accomplished by accompanying the returned data together with a document called authenticity proof. The authenticity proofs can build upon different technologies such as auditable virtual machines and Trusted Execution Environments.
image

image

The following authenticity proof is supported by oraclize:

  • proofType_TLSNotary
  • proofType_Android
  • proofType_Native
  • proofType_Ledger
  • proofStorage_IPFS

Note that, the TLSNotary is an open-source technology, developed and used by the PageSigner project. TLSNotary allows a client to provide evidence to a third party auditor that certain web traffic occurred between himself and a server. The evidence is irrefutable as long as the auditor trusts the server’s public key.

The full detail of this protocol is out of scope of this review, we put the flow chart of TLSNatory for reference here:

3. Oracle Example in Lending

[] bzx: call Kybernet work

[] Lendroid

ethfinex
dydx
ethlend

4. Oracle in Vena

5. References

  1. https://people.cs.uchicago.edu/~teutsch/papers/decentralized_oracles.pdf
  2. https://arxiv.org/pdf/1808.00528.pdf
  3. https://blog.aeternity.com/blockchain-oracles-657f134ffbc0
  4. https://bitcoinexchangeguide.com/oraclize/
  5. https://www.coinbureau.com/education/what-are-blockchain-oracles/
  6. Verify dilemma: https://eprint.iacr.org/2015/702.pdf
  7. TrueBits: https://people.cs.uchicago.edu/~teutsch/papers/truebit.pdf
  8. Oracle Machine: https://en.wikipedia.org/wiki/Oracle_machine
  9. Introducing Oracul: https://medium.com/@roman.brodetski/introducing-oracul-decentralized-oracle-data-feed-solution-for-ethereum-5cab1ca8bb64

MakerDao Deep Dive

Overview of MakerDao

  • Rune Christensen
  • 2015.8: first github commit
  • 2017.12: single-collateral live on Mainnet
  • MKR: 7w BTC, rank 22, target of ICO, Total token supply: 1,000,000 MKR
  • DAI: 1w BTC

CDP work flow

  • Creating the CDP and depositing collateral PETH
  • Generating Dai from the collateralized CDP
    • Pooled ETH: 1:1 ETH
    • DAI price is peged with 1USD
  • Paying down the debt and Stability Fee
    • The Stability Fee can only be paid in MKR
    • Once the user sends the requisite Dai and MKR to the CDP, paying down the debt and Stability Fee, the CDP becomes debt free
  • Withdrawing collateral and closing the CDP

Stability Mechanism

The TRFM is prominently described in their current white paper, but it is now no longer meant to be part of the Dai protocol.

Stability Mechanisms (Dai is not redeemable)

  • Global settlement expectations
    • why? a last resort for guaranteeing Dai’s pegged price
    • when?
      • if the Dai price goes too far off the peg for too long
      • long term market irrationality,
      • hacking or security breaches,
      • system upgrades.
    • who? Triggered by global settlers, who are elected by MKR holders
    • comments: depends on elections and human actions, there is a lot of uncertainty around when and if it will happen
  • Stabilizing speculation (套利)
    • downwards stabilization,
    • upwards stabilization
    • Comments: a second-order stabilization mechanism
  • Price manipulation by CDP creators
    • this stabilization mechanism is secondary
    • when DAI is failing
      • buy DAI
      • wipe and close CDP
    • when DAI is rising
      • open CDP
      • sell DAI
  • Off-Chain Stabilization
    • MKR token currently has a much larger market cap than Dai
    • buy walls
    • sell walls
    • counterparty risk
    • scale problem

Criticism: Dai Does Not Scale Well

  • Demand for CDP debt is limited
    • use case:
      • debt
      • leverage
    • Currently only eth, more type of token need to be supported.
    • Overcoming the competition from other issuers of collateralized debt
      • On-chain lending has low counterparty risk.
      • Dai has very low interest rates, pay 0.5% annually.
      • Low maximum leverage. (3x)
      • Lack of standard margin calls (在爆仓前不会提示用户)
      • Auto-liquidation penalty.
    • solution: offer nagetive interest rate for CDP
      • dilute MKR and pay for CDP
      • Pay less DAI, make system undercollateralized
      • Direct tax to hold Dai
  • Diversification Further Limits Dai’s Supply
    • they must either sacrifice Dai’s resilience or its scalability.

Resilient to crashes

  • Really bad for CDP holders to get auto-liquidated
    • 13% penalty
    • keep track of when their collateral value drops and to do what they can to increase their collateralization ratio when it happens
    • by buying and adding more collateral or by buying up Dai to pay off their debt
  • there is a decent buffer between the liquidation ratio of 150% and the point where Dai becomes undercollateralized at less than 100% overall backing
  • diversified collateral backing

Oracle Set Design

MakerDao目前有两个版本的Oracle。一种为直接的喂价加中位数机制,另外一种利用signed message进行链下的计算并上链。

方案一

该方案的代码在一下3个repo,同时也是现行方案

Independent price feed operators constanly monitor the reference price across a number of external sources and will submit updates to the blockchain when:

  • Source price differs to the most recently submitted price by more than the defined amount (currently 1%)
  • Last price update was more than 6 hours ago.

简单画了流程图如下:
11291540278713_ pic_hd
可以看到,上图中的operator是服务器上的中心化程序,实时监控币价波动,在满足上述时间或者波动幅度条件的情况下,调用合约的poke方法,以更新medianizer合约中的最新价格中位数。上述的预言机设计和代码对我们的参考价值很大,初步我们可以直接借鉴这种设计来实现我们的预言机机制。

方案二

该方案的代码在一下2个repo,是未来的makerdao的预言机系统,主要使用了链下的签名消息来节约手续费,目前还没有上线主网,安全性有待检验。时间所限,只把repo列出,详细的原理和代码视makerdao上线后的实际效果后续跟进。

Liquadation Process

MakerDao的平仓机制,只支持一种抵押物的Sai中,和在支持多种抵押物的Dss中,是完全不同的。由于dss现在系统还处于不成熟阶段,我们主要介绍Sai的liquadation流程。

在MakerDao中,keeper这个角色会对所有链上CDP的抵押率做实时的监控。当发现某个CDP的抵押率低于150%的时候,调用tub合约的bite(cup)方法,其中tub合约的作用是 CDP record store, cup的参数是指某一个cdp的编号。 激励keeper角色去主动监控抵押率并调用bite方法的动力有以下两点:

  • The price discount for liquidated assets is 3%. (进入拍卖流程的PETH资产享有3%的价格优惠)
  • Keepers who flag CDPs for liquidation are not given the 13% penalty fee. The liquidation penalty is used to burn PETH tokens. (13%的penalty fee并非直接给到keeper,而是用来在整个PETH的池子中销毁等值的PETH,从而使PETH的持有者获益。)

在Maker的术语体系中,bite()方法完成了下面的事情。
image
其中涉及到的术语如下:
SIN: token DAI 的镜像,表示CDP中待偿还的token数目。
Tub: CDP record store
Tap: The Liquidator
woe: SIN balance, bad debt transferred from bite
fog: PETH balance, collateral pending liquidation
air: PETH backing CDPs
ice: SIN locked up with CDPs

代码如下

    function bite(bytes32 cup) public note {
        require(!safe(cup) || off); // 判断是否真的抵押率低于150%

        // Take on all of the debt, except unpaid fees
       // // 计算当前这个CDP中有多少未还的DAI,
       // 将这一部分用SIN表示的债转到tap合约中, 并清空CDP中的债务
        var rue = tab(cup);
        sin.mint(tap, rue);
        rum = sub(rum, cups[cup].art);
        cups[cup].art = 0;
        cups[cup].ire = 0;
       
        // Amount owed in SKR, including liquidation penalty
        // owe表示可以拍卖的peth数目,并将这部分peth转到tap合约中,
        // 计算后剩余的peth可以留给cdp的开单者提取
        var owe = rdiv(rmul(rmul(rue, axe), vox.par()), tag());

        if (owe > cups[cup].ink) {
            owe = cups[cup].ink;
        }

        skr.push(tap, owe);
        cups[cup].ink = sub(cups[cup].ink, owe);
    }

如上述代码,在bite方法被调用之后,tap合约中的 Sin 表示需要换上的DAI的数目,而SKR 表示可以卖的PETH数量,这两者的比例就可以算出一个拍卖池子中PETH的价格。当SIN归零的时候,表示整个系统中所有处于liquadation阶段的CDP欠下的债务都已经还清,也就完成了CDP的清算。

一点感慨

MakerDao是一个非常吸引我的项目,他在以下的几点都是有非常独特的价值:

  • 去中心化的稳定币中最能落地的,目前来看也是整个市值最大的去中心化稳定币。
  • MKR没有进行ICO,因为Rune这个人不希望通过ICO导致大量的投机客和断线投资者进入DAO,影响整个社区的风气。当然,在没有进行ICO的情况下,如何获得充足的项目经费,同时维持一个比较大的MKR的市值,是我们需要思考的问题。
  • 对安全的重视和形式化验证的技术。makerDao是一个非常复杂的系统,有着复杂的机制设计,从第一行代码到系统上线,花了3年的时间。这个系统是为了承载巨大的资产而设计,所以安全性是极其极其重要的。第一个版本的SAI,邀请了参与theDAO事件的白帽子组织WHG,和非常牛逼的区块链安全团队trailofbits做代码审计。同时对整个系统的上限做了限制,防止出现安全事件时候波及的资金过多。事实证明,对安全足够的重视,导致maker这个项目基本没有负面的事件发生。在第二个版本的多抵押物dss实现中,更是引入了形式化验证的技术,由dapphub团队对整个模型做了严格的形式化推倒(https://github.com/dapphub/k-dss/),从逻辑层面证明了系统的安全性,是区块链领域第一个进行完整证明的系统。
  • 最后吐槽一下,makerdao这个项目,两个版本之间的设计存在巨大割裂,文档也是缺乏更新,经常让人不知道描述的是哪个版本,无疑增加了研究他的难度。不过话说回来,开源项目,文档往往是最后一个去完善的,在代码还没有完全finalize的时候去做文档的工作,在contributor有限的情况下,往往是不经济的。但也从侧面说明了这个项目本身内在的复杂度导致社区参与的难度,不然文档这种工作很多时候都是社区contributor刷commit的好方法。

QA

  • NTF or FT?
  • 有没有加仓机制?
  • MKR如何增值
  • 对比chenxin的想法,有何异同?
  • how markerdao deal with btc

References

科普:比特币历史

话说有信仰的基督信徒,在每餐之前,都会双手合十,说一句:感谢上帝赐予我食物。上次,我和某位币圈大佬请我们吃饭,高朋满座,玉盘珍馐,米其林三星。饭前呢,这位币圈大佬酒杯一举,说:干杯干杯,感谢中本聪赐予我食物。

上面只是一个故事哈,有一点演绎的成分,但确是我亲身经历。那中本聪是谁?为什么能养活这么多币圈的人?比特币又如何从艰难中前行,一步一步走到今天的呢?作为系列课程的第一节,我今天会从区块链的创始人讲起,扒一扒比特币的历史。

在比特币成功之前,已经存在了下图这些数不清的电子支付方式。比特币的成功,是踩着失败者的尸体过来的。这些失败的项目往往不被人们重视和提起。我相信,对于历史的认知会让我们对区块链技术有一个更加全面的理解。我们可以认识到,比特币从这些失败的项目身上,汲取了哪些营养。这个复杂的技术落地的过程中,经历了哪些问题。

208521539159489_ pic_hd

eCash

电子现金的初步尝试,来源于1982年的David Chaum[1]。从1982年 Chaum发表第一篇探索电子文献,到1998年到eCash技术的主体公司DigiCash破产,这个技术经历了大起大落,但无疑是人类社会探索电子现金支付系统的第一步。

Ecash对于电子现金最大的贡献,在于他对双花问题提出了第一个解决方案。所谓的双花问题,double-spend,顾名思义,即1块钱花两次,甚至更多次。在现实世界中,这当然很难发生,你拿着1块钱,造一枚假币来花,或者是给别人1块钱花出去之后,再偷回来,都是一种“双花”。而你这样做,多半是要蹲局子的。但是在电子世界中,money,是一串数据。对于ecash来讲,就是一个编号,一个面值,加一个签名组成的数据。这串数据如果可以代表money,那无疑是可以轻易的被复制的,所以单纯的靠这串数据,是无法避免双花问题的。

在Ecash中:

208531539159502_ pic_hd

可以看到,eCash是由一个中心化的服务器,来保证每一比电子现金只能被花费一次。从其核心原理上讲,eCash无疑是一个真正具有支付功能的电子现金系统。随着核心原理的提出,其商业化的尝试“DigiCash”也印证了这一点,同时也对其核心技术做了一些专利。但是,eCash面临以下的局限性:
中心化:依赖中心化的服务器,单节点故障将导致整个系统故障。
适用性不足:系统区分普通用户和商家,只支持普通用户和商家之间的交易,不支持普通用户之间的交易。
不可找零:最初版本的eCash系统不支持找零。后期引入了找零机制,增加了整个系统复杂度的同时,牺牲了用户的可匿名性。

虽然eCash面临上述局限性,但其对money电子化的尝试,和一些密码学技术包括blind signature, 零知识证明等运用。无疑启发了后人,同时也促使后人对其系统中的缺陷加以探索和改进。

Hashcash:puzzle

Hashcash,是中本聪在bitcoin白皮书中直接参考的8篇文献之一。其背景却完全跟电子支付没有关系。那为什么我们要在比特币的发展历史上提到他。因为这个系统,是第一个提出工作量证明也即PoW(后面章节会专门讲解PoW)的系统!

HashCash由Adam Back[2]在1997年第一次提出,其目的是为了防止垃圾邮件。比如Alice想发给Bob一封邮件,Bob为了防止Alice产生大量的垃圾邮件对其产生*扰。Bob要求Alice在邮件的末尾,附上一个“可计算性难题”和其答案。Alice为了得到这个难题的答案,往往需要用其电脑进行几秒的演算时间,从而限制住了Alice发送邮件的频率。

那么,究竟“可计算性难题”是个什么玩意。我们可能都玩儿过数独,一个数独,解出他的答案,非常非常难,但是我们拿着答案,验证他,那是非常容易知道这个答案的对错。具体对于这个系统而言,这个数独问题要满足以下的三点性质:
对于不同的,发件人,收件人,邮件内容和发送时间,这个题目和答案都是不一样的。这样做可以防止Alice提前准备好问题和答案。
验证Alice提供的题目和答案,可以在很短的时间内完成验证。
不同的题目之间,是完全独立的。也即,不管Alice之前解过多少题目,每一次解一道新题的难度,都不会因此而变化。

HashCash就是一个基于上述“可计算性难题”的反垃圾邮件系统。比特币的很大一部分设计灵感,直接源于hashCash。Adam曾经声称 “Bitcoin is Hashcash extended with inflation control.” 但是这么说肯定是有失偏颇的,比特币相比hashCash,是一个更加复杂且精巧的系统,与其这么说,还不如说 “Tesla只不过是加了四个轮子的电池”。哈哈哈哈!

“Haber and Stornetta starting in 1991”

1991年,Haber和Stornetta两位学者,在论文中第一次提出了Linked timestamping,即链式时间戳技术。这个技术通过将内容,时间戳,和签名作为一份文档,并对没份文档加上对上一份文档的一个链接的引用,实现了非常重要的特性:文档有序性,和文档历史的不可篡改性。通过对过去文档历史的引用,每当一份新的文档被附加在这个文档链条的末尾,都是对之前整个文档历史的肯定和确认。在链式结构生成后任何对文档历史的改变,都可以被第三方发现。

Linked timestamping是比特币另一个重要的灵感源泉。可以说,HashCash启发了比特币的共识体系设计,而Linked timestamping启发了比特币的去中心化账本设计,也即“区块链”这个词本身。Linked timestamping的文档概念可以直接对应于比特币中的区块概念,每个文档可以看成一个区块,而每一笔交易组成了文档内容。

208541539159510_ pic

B-money

Wei Dai, 1998, 第一次提出了b-money的想法。b-money系统,结合了上述HashCash和Linked timepstamping,用HashCash的可计算性难题来产出具有稀缺性的电子money,同时,用Linked timepstamping来实现账本的记账。b-money无疑是在比特币之前,设计上与比特币最接近的尝试。同时,b-money也是中本聪提出比特币白皮书中的第一篇参考文献,可见其思路对Bitcoin的启发和影响。但是,下面三个问题,是b-money与Bitcoin体系的不同之处:
B-money中,可计算性难题用来直接产出money。
B-money依赖中心化的timepstamping服务。
B-money对于多个节点发生不一致时,没有给出明确的算法来解决。
上述三个问题,让b-money仅仅停留在学术研究阶段。中心化和缺乏一致性算法,是其最大漏洞。

Bitcoin和中本聪

中本聪,从2007年五月份,开始写第一行Bitcoin代码。2008年十月份,他发表了大名鼎鼎的比特币白皮书。从此,中本聪老爷子赐饭的时代,就此拉开序幕。

此后的两年间,他成立了Botcoin talk,并各种论坛中传播Bitcoin,一面持续维护Bitcoin的项目代码。直到2010年12月份,中本聪这个名字,从论坛和开源项目中消失,除了世人对其真实身份的各种猜测,不带走一片云彩。

关于中本聪的真实身份,众说纷纭,没有定论。有人因姓名,猜测中本聪是日本人。有人因为其在论坛中的英式英语表达方式如”bloody hard“,猜测他是英国人。甚至有人猜测他是一个团体而非一个个人。同时中本聪在密码邮件组中是一个年轻后辈(可能30岁出头),地位十分显赫。(在这个邮件组中,有菲利普·希默曼(PGP技术的开发者)、约翰·吉尔摩(太阳微系统公司的明星员工)、斯蒂文·贝洛文(美国贝尔实验室研究员,哥伦比亚大学计算机科学教授)、布拉姆·科恩(BT下载的作者)、蒂姆希·C·梅(英特尔公司前首席科学家)、阿桑奇(维基解密创始人)这样的大牛)

比特币的理论总量为2100万,中本聪持有100万个,但从未使用过一个。有人说,正是中本聪的消失,才使比特币成为今天的比特币。中本聪是谁,其实并不重要。时代和技术的进步,往往是靠巨人的推动,产生了跨越式的发展。我们做不到像中本聪一样开创整个时代的发展,但是我们可以紧随他的脚步,学习区块链的**和技术,站在巨人的肩膀上,通过不断地学习完善我们对这个领域的认知,探索更多的机会,甚至推动技术的进步和变革。

208551539159524_ pic_hd

参考文献:

  1. Chaum, David (1983). "Blind signatures for untraceable payments". Advances in Cryptology Proceedings. 82 (3): 199–203.
  2. A. Back, "Hashcash - a denial of service counter-measure,"
    http://www.hashcash.org/papers/hashcash.pdf, 2002.
    3.Narayanan, Arvind, et al. Bitcoin and cryptocurrency technologies: a comprehensive introduction. Princeton University Press, 2016.

调研:基于区块链的争议解决系统设计

近年来,区块链技术风靡全球。得益于区块链技术的发展,我们正在全球构建起一个数字化的,去中心化的网络世界。在区块链技术繁荣发展的同时,也会有越来越多的去中心化组织(DAO),去中心化交易出现在区块链平台之上。正如现实生活中所发生的一样,有交易往来,就会有争议发生。比如去中心化的eBay上的顾客可能对买到的产品不满意,去中心化的airbnb上的顾客也可能会抱怨房子跟照片中的不一致,这个时候我们就得思考,如何在去中心化的网络中解决这些争议? 这已经成为当下区块链技术发展一个急需解决的问题。

区块链平台上有一大技术特点,就是智能合约。智能合约一旦部署,就能自动运行。但是,智能合约无法自动处理上面所说的争议,因为这些争议的解决涉及到主观意愿的判断,而这些是智能合约不擅长且无法解决的。鉴于此,我们需要思考一下,如何在去中心化网络,尤其是区块链平台上,设定一种机制,公平,高效地解决区块链平台上所出现的争议。目前已有针对这个问题所提出的解决方案,是一种叫做陪审员网络的制度。

在探索如何解决区块链平台上的争议这一领域,已经有了一些优秀开拓者。OpenBazaar是一个结合了ebay和BitTorrent特点的去中心化商品交易市场,使用Bitcoin,Zcash等数字货币进行交易。用户在该平台上交易,既不用被平台收取高额交易费,也不用担心受到审查。在该平台上,有一个类似于“调解员”的角色,用户可以在进行交易的时候,选取一个“调解员”参与交易。当有争议发生的时候,就由该“调解员”来判断争议谁对谁错,以此来决定资金该流向何处,同时“调解员”获得相应的报酬。“调解员”参与过的案例也会公布在平台上,并伴随有客户的评价。这样的一套评价系统,可以更好地帮助用户选择“调解员”。作为前期的开拓者,OpenBazaar可以说是很优秀的,但是它的局限性也比较明显:a. 只适用于电商交易这一特定领域的交易,通用性不高;b.“调解员”由用户选取,且仅有一位,安全性和公平性不高。

Kleros是以太坊上的一个应用,它作为一个去中心化的第三方团体,致力于解决智能合约上的任何争议。它有一个分层的法院系统,最底层的法院会解决具体领域的一些“案件”,如有的法院会专门处理有关汽车保险的案件。如果用户对于法院做出的结果不满意,则可以提起上诉,由更高一级的法院来审理案件,比如,由专门管理交通保险的法院来重新处理该案件。上一级的法院可以驳回下一级的法院做出的决定。逐级往上,直到最上面一层的法院,案件无法再提起上诉。每一级法院中的陪审员,都是通过抵押代币来成为陪审员的候选人,且是根据抵押的代币数量多少而随机选取,一旦有陪审员做出和绝大多数陪审员相反的决定,那么该陪审员不但不能获得奖励,还会受到惩罚,其抵押的代币会被部分收走。

Aragon也有类似的陪审员制度。相比于Kleros,Aragon做出了以下几点改进:a. 增加了一个reputation system(名声系统),陪审员的选择不单单根据抵押代币的多少,还根据过去的表现。b. 不仅仅是用户可以上诉,陪审员对于结果不满也可以上诉。

针对以上三个Projects的分析,我们可以总结出在去中心化网络解决争议的一般步骤:
(1)用户发起争议解决的请求,触发相应的陪审员网络制度
(2)选取相应的法院和陪审员
(3)陪审员投票
(4)陪审员获得结果,不服可上诉
(5)用户获得结果,不服可上诉
(6)争议解决,分配奖励
首先,让我们来根据以上步骤的核心要素来对OpenBazaar,Kleros,Aragon来做一个对比。

1681539158933_ pic_hd

从上面来看,我们可以总结出这三个项目的优缺点:
OpenBazaar的争议处理机制简单易行,同时还有评价系统,提供陪审员的过往案例,让用户可以更好地根据自己的需要而选择陪审员。但同时它的缺点也是很明显的,主要有以下几点:
其机制只适用于电商交易纠纷等少数案例,并没有普遍性。主要的原因是其只使用数字货币的多签交易来作为纠纷的解决机制,没有利用智能合约来进一步地拓宽适用范围。
单个案例的陪审员数量太少,只有一个,公平性和安全性不高
用户对结果不满,无法提起上诉

Kleros可以说在OpenBazaar的基础上进一步地完善了这个陪审员网络机制。Kleros是以太坊上的一个应用,它利用智能合约来完善陪审员网络机制,让其可以适用于区块链平台上的所有应用,同时也可以让更多的陪审员参与进来。Kleros还提出了分层级的陪审员网络,每一层级都有对应领域的法院处理相应的案件。用户可以决定选择哪个法院及多少名陪审员来审理这个案件。一般会是3个。当然了,越多的陪审员参与进来,需要付出的手续费就会越高。每一名陪审员都是根据抵押资金的多少随机选取出来的。在此基础上,用户如果对于法院审查的结果不满意,可以提起上诉,让更高一级的法院来审理案件。这种制度,可以更进一步地防止陪审员被贿赂而作恶的情况发生。Kleros已经做得挺好的了,但是还有以下几点需要改进:
陪审员是通过自愿抵押代币来成为某一个案例的陪审员候选人的,那么就可能出现有一些案例没有陪审员来审查(原因可能有手续费太低,或在线的陪审员人数不够)。同时,陪审员过往的案例并没有积累效应,以前做的好与不好并不影响其被选择成为某一案例的陪审员的概率。这有点不合理。
陪审员无法对不满意的结果进行上诉,只有争议双方可以上诉

Aragon的陪审员网络机制,很多一部分是借鉴Kleros的,所以说Kleros有的优点,Aragon也有。Aragon在Kleros的基础上,增加了一个reputation system,陪审员过往的表现会体现在其reputation上,其表现的好,reputation越高,反之则越低。而陪审员被选为某一个案例的陪审员的概率是跟其reputation有关的。其次,Aragon还增加了一个流程,即陪审员也可以对结果进行上诉。要求一个叫做review court的法院来审查处理该案件的陪审员是否有作恶。当然,Aragon也有可以改进的地方:
在Aragon中,无论陪审员做的好与不好,都是能够获得奖励的,同时其对于陪审员没有一个准入机制。这样会导致一旦某一陪审员做错了,就可以选择重新换一个账号继续作恶。
陪审员提起上诉并不需要额外付费,这样会导致该机制被滥用,影响整个陪审员网络的效率。
Aragon同样没解决陪审员不在线的情况。

综合上述三个Projects的分析,我们可以扬长避短,来设计一个更好的陪审员网络机制。我觉得我们可以借鉴以下几点:
利用智能合约,设计出一套适用性更广的陪审员网络机制。
引入层级结构的陪审员网络,用户和陪审员都可以对结果进行上诉,且提起上诉需要再额外付费
引入“评价系统”,让陪审员过往的表现可以积累。
对陪审员进行KYC或设计一套良好的奖惩,准入机制,让陪审员作恶成本极大。
针对陪审员不在线或人数不够的情况,我认为可以有以下方法:a. 针对长期在线的陪审员进行奖励,同时对于长期不在线的陪审员进行惩罚或取消其陪审员的身份;b. 社区和项目方共同拿出一部分代币,来雇佣一些全职的,经过专门培训的陪审员来长期在线应对各种案例。

Review Casper FFG

Core Spec

casper ffg basics

what Checkpoint means?

In bitcoin context

Nothing at stake

PeerCoin argue to this attack

inspiration

  • deposit 的 delay
    • by case:
    • by person:
  • crash of validtor
    • offline of juror

Sharding

References

Collateral Risk Control Design Doc

Related Solution

We went deep in tree projects previously, namely MakerDao, Dharma, dydx. They all have some design choice of Risk Management. The following Chapter will grasp some key desisn rationale of them.

Dharma

Dharma provides only seizable collateral if and only if the creditor fails to repay. 这种方式无疑只提供了最低程度的风险控制,对于真实的抵押借贷业务,没有基于抵押物价格波动的风控方案无疑是无法对接真实业务的。当然,社区现在也在探索风控方案,详见讨论 https://github.com/dharmaprotocol/DIPs/issues/2

dydx

dydx 的风控机制在于,将全部的风险给到creditor,creditor需要自己在链下实时监测币价,同时creditor可以在任何时间要求debtor加仓任何金额的抵押物,所以creditor需要一个reputation系统来保证其最大程度的自由带来的最大程度作恶的可能性。这种设计哲学无疑是粗放的且奇怪的,因为他极大程度的增加了creditor的操作成本,同时由于reputation系统的引入,并没有减小整个系统的复杂度。

makerdao

在上一篇MakerDao Deep Dive文章中,我们事无巨细的探讨了MakerDao的平仓系统,这套系统为我们提供了很多的参考,但同时也面临着很多的问题。在下面的讨论中,在预言机和平仓机制中,我们都阐释了其可取之处和局限性,并采用了十分不同于MakerDao方案的路径来考虑整个系统的设计。

Current Proposal

首先,我们进行拆分,按照不同的功能需求,这个问题可以拆分成以下几个问题:

  • 实时币价的喂价:币价用来计算当前的抵押率,是抵押率不良的一个判断条件。
  • debt servcing: 包括订单催收和加仓提醒(margin call),本文只探讨margin call,催收在另外的话题中另行讨论
  • 强平机制:在抵押率达到强平线时,需要有一套平仓机制及时的将抵押物变成对principle稳定的币种或者principle本身的币种。强平机制中,时效性和流动性,是最关键的两个指标。

三者个系统之间有一定的调用关系,1. 喂价系统要被加仓提醒,和强平机制调用。喂价系统的主要指标是安全性,及时性,准确性。2.加仓提醒由加仓警戒线触发,强平机制由强制平仓线来触发。考虑到债务人的利益,在强平之前,一定要有充足的时间去提醒债务人加仓。这个问题非常关键,下面我们将分别讨论这三个系统的设计。

币价feed

币价feed是一个经典的oracle问题,oracle在很多系统中例如makerdao和oraclize已经有了基本的解决方案。但在设计这个oracle系统的时候,我们首先考的的问题是。链下的价格,何时,何地上链。

选项一:链下价格实时上链

该种方案,根据makerdao的经验,在满足条件1. 交易对币价波动超过某个百分比。2.一段时间内未更新币价的两种下,触发币价的上链。参考上一篇文档,在币价上链后,结合多个交易所最后更新的币价,取中位数得到某个交易对当前的币价。下面的图阐释了这种完全的链上喂价的基本流程:
2751541056708_ pic_hd

这一种方案,在makerdao中已经安全的运行了半个多年头,可以说安全性,稳定性,去中心化程度都是非常不错的。但面临的最大问题是:随着交易对的增多,需要部署的合约和需要触发的交易手续费快速增加。假设每个交易对需要对接5个交易所,那么一共就是6个合约,平均每个合约至少6个小时调用一次,每次调用平均需要 0.001个以太坊。那么如果我们有n种支持的抵押物,就需要至少1个稳定币USDT和其的交易对的币价,至少需要6n个合约部署,和每天至少4*5*0.001*n = 0.02n ether的合约手续费。假设n=10, 那么每天花在预言机上面的手续费就是至少0.2个以太,一个月的成本至少在 15个以太。这样的成本是否对于一个节点来说是可控的,现在看起来是非常危险的。makerdao之所以可以这么玩儿,是因为其交易对偏少,同时对进入强平阶段的CDP惩罚较高,故可以用这种方式来实现。

这里面的一个关键问题在于,谁来call这个喂价合约,在makerdao中是DAO选取的server,和foundation运营的server在提供服务。因为maker只有4个交易对,估算其所有的server加起来一个月的开销可能就在十个以太以内。而我们如果这个费用让节点来出,在支持的抵押物越来越多的情况下,gas开销可能会非常的难以接受。 同时,我们研究了目前处在实验阶段的新的makerdao的喂价合约terra,其使用了签名消息来减少交易手续费的使用。调研后发现terra的代码还处在非常不成熟的阶段,基本上没有实际的实现。无法获得太多参考信息。

如果我们想规避手续费过高的问题,我们可以将喂价逻辑与平仓逻辑整合,也即在 liquadation 的 transaction call 中进行喂价。这是一个非常机智的idea,但是面临的最大question就是,caller即控制了喂价也控制了平仓的触发,如何保证合约caller喂价的准确性和客观性。可以强制要求caller在链上喂价时,也附带除自己之外的一定数量的喂价者签名的喂价值,换句话说,就是设计一个基于链下签名消息的喂价协议,不同的节点和喂价人(这些喂价者是注册在喂价合约白名单中的)之间可以交换其喂价值,在喂价时进行链上的真实喂价值计算(目前采取的算法是基于中位数)。这样做的好处是,在理论上保证了和链上喂价一样的安全性,甚至可以做到更加实时。(我们可以在平仓的时候直接链下请求喂价人暴露的接口,实时请求当前的价格)同时,这种方案节省下来的gas开销可以作为喂价者的激励返还给他一部分,激励大家去run一个喂价服务。同时,这个系统中可以设计一些惩罚去治理错误的喂价者。

预警系统(Margin Call)

margin call 的定义是在抵押率危险的时候,提前提醒用户加仓,以为强制平仓提供一定程度的缓冲时间。作为debt servicing 的一部分。这样的时间对于债务人是必要的,否则可能会发生一些纠纷。在Dharma的DIP-2 中提到了一种链上触发预警transaction,同时触发margin call事件以供dapp来监听的一种模式。我个人非常不欣赏这种方式的预警系统设计,原因有二:1. dapp一定不是永远在线,靠dapp去监听margin call 面临及时性的危险。 2. 在没有集成DID系统的情况下,dapp无法掌握用户的真实联系方式,这对提醒加仓是十分不利的。3. 同时,对于ETH为抵押物的债权人,由于不涉及催收,技术上可以不需要其kyc信息。

因此,基于上述的考虑,我认为Margin Call的系统可以完全做成一个链下的系统,因为margin call不会实际的引起抵押物的transfer,故其对交易对价格源的安全性有较高的容忍,节点可以自行决定是否对一笔订单发起margin call,如果错误的发起了margin call,只是某个节点的单独错误行为,受到的影响是可控的。同时因为签单的节点可以对用户进行链下的kyc,在做margin call的时候就有可能联系到真实用户,甚至可以线下去接触用户。这样无疑才是预警系统的意义之所在。

平仓系统

平仓系统可以拆分为3个步骤:

  1. 平仓合约调用者触发合约,通过计算当前的价格来判断抵押率是否达到平仓线, 也即 (collateralUnitPrice * collateralAmount) / (principalUnitPrice * principalAmount) < minimumCollateralRatio
  2. 抵押物的transfer:这个里面有两种设计。方案一为在步骤一时,由智能合约直接的将代币转到合约调用者的地址,由其进行平仓。第二种则将这两者解耦,在步骤一时触发时间,由其他全网所有白名单中的节点进行监听,监听到的节点可以申请抵押物的获得。在获得抵押物后,在链下对抵押物进行强制平仓处理。第一种方案较为简洁,第二种则更加灵活。有可能发生的情况是,某一节点认真的监听币价波动可以率先的发现某笔订单的抵押率问题,但自身无法获得该交易对的流动性。则可以由另外某个节点去做真正的平仓工作。在现阶段,我比较倾向于第一种方案,根据奥卡姆剃刀,我们一方面前期只会有一个节点,另外一方面后期也只希望暴露一份节点代码,如果某个节点的某个交易对流动性不足,可以让他自己想办法,去调用流动性充足的交易所。
  3. 平仓后的repay:平仓后repay也分两种,链上或者链下。链上发生更加安全透明不可抵赖,链下交易则快捷灵活。在这里,我比较倾向于节点做了平仓处理后上链操作,因为考虑到未来的可扩展性,可能需要暴露相应的repay钩子函数,同时也方面链上合约状态的更新。

Rationale(Meeting Topic)

在上次讨论中,我们的提出了在以下几点上的优化空间或者需要跟进的点。

喂价中位数的计算,是否有可能可以放在链下。

按照上一次的讨论,每一个server对接了一家交易所,上链时仅代表某一家交易所的价格。但其实对于中心化的服务器资源,我们完全可以在每一个server都对接所有交易所,对所有交易所的币价取中位数后,再将该中位数作为自己的喂价,在平仓线判断和平仓出发时,都使用该喂价。在有效市场的前提下,多个交易所之间的价差应该不会太大,但实际去将抵押物平仓的平仓者,可能在交易所中平仓的价格与其喂价不同。这个价差导致的亏损或者盈利都应该由节点自己来承担。在市场不会剧烈波动的大部分情况下,节点应该都是有盈利空间的,因为我们再计算中使用的是价格的中位数,而真正liquadate抵押物时,可以使用价格最优的某个交易所的价格。

尝试探索在链下的喂价源上建立惩罚机制,以确保价格的准确性。

喂价源是一个非常重要的系统,不准确的喂价将导致不必要的平仓发生,使债务人蒙受损失。在vitalik 2014年就畅想过一篇trust-free的喂价源系统,使用了博弈论中的schelling point作为理论支持。这个理论强调,要无偏向性的选择喂价人,同时尽量保证在喂价人中间有不同的价格趋向性以应对micro-cheating。在我们的系统中,节点方由于有平仓激励,所有有将价格喂低来提早触发平仓的倾向。同时,债务人由于将抵押物抵押到了智能合约中,故有倾向将价格喂得偏高。在健康的喂价源选择中,两方的人都需要,同时要无偏见的选取这两方参与系统。

直接在链上设计价格的审查机制可能比较困难同时不够经济(类似casper这种智能合约,设计一定是非常复杂的且没有通用性),我们可以通过仲裁网络,对由喂价不准确的平仓发生的订单寓意仲裁。我们只要在链下喂价的系统中提交上链时,附带每一个提交价格者的价格数据结构的commitment,在发生纠纷时可以reveal这一部分信息,以为仲裁者提供仲裁依据。

在平仓中,是否可以通过什么机制的设计来增加vena的使用需求。

  • 首先,喂价人需要抵押一部分vena做喂价的保证金,在喂价人喂价异常时,可以由仲裁网络来仲裁。扣除的的喂价人保证金用来分配给仲裁网络中的陪审员和受害的债务人或者债权人。
  • 每当平仓触发时,可以要求触发者,支付一小部分的vena代币(比例不会太高),在币价大幅波动导致节点保证金无法支付债务人损失的情况下,可以有社区来共同决定是否可以使用这部分vena补偿利益受损的个体。
  • 更多场景,可以探索,不过第二点个人认为已经有点过度设计了。在平仓这个环节中,由于涉及到的principle代币和colleteral代币都可能不是vena,所以设计模型时一定要小心不要给系统引入过多的复杂度和规则,否则极其容易画蛇添足。

Implantation 1.0

如果上面的章节关注的是各种设计的可能性,和不同可能性的优缺点。这一章更加关注在短时间比较容易落地的方案,所以简洁性,可拓展性是一个主要的trade-off因素。

对于三个不同的系统,我们分别采取以下路径

  • 喂价系统:官方节点喂价,对链下的喂价数据格式保留拓展的空间,已实现喂价网络的可扩展。
  • 预警系统:官方节点检测抵押率,结合用户的kyc信息,做中心化的Margin Call
  • 平仓系统:分为两个步骤,
    - 官方节点调用合约完成触发,直接获得抵押物。
    - 在交易所链下liquadate后,将获得的代币通过合约提交上链。

这三个系统的优先级是 喂价 = 平仓 > 预警, 实现难度是 平仓>喂价>预警。同时,这三个系统都是可以完全开源的系统。

下面做出调用时序图,以阐释不同系统之间的关联和平仓整体的业务流程:

2761541073732_ pic_hd

其中特别值得注意的有两点:

  1. 我们需要在现有的抵押借贷合约中,添加加仓抵押物的接口,同时,当前抵押物的信息需要做到链上可查询,以便为节点做出触发平仓的依据。在我们现有实现中,抵押物的调整信息,需要在TermsContract中暴露特定的查询接口。
  2. 在第五步return principle中,其return的币总量为扣除其本身佣金比例后剩余的principle,提交上链后,可能会有的剩余可以用来返还给debtor。

Known Limitations

在现有的设计中,我们的平仓机制仍然局限于ETH及ERC-20代币的抵押。对于BTC,目前我们走的方式还是多签地址用来抵押的模式,在产品设计中,对于BTC的抵押和ERC20的抵押,我们希望获得较为一致的产品体验和统一的dapp包装,对于BTC而言,可能面临一些技术挑战如下:

  • BTC的平仓

Ref

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.