Corda: Oracles
前提
Corda v4 (Opensource or Enterprise)
References
Oracleを実装する際の2つの基本的なやり方
それぞれ長所・短所があり、トレードオフである
Attachmentを使う
txにハッシュをattachmentとして付ける。txをdownloadするタイミングと同タイミングでノードからattachmentをダウンロードする。一度ダウンロードされたら保持され続け、コントラクトは実行される時にattachmentを参照できる
現状attachmentに電子署名の仕組みはない(将来的には導入される予定)
Commandを使う
factがコマンドの中に含まれている=factがtransactionの中に埋め込まれている
Oracleはtxのco-signerとして振る舞う
共通項
任意の(バイナリ)データをトラ ンザクションに提供できること
差異
「事実が独立したデータになって」おり再利用可能か=attachment
トランザクションに埋め込まれているため再利用ができない=command
どちらを使うか?
下記にあるように、人が読む前提 ・静的である場合はAttachment、動的に変化し、契約に影響する場合はCommandを用いる
Is your data continuously changing, like a stock price, the current time, etc? If yes, use a command.
連続的に変化する場合 => Command
Is your data commercially valuable, like a feed which you are not allowed to resell unless it’s incorporated into a business deal? If yes, use a command, so you can charge money for signing the same fact in each unique business context.
ビジネス的に重要である場合 => Command
Is your data very small, like a single number? If yes, use a command.
1つの数字の用に小さい場合 => Command
Is your data large, static and commercially worthless, for instance, a holiday calendar? If yes, use an attachment.
データが大きく静的で、ビジネスの観点からは無価値な場合 => Attatchment
Is your data intended for human consumption, like a PDF of legal prose, or an Excel spreadsheet? If yes, use an attachment..
PDFなど人間に読まれることを前提としている場合 => Attatchment
NodeInterestRateを例に
現在の利率は連続的に変化するのでCommandを用いる
割と自明な実装方法は下記
利率に依存するtx作る側が利率をオラクルに問い合わせる
Oracleは利率付きのCommandをinsertする
oracleはcommandを送り返す
が、これだとoracleが常に最初のsignerでないといけない=署名の順序に制約が生まれてイケてない
そのため、下記の方法で実装する
txが作られた側は利率を問い合わせる
txに利率と、利率が得られた時間を記載する
Oracleは他の署名者と並列で時間と値を見て署名する
ある時刻tの値がvでるという署名
code:NodeInterestRates.Oracle.kt
@CordaSerializable
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
コマンドのデータ構造
code:Command.kt
/** A Fix represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
Oracleのインターフェイス
code:Oracle.kt
class Oracle {
fun query(queries: List<FixOf>): List<Fix>
fun sign(ftx: FilteredTransaction): TransactionSignature
}
txをOracleから隠す
txに関与させると全部見えてしまう = Privacy Leakだよね
FilteredTransaction を使うことでこの問題を回避できる
Merkle Treeを用いて、ハッシュに署名させる
Pay-per-play oracles
Because the signature covers the transaction and not only the fact, this allows for a kind of weak pseudo-DRM over data feeds. Whilst a contract could in theory include a transaction parsing and signature checking library, writing a contract in this way would be conclusive evidence of intent to disobey the rules of the service (res ipsa loquitur). In an environment where parties are legally identifiable, usage of such a contract would by itself be sufficient to trigger some sort of punishment.
実装例
N番目の素数を要求
トランザクションに含まれている素数がたしかにN番目の素数だというオラクルの署名を要求
N番目の素数かどうかは決定的に判定できるので本質的な例ではない
code: run.sh
./gradlew deployNodes
./build/node/runNodes
Notary, NodeA, Oracleがノードとして立ち上がる
Oralceのnode.confは下記、特筆すべき設定をしているわけではない
code: node.conf
devMode=true
myLegalName="O=Oracle,L=New York,C=US"
p2pAddress="localhost:10008"
rpcSettings {
address="localhost:10009"
adminAddress="localhost:10049"
}
security {
authService {
dataSource {
type=INMEMORY
users=[
{
password=test
permissions=[
ALL
]
user=user1
}
]
}
}
}
動作確認としてcreatePrimeFlowを実行する(5番目の素数が含まれていることをOracleに署名してもらう)
code: flow
Wed Nov 06 14:28:08 JST 2019>>> flow start CreatePrime index: 5
✅ Starting
✅ Initialising flow.
✅ Querying oracle for the Nth prime.
✅ Building transaction.
✅ Verifying transaction.
✅ signing transaction.
✅ Requesting oracle signature.
✅ Finalising transaction.
Requesting signature by notary service
Requesting signature by Notary service
Validating response from Notary service
✅ Broadcasting transaction to participants
createPrimeFlowを見ていく
code: createPriveFlow.kt
class CreatePrime(val index: Int) : FlowLogic<SignedTransaction>() {
companion object {
// ProgressTrackerで設定したものがflowの実行中に表示される
object SET_UP : ProgressTracker.Step("Initialising flow.")
object QUERYING_THE_ORACLE : ProgressTracker.Step("Querying oracle for the Nth prime.")
Oracle役のノードに名前指定でアクセス(コネクションを張る?)
code: createFlow_call.kt
override fun call(): SignedTransaction {
progressTracker.currentStep = SET_UP
val notary = serviceHub.networkMapCache.notaryIdentities.first()
// In Corda v1.0, we identify oracles we want to use by name.
// ネットワーク内にいるOracleに問い合わせ
val oracleName = CordaX500Name("Oracle", "New York","US")
val oracle = serviceHub.networkMapCache.getNodeByLegalName(oracleName)?.legalIdentities?.first()
?: throw IllegalArgumentException("Requested oracle $oracleName not found on network.")
progressTracker.currentStep = QUERYING_THE_ORACLE
val nthPrimeRequestedFromOracle = subFlow(QueryPrime(oracle, index))
Oracleへの問い合わせ
code: createPrimeFlow.kt
progressTracker.currentStep = QUERYING_THE_ORACLE
val nthPrimeRequestedFromOracle = subFlow(QueryPrime(oracle, index))
progressTracker.currentStep = BUILDING_THE_TX
val primeState = PrimeState(index, nthPrimeRequestedFromOracle, ourIdentity)
val primeCmdData = PrimeContract.Create(index, nthPrimeRequestedFromOracle)
QueryPrimeの処理の実態は下記
code: queryPrime.kt
@InitiatingFlow
class QueryPrime(val oracle: Party, val n: Int) : FlowLogic<Int>() {
@Suspendable override fun call() = initiateFlow(oracle).sendAndReceive<Int>(n).unwrap { it }
}
SendAndReceiveを呼ぶとOracleクラス側のqueryが呼ばれ、n番目の素数がrequestされる
code: oracle.kt
// Returns the Nth prime for N > 0.
fun query(n: Int): Int {
return cache.get(n) ?: {
require(n > 0) { "n must be at least one." } // URL param is n not N.
val result = primes.take(n).last() // 予め定義された素数のリストの該当indexのものを返す
cache.put(n, result)
result
}()
}
その後、signerにoracleが指定され、transactionが組み立てられる。
code: CreatePrimeFlow.kt
progressTracker.currentStep = BUILDING_THE_TX
val primeState = PrimeState(index, nthPrimeRequestedFromOracle, ourIdentity)
val primeCmdData = PrimeContract.Create(index, nthPrimeRequestedFromOracle)
// By listing the oracle here, we make the oracle a required signer.
val primeCmdRequiredSigners = listOf(oracle.owningKey, ourIdentity.owningKey)
val builder = TransactionBuilder(notary)
.addOutputState(primeState, PRIME_PROGRAM_ID)
.addCommand(primeCmdData, primeCmdRequiredSigners)
PrimeStateの定義は下記
code: PrimeState.kt
data class PrimeState(val n: Int,
val nthPrime: Int,
val requester: AbstractParty) : ContractState {
override val participants: List<AbstractParty> get() = listOf(requester)
override fun toString() = "The ${n}th prime number is $nthPrime."
}
この状態(データ構造のバリデーション=Contractが下記で、commandで指定したn番目の素数にoutputのstateの中身 が一致していることを検証している
code: contract.kt
class PrimeContract : Contract {
// Commands signed by oracles must contain the facts the oracle is attesting to.
class Create(val n: Int, val nthPrime: Int) : CommandData
// Our contract does not check that the Nth prime is correct. Instead, it checks that the
// information in the command and state match.
override fun verify(tx: LedgerTransaction) = requireThat {
"There are no inputs" using (tx.inputs.isEmpty())
val output = tx.outputsOfType<PrimeState>().single()
val command = tx.commands.requireSingleCommand<Create>().value
"The prime in the output does not match the prime in the command." using
(command.n == output.n && command.nthPrime == output.nthPrime)
}
}
実際、Commandは下記で定義されている
code:command.kt
val primeCmdData = PrimeContract.Create(index, nthPrimeRequestedFromOracle)
# index # Oracleから受け取ったn番目の素数
OracleはCreatePrimeFlowの下記の部分でPrimeに署名する
code:createPrimeFlow.kt
val oracleSignature = subFlow(SignPrime(oracle, ftx))
code: hoge.kt
@InitiatingFlow
class SignPrime(val oracle: Party, val ftx: FilteredTransaction) : FlowLogic<TransactionSignature>() {
@Suspendable override fun call(): TransactionSignature {
val session = initiateFlow(oracle)
return session.sendAndReceive<TransactionSignature>(ftx).unwrap { it }
}
}
code: Oracle.kt
fun sign(ftx: FilteredTransaction): TransactionSignature {
// Check the partial Merkle tree is valid.
ftx.verify()
/** Returns true if the component is an Create command that:
* - States the correct prime
* - Has the oracle listed as a signer
*/
fun isCommandWithCorrectPrimeAndIAmSigner(elem: Any) = when {
elem is Command<*> && elem.value is PrimeContract.Create -> {
val cmdData = elem.value as PrimeContract.Create
myKey in elem.signers && query(cmdData.n) == cmdData.nthPrime
}
else -> false
}
// Is it a Merkle tree we are willing to sign over?
val isValidMerkleTree = ftx.checkWithFun(::isCommandWithCorrectPrimeAndIAmSigner)
if (isValidMerkleTree) {
return services.createSignature(ftx, myKey)
} else {
throw IllegalArgumentException("Oracle signature requested over invalid transaction.")
}
Oralceは自分の手元にあるデータを改めて引いてn番目の素数がPであるというtxを検証し、署名を行う。
ここでFilteredTransactionが用いられている。Filtered txはpartial Markle Treeを含んでおり、Oracleに見える必要のある部分以外は除外されている。Oracleは検証を行い、rootHashに署名する
Partial Merkle Tree (Merkle Branchのこと)
code: filtertetx.kt
val ftx = ptx.buildFilteredTransaction(Predicate {
when (it) {
is Command<*> -> oracle.owningKey in it.signers && it.value is PrimeContract.Create
else -> false
}
})
そもそもCordaのtxはMerkle treeを含んでいる
corda/corda/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net.corda.irs/api辺りにOracleの実装が転がっている
問い合わせが来た金利を返す関数
code: NodeInterestRates.kt
@Suspendable
fun query(queries: List<FixOf>): List<Fix> {
require(queries.isNotEmpty())
return mutex.locked {
val answers: List<Fix?> = queries.map { containerit } val firstNull = answers.indexOf(null)
if (firstNull != -1) {
} else {
answers.filterNotNull()
}
}
}
ファイル経由でOracleのknown valueをupdateする関数
code: uploadFixes.kt
fun uploadFixes(s: String) {
knownFixes = parseFile(s)
}
private fun addDefaultFixes() {
knownFixes = parseFile(IOUtils.toString(this::class.java.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name()))
}
}
時間経過と共に変化するデータに対するOracleの実装
Queryできるデータソースがあることを仮定すると実装は下記のようになる
code:Oracle.kt
fun sign(ftx: FilteredTransaction): TransactionSignature {
ftx.verify()
// Performing validation of obtained filtered components.
fun commandValidator(elem: Command<*>): Boolean {
require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) {
"Oracle received unknown command (not in signers or not Fix)."
}
/* Oracleが知っている値だったら */
val fix = elem.value as Fix
if (known == null || known != fix)
throw UnknownFix(fix.of)
return true
}
fun check(elem: Any): Boolean {
return when (elem) {
is Command<*> -> commandValidator(elem)
else -> throw IllegalArgumentException("Oracle received data of different type than expected.")
}
}
require(ftx.checkWithFun(::check))
ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey)
// It all checks out, so we can return a signature.
//
// Note that we will happily sign an invalid transaction, as we are only being presented with a filtered
// version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign
// an invalid transaction the signature is worthless.
return services.createSignature(ftx, services.myInfo.legalIdentities.first().owningKey)
}
下記の3ステップで行われることが見て取れる
1. tx全体をみることができなくても、送られてきたtxが有効で、verificationを通っていることを保証する
2. Commandが送りつけられてくる。Commandは予期した型(Fix)であれば署名するように要求する
3. それぞれのcommandに対して、commandが表象する値がOracle側の持っているデータソースと一致するかを確認し、署名をする
OracleをServiceとして作る場合は@CordaServiceというアノテーションを付ける
code: Oracle.kt
class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() {
private val mutex = ThreadBox(InnerState())
init {
// Set some default fixes to the Oracle, so we can smoothly run the IRS Demo without uploading fixes.
// This is required to avoid a situation where the runnodes version of the demo isn't in a good state
// upon startup.
addDefaultFixes()
}
AppServiceHubを唯一の引数として取る必要がある
code: FixFlowHandler.kt
@InitiatedBy(RatesFixFlow.FixSignFlow::class)
class FixSignHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val request = otherPartySession.receive<RatesFixFlow.SignRequest>().unwrap { it }
val oracle = serviceHub.cordaService(Oracle::class.java)
otherPartySession.send(oracle.sign(request.ftx))
}
}
@InitiatedBy(RatesFixFlow.FixQueryFlow::class)
class FixQueryHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
object RECEIVED : ProgressTracker.Step("Received fix request")
object SENDING : ProgressTracker.Step("Sending fix response")
override val progressTracker = ProgressTracker(RECEIVED, SENDING)
@Suspendable
override fun call() {
val request = otherPartySession.receive<RatesFixFlow.QueryRequest>().unwrap { it }
progressTracker.currentStep = RECEIVED
val oracle = serviceHub.cordaService(Oracle::class.java)
val answers = oracle.query(request.queries)
progressTracker.currentStep = SENDING
otherPartySession.send(answers)
}
}
code: FixFlow.kt
@InitiatingFlow
class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic<Fix>() {
@Suspendable
override fun call(): Fix {
val oracleSession = initiateFlow(oracle)
// TODO: add deadline to receive
val resp = oracleSession.sendAndReceive<List<Fix>>(QueryRequest(listOf(fixOf)))
return resp.unwrap {
val fix = it.first()
// Check the returned fix is for what we asked for.
check(fix.of == fixOf)
fix
}
}
}
@InitiatingFlow
class FixSignFlow(val tx: TransactionBuilder, val oracle: Party,
val partialMerkleTx: FilteredTransaction) : FlowLogic<TransactionSignature>() {
@Suspendable
override fun call(): TransactionSignature {
val oracleSession = initiateFlow(oracle)
val resp = oracleSession.sendAndReceive<TransactionSignature>(SignRequest(partialMerkleTx))
return resp.unwrap { sig ->
check(oracleSession.counterparty.owningKey.isFulfilledBy(listOf(sig.by)))
tx.toWireTransaction(serviceHub).checkSignature(sig)
sig
}
}
}