Corda Settler
TL, DR
Corda Settlerの概念
Corda Settlerの実装例
Corda Settlerの動作例
のまとめ
Corda Settlerの概略
Corda Settler is a CorDapp allowing settlement using off-ledger payment rails. It is easily interoperable with other CorDapps (including your own), supports cryptocurrency digital payments as well as traditional bank API’s, and returns a cryptographic proof of settlement.
Ledger外のpaymentを用いて決済を行うCordapp
Traditional Bank APIもCryptoによるpaymentもサポートしている
決済の暗号学的証明がreturnされる
他のCordaとの連携も容易
Corda Settlerの処理フロー(例)
https://gyazo.com/7c3a5df2cb285dc769319bfdcad68c4d
Here are the steps that occur in the reference Corda Settler CorDapp. The example R3 uses is XRP cryptocurrency tokens from the Ripple blockchain.
R3がRippleのXRPを使う場合を例にCorda Settlerの処理フローを追う
1. 通常のFlowを用いてObligation (債権証書)を作成しoutput stateと必要な署名を集める
2. FX-Oracleから交換レートを取得する
XRPは時間経過で価格変動するのでOracleが必要(変動しない場合はOracleは不要)
3. obligation(債権証書)をXRP等の間衣rんする通貨建ての物に取り替える
FXレートを取得した後、obligationをXRPのものに変換する
4. 決済条項(期間?)を追加する
obligation(債権証書)を更新して、settlement typeを"payment rail" に対応しているものに変える
5. off-chain railsでpaymentを行う(例のばあいはXRP)
MakeOffLegderPaymentFlowを用いる
6. Settle Oracleからconfirmationを入手する
Settlement Oracleを用いて、受け取り手のoff-ledger account(例の場合はXRPのアカウント)で受領したことの検証を行う
7. obligation(債権証書)の決済を完了するために署名を集める
paymentが検証されたら署名をあつめてNotaryに送る。このことで、Party AもParty Bも自身のvalut内で決済されていることを記録できる
Custom Payment Railの実装方法
1. Create a CordaService API to interface with the payment rail. @CordaService annotation on class
2. Sub-class MakeOffLedgerPayment for creating a payment using your payment rail.
3. Add a SettlementMethod type and Payment type for your payment rail.
4. Create an Oracle to verify payments.
動作確認&実装の確認
ローカルでRippleとCordaを動かす
夢の共演感
どうでもいいけどripple-javaはripple-unmaintainedってユーザのリポジトリになっていて吹く
mvn, Android CLIが必要です (Android StudioをインスコしてANDROID_HOMEを環境変数に設定してください
流石に...(build位できて欲しい..)
Default kotlin version set to 1.2.41
masashi.mitsuzawa@pc-008(19:37:29) ~/dev/go/1.12.0/src/github.com/corda/corda-settler
e: /Users/masashi.mitsuzawa/dev/go/1.12.0/src/github.com/corda/corda-settler/cordapp/src/main/kotlin/com/r3/corda/finance/obligation/workflows/flows/CreateObligation.kt: (44, 13): Unresolved reference: private
e: /Users/masashi.mitsuzawa/dev/go/1.12.0/src/github.com/corda/corda-settler/cordapp/src/main/kotlin/com/r3/corda/finance/obligation/workflows/flows/CreateObligation.kt: (44, 21): Declarations are not allowed in this position
e: /Users/masashi.mitsuzawa/dev/go/1.12.0/src/github.com/corda/corda-settler/cordapp/src/main/kotlin/com/r3/corda/finance/obligation/workflows/flows/CreateObligation.kt: (115, 21): Unresolved reference: timeLimit
e: /Users/masashi.mitsuzawa/dev/go/1.12.0/src/github.com/corda/corda-settler/cordapp/src/main/kotlin/com/r3/corda/finance/obligation/workflows/flows/CreateObligation.kt: (115, 82): Unresolved reference: timeLimit
> Task :cordapp:compileKotlin FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':cordapp:compileKotlin'.
> Compilation error. See log for more details
kotlin 1.2.51だと動かなかったのでsdkmanで適当にkotlin versionを変更する
info: kotlinc-jvm 1.2.41 (JRE 1.8.0_212-b04)
その上で、普通にbuild errorが出るので
これの変更を取り込む
IntelliJで開く -> gradle import -> autoimportをonにする
./gradlew clean deployNodes
./build/nodes/runnodes
下記エラー
Tue Nov 12 16:05:05 JST 2019>>> start CreateObligation amount: { quantity: 1000, token: { currencyCode: USD, type: fiat } }, role: OBLIGOR, counterparty: PartyB, dueBy: 1543922400, anonymous: false, externalId: "AAA", timeLimit: "2019/11/20"
No matching constructor found:
READMEのexampleだとDurationが引数にない -> error
timeLImit: "xxxx" を追加してもエラー
謎
色々がんばったら動いたので後でまとめます。
債権証書作成
start CreateObligation amount: { quantity: 1000, token: { currencyCode: USD, type: fiat } }, role: OBLIGOR, counterparty: PartyB, dueBy: 1543922400, anonymous: false
債権証書の通貨変更
start NovateObligation linearId: PASTE_UUID, novationCommand: { oldToken: { currencyCode: USD, type: fiat }, newToken: { currencyCode: XRP, type: digital }, oracle: Oracle, type: token }
XRPで支払う(XRPを持っていない時は、testネット用のXRPをfaucetから取得する
start UpdateSettlementMethod linearId: PASTE_UUID, settlementMethod: { accountToPay: PASTE_ACCOUNT, settlementOracle: Oracle, _type: com.r3.corda.finance.ripple.types.XrpSettlement }
OffLedger(XRP)で債権証書を決済する
start OffLedgerSettleObligation amount: { quantity: 20000000, token: { currencyCode: XRP, type: digital } }, linearId: PASTE_UUID
NovateObligationの実装 (USD建てで発行したObligationをXRP建てに書き換える、債権者、債務者双方の署名が必要
code: NovateObligation.kt
override fun call(): WireTransaction {
// Get the obligation from our vault.
progressTracker.currentStep = INITIALISING
val obligationStateAndRef = getLinearStateById<Obligation<TokenType>>(linearId, serviceHub)
?: throw IllegalArgumentException("LinearId not recognised.")
// Generate output and required signers list based based upon supplied command.
progressTracker.currentStep = HANDLING
val (novatedObligation, updatedNovationCommand) = handleNovationCommand(obligationStateAndRef)
// Create the new transaction.
progressTracker.currentStep = BUILDING
val notary = serviceHub.networkMapCache.notaryIdentities.first()
val signers = novatedObligation.participants.map { it.owningKey }
val utx = TransactionBuilder(notary = notary).apply {
addInputState(obligationStateAndRef)
addOutputState(novatedObligation, ObligationContract.CONTRACT_REF)
// Add the oracle key if required.
if (novationCommand is ObligationCommands.Novate.UpdateFaceAmountToken<*, *>) {
val oracleKey = novationCommand.oracle.owningKey
addCommand(updatedNovationCommand, signers + oracleKey)
} else {
addCommand(updatedNovationCommand, signers)
}
}
// Get the counterparty and our signing key.
val obligation = obligationStateAndRef.state.data.withWellKnownIdentities(resolver)
val (us, counterparty) = if (obligation.obligor == ourIdentity) {
Pair(novatedObligation.obligor, obligation.obligee)
} else {
Pair(novatedObligation.obligee, obligation.obligor)
}
// Sign it and get the oracle's signature if required.
progressTracker.currentStep = SIGNING
val ptx = if (novationCommand is ObligationCommands.Novate.UpdateFaceAmountToken<*, *>) {
val selfSignedTransaction = serviceHub.signInitialTransaction(utx, us.owningKey)
// COMMENT OralceにRateを問い合わせる場合はここで聞く val signature = subFlow(GetFxRateOracleSignature(selfSignedTransaction, novationCommand.oracle))
selfSignedTransaction + signature
} else {
serviceHub.signInitialTransaction(utx, us.owningKey)
}
// Get the counterparty's signature.
progressTracker.currentStep = COLLECTING
val counterpartyFlow = initiateFlow(counterparty as Party)
val stx = subFlow(CollectSignaturesFlow(
partiallySignedTx = ptx,
sessionsToCollectFrom = setOf(counterpartyFlow),
myOptionalKeys = listOf(us.owningKey),
progressTracker = COLLECTING.childProgressTracker()
))
progressTracker.currentStep = FINALISING
return subFlow(FinalityFlow(stx, setOf(counterpartyFlow), FINALISING.childProgressTracker())).tx
}
}
OracleがFxRateを取ってくる関数は下記
code: GetFxRate.kt
fun getRate(request: FxRateRequest): FxRateResponse {
val url = createRequestUrl(request.baseCurrency, request.counterCurrency, request.time.toEpochMilli())
val response = makeRequest(URI(url))
val rate = parseResponse(response, request)
return FxRateResponse(request.baseCurrency, request.counterCurrency, request.time, rate)
}
XRPOracle (Xrp上でSettlementが完了しているか教えてくれるマン)
code: XrpOracleService.kt
package com.r3.corda.finance.obligation.oracle.services
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import com.r3.corda.finance.obligation.contracts.states.Obligation
import com.r3.corda.finance.obligation.oracle.flows.VerifySettlement
// このへんがr3が提供しているxrpのクライアントライブラリ
import com.r3.corda.finance.ripple.services.XRPClientForVerification
import com.r3.corda.finance.ripple.types.TransactionNotFoundException
import com.r3.corda.finance.ripple.types.XrpPayment
import com.r3.corda.finance.ripple.utilities.hasSucceeded
import com.r3.corda.finance.ripple.utilities.toXRPAmount
import com.r3.corda.lib.tokens.contracts.types.TokenType
import com.typesafe.config.ConfigFactory
import net.corda.core.crypto.SecureHash
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.serialization.SingletonSerializeAsToken
import java.net.URI
@CordaService
class XrpOracleService(val services: AppServiceHub) : SingletonSerializeAsToken() {
private val configFileName = "xrp.conf"
private val nodes by lazy { ConfigFactory.parseResources(configFileName).getStringList("nodes").mapNotNull(::URI) }
private val clientsForVerification = nodes.map { nodeUri -> XRPClientForVerification(nodeUri) }
/** Check that the last ledger sequence has not passed. */
private fun isPastLastLedger(payment: XrpPayment<TokenType>): Boolean {
return clientsForVerification.all { client ->
client.ledgerIndex().ledgerCurrentIndex > payment.lastLedgerSequence
}
}
private fun checkServersAreUpToDate(): Boolean {
return clientsForVerification.all { client ->
val serverState = client.serverState().state.serverState
serverState in setOf("tracking", "full", "validating", "proposing")
}
}
private fun checkObligeeReceivedPayment(
xrpPayment: XrpPayment<TokenType>,
obligation: Obligation<TokenType>
): Boolean {
// Query all the ripple nodes.
val results = clientsForVerification.map { client ->
try {
client.transaction(xrpPayment.paymentReference)
} catch (e: TransactionNotFoundException) {
// The transaction is not recognised by the Oracle.
return false
} catch (e: MissingKotlinParameterException) {
// The transaction has no associated metadata yet. In which case, Jackson will not be able to deserialize
// the response to the TransactionInfoResponse object due to the missing "meta" property.
if (e.msg.contains("""JSON property meta""")) {
return false
} else {
throw e
}
}
}
// All nodes should report the same result.
val destinationCorrect = results.all { it.destination.toString() == obligation.settlementMethod?.accountToPay }
// ^^ 対応するxrpのアカウントに払い出されているか
// Using delivered amount instead of amount.
val amountCorrect = results.all { it.meta.deliveredAmount == xrpPayment.amount.toXRPAmount() }
val referenceCorrect = results.all { it.invoiceId == SecureHash.sha256(obligation.linearId.id.toString()).toString() }
val hasSucceeded = results.all { it.hasSucceeded() }
return destinationCorrect && amountCorrect && referenceCorrect && hasSucceeded
}
fun hasPaymentSettled(
xrpPayment: XrpPayment<TokenType>,
obligation: Obligation<TokenType>
): VerifySettlement.VerifyResult {
val upToDate = checkServersAreUpToDate()
if (!upToDate) {
return VerifySettlement.VerifyResult.PENDING
}
val isPastLastLedger = isPastLastLedger(xrpPayment)
val receivedPayment = checkObligeeReceivedPayment(xrpPayment, obligation)
return when {
// Payment received. Boom!
receivedPayment && !isPastLastLedger -> VerifySettlement.VerifyResult.SUCCESS
// Return success even if the deadline is passed.
receivedPayment && isPastLastLedger -> VerifySettlement.VerifyResult.SUCCESS
// Payment not received. Maybe the reference is wrong or it was sent to the wrong address.
// This situation will need to be sorted out manually for now...
!receivedPayment && isPastLastLedger -> VerifySettlement.VerifyResult.TIMEOUT
// If the deadline is not yet up then we are still pending.
!receivedPayment && !isPastLastLedger -> VerifySettlement.VerifyResult.PENDING
else -> throw IllegalStateException("Shouldn't happen!")
}
}
}
実装
Add a new module to this project with an outline similar to the ripple module.
The settlement rail you intend to use probably already has a Java client API, so all you need to do is create a wrapper around this for Corda. Look at what I did with the Ripple library as an example. If you are sending library types over the wire, you'll need to create proxy serialisers for those types. The interface to your payment rail should exist as a CordaService.
Sub-class MakeOffLedgerPayment for creating a payment using the payment rail of your choice. Note, that the payment must also be submitted to the payment rail. For Ripple and other cryptos this is easy as there are publicly available nodes. For legacy rails like RTGS and DFS you'll need access to an API for submitting transactions.
Implement an Oracle service which will update and sign a transaction containing a payment against an Oracle service, if and only if the payment credited the specified beneficiaries account on the settlement rail. For cryptos you can query some nodes of your choosing to check whether the payment settled correctly. For RTGS and DFS, again, you'll need access to an API.
Add a SettlementMethod type for your payment rail.
Add a Payment type for your payment rail.
References