Add DLC Oracle Server Endpoints (#2105)

This commit is contained in:
Ben Carman 2020-10-03 15:24:02 -05:00 committed by GitHub
parent 323d324c9c
commit 93852aa438
12 changed files with 415 additions and 9 deletions

View File

@ -1,5 +1,7 @@
package org.bitcoins.commons.serializers
import java.time.Instant
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
@ -12,7 +14,7 @@ import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
import org.bitcoins.crypto._
import upickle.default._
object Picklers {
@ -29,6 +31,12 @@ object Picklers {
implicit val satoshisPickler: ReadWriter[Satoshis] =
readwriter[Long].bimap(_.toLong, Satoshis.apply)
implicit val schnorrNoncePickler: ReadWriter[SchnorrNonce] =
readwriter[String].bimap(_.hex, SchnorrNonce.fromHex)
implicit val instantPickler: ReadWriter[Instant] =
readwriter[Long].bimap(_.getEpochSecond, Instant.ofEpochSecond)
implicit val sha256DigestBEPickler: ReadWriter[Sha256DigestBE] =
readwriter[String].bimap(_.hex, Sha256DigestBE.fromHex)

View File

@ -0,0 +1 @@
<configuration />

View File

@ -1,6 +1,6 @@
package org.bitcoins.cli
import java.time.{ZoneId, ZonedDateTime}
import java.time.{Instant, ZoneId, ZonedDateTime}
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
@ -15,7 +15,11 @@ import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
import org.bitcoins.crypto.{
SchnorrDigitalSignature,
SchnorrNonce,
Sha256DigestBE
}
import scopt._
/** scopt readers for parsing CLI params and options */
@ -39,6 +43,21 @@ object CliReaders {
}
}
implicit val schnorrNonceReads: Read[SchnorrNonce] =
new Read[SchnorrNonce] {
override def arity: Int = 1
override def reads: String => SchnorrNonce = SchnorrNonce.fromHex
}
implicit val instantReads: Read[Instant] =
new Read[Instant] {
override def arity: Int = 1
override def reads: String => Instant =
str => Instant.ofEpochSecond(str.toLong)
}
implicit val bitcoinAddressReads: Read[BitcoinAddress] =
new Read[BitcoinAddress] {
val arity: Int = 1

View File

@ -1,5 +1,7 @@
package org.bitcoins.cli
import java.time.Instant
import org.bitcoins.cli.CliCommand._
import org.bitcoins.cli.CliReaders._
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
@ -18,7 +20,11 @@ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
import org.bitcoins.crypto.{
SchnorrDigitalSignature,
SchnorrNonce,
Sha256DigestBE
}
import scopt.OParser
import ujson.{Num, Str}
import upickle.{default => up}
@ -943,6 +949,100 @@ object ConsoleCli {
case other => other
}))
),
note(sys.props("line.separator") + "=== Oracle ==="),
cmd("getpublickey")
.action((_, conf) => conf.copy(command = GetPublicKey))
.text(s"Get oracle's public key"),
cmd("getstakingaddress")
.action((_, conf) => conf.copy(command = GetStakingAddress))
.text(s"Get oracle's staking address"),
cmd("listevents")
.action((_, conf) => conf.copy(command = ListEvents))
.text(s"Lists all event nonces"),
cmd("createevent")
.action((_, conf) =>
conf.copy(command = CreateEvent("", Instant.MIN, Seq.empty)))
.text("Registers an oracle event")
.children(
arg[String]("label")
.text("Label for this event")
.required()
.action((label, conf) =>
conf.copy(command = conf.command match {
case createEvent: CreateEvent =>
createEvent.copy(label = label)
case other => other
})),
arg[Instant]("maturationtime")
.text("The earliest expected time an outcome will be signed, given in epoch second")
.required()
.action((time, conf) =>
conf.copy(command = conf.command match {
case createEvent: CreateEvent =>
createEvent.copy(maturationTime = time)
case other => other
})),
arg[Seq[String]]("outcomes")
.text("Possible outcomes for this event")
.required()
.action((outcomes, conf) =>
conf.copy(command = conf.command match {
case createEvent: CreateEvent =>
createEvent.copy(outcomes = outcomes)
case other => other
}))
),
cmd("getevent")
.action((_, conf) => conf.copy(command = GetEvent(null)))
.text("Get an event's details")
.children(
arg[SchnorrNonce]("nonce")
.text("Nonce associated with the event")
.required()
.action((nonce, conf) =>
conf.copy(command = conf.command match {
case getEvent: GetEvent =>
getEvent.copy(nonce = nonce)
case other => other
}))
),
cmd("signevent")
.action((_, conf) => conf.copy(command = SignEvent(null, "")))
.text("Signs an event")
.children(
arg[SchnorrNonce]("nonce")
.text("Nonce associated with the event to sign")
.required()
.action((nonce, conf) =>
conf.copy(command = conf.command match {
case signEvent: SignEvent =>
signEvent.copy(nonce = nonce)
case other => other
})),
arg[String]("outcome")
.text("Outcome to sign for this event")
.required()
.action((outcome, conf) =>
conf.copy(command = conf.command match {
case signEvent: SignEvent =>
signEvent.copy(outcome = outcome)
case other => other
}))
),
cmd("getsignature")
.action((_, conf) => conf.copy(command = GetSignature(null)))
.text("Get the signature from a signed event")
.children(
arg[SchnorrNonce]("nonce")
.text("Nonce associated with the signed event")
.required()
.action((nonce, conf) =>
conf.copy(command = conf.command match {
case getSignature: GetSignature =>
getSignature.copy(nonce = nonce)
case other => other
}))
),
checkConfig {
case Config(NoCommand, _, _, _) =>
failure("You need to provide a command!")
@ -1146,6 +1246,24 @@ object ConsoleCli {
case DecodeRawTransaction(tx) =>
RequestParam("decoderawtransaction", Seq(up.writeJs(tx)))
// Oracle
case GetPublicKey =>
RequestParam("getpublickey")
case GetStakingAddress =>
RequestParam("getstakingaddress")
case ListEvents =>
RequestParam("listevents")
case GetEvent(nonce) =>
RequestParam("getevent", Seq(up.writeJs(nonce)))
case CreateEvent(label, time, outcomes) =>
RequestParam(
"createevent",
Seq(up.writeJs(label), up.writeJs(time), up.writeJs(outcomes)))
case SignEvent(nonce, outcome) =>
RequestParam("signevent", Seq(up.writeJs(nonce), up.writeJs(outcome)))
case GetSignature(nonce) =>
RequestParam("getsignature", Seq(up.writeJs(nonce)))
case NoCommand => ???
}
@ -1411,4 +1529,19 @@ object CliCommand {
case class FinalizePSBT(psbt: PSBT) extends CliCommand
case class ExtractFromPSBT(psbt: PSBT) extends CliCommand
case class ConvertToPSBT(transaction: Transaction) extends CliCommand
// Oracle
case object GetPublicKey extends CliCommand
case object GetStakingAddress extends CliCommand
case object ListEvents extends CliCommand
case class GetEvent(nonce: SchnorrNonce) extends CliCommand
case class CreateEvent(
label: String,
maturationTime: Instant,
outcomes: Seq[String])
extends CliCommand
case class SignEvent(nonce: SchnorrNonce, outcome: String) extends CliCommand
case class GetSignature(nonce: SchnorrNonce) extends CliCommand
}

View File

@ -0,0 +1,27 @@
name := "bitcoin-s-oracle-server"
// Ensure actor system is shut down
// when server is quit
Compile / fork := true
libraryDependencies ++= Deps.oracleServer
mainClass := Some("org.bitcoins.oracle.server.Main")
graalVMNativeImageOptions ++= Seq(
"-H:EnableURLProtocols=http",
"-H:+ReportExceptionStackTraces",
// builds a stand-alone image or reports a failure
"--no-fallback",
// without this, we get complaints about Function3
// I'm not sure why, though...
"--initialize-at-build-time=scala.Function3",
"--report-unsupported-elements-at-runtime",
"--verbose"
)
packageSummary := "A DLC Oracle"
packageDescription := "A basic DLC oracle that allows you to commit to events and sign them"
enablePlugins(JavaAppPackaging, GraalVMNativeImagePlugin)

View File

@ -0,0 +1,116 @@
package org.bitcoins.oracle.server
import akka.actor.ActorSystem
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import org.bitcoins.dlc.oracle._
import org.bitcoins.server._
import ujson._
import scala.util.{Failure, Success}
case class OracleRoutes(oracle: DLCOracle)(implicit system: ActorSystem)
extends ServerRoute {
import system.dispatcher
def handleCommand: PartialFunction[ServerCommand, StandardRoute] = {
case ServerCommand("getpublickey", _) =>
complete {
Server.httpSuccess(oracle.publicKey.hex)
}
case ServerCommand("getstakingaddress", _) =>
complete {
val network = oracle.conf.network
val address = oracle.stakingAddress(network)
Server.httpSuccess(address.toString)
}
case ServerCommand("listevents", _) =>
complete {
oracle.listEventDbs().map { eventDbs =>
val nonceStrs = eventDbs.map(_.nonce.hex)
val json = Arr.from(nonceStrs)
Server.httpSuccess(json.render(indent = 2))
}
}
case ServerCommand("createevent", arr) =>
CreateEvent.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(CreateEvent(label, maturationTime, outcomes)) =>
complete {
oracle.createNewEvent(label, maturationTime, outcomes).map {
eventDb =>
Server.httpSuccess(eventDb.nonce.hex)
}
}
}
case ServerCommand("getevent", arr) =>
GetEvent.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(GetEvent(label)) =>
complete {
oracle.getEvent(label).map {
case Some(event: Event) =>
val outcomesJson = event.outcomes.map(Str)
val (attestationJson, signatureJson) = event match {
case completedEvent: CompletedEvent =>
(Str(completedEvent.attestation.hex),
Str(completedEvent.signature.hex))
case _: PendingEvent =>
(Str(""), Str(""))
}
val json = Obj(
"nonce" -> Str(event.nonce.hex),
"eventName" -> Str(event.eventName),
"numOutcomes" -> Num(event.numOutcomes.toDouble),
"signingVersion" -> Str(event.signingVersion.toString),
"maturationTime" -> Str(event.maturationTime.toString),
"commitmentSignature" -> Str(event.commitmentSignature.hex),
"attestation" -> attestationJson,
"signature" -> signatureJson,
"outcomes" -> outcomesJson
)
Server.httpSuccess(json.render(indent = 2))
case None =>
Server.httpSuccess("[]")
}
}
}
case ServerCommand("signevent", arr) =>
SignEvent.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(SignEvent(nonce, outcome)) =>
complete {
oracle.signEvent(nonce, outcome).map { eventDb =>
Server.httpSuccess(eventDb.sigOpt.get.hex)
}
}
}
case ServerCommand("getsignature", arr) =>
GetEvent.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(GetEvent(nonce)) =>
complete {
oracle.getEvent(nonce).map {
case Some(completed: CompletedEvent) =>
Server.httpSuccess(completed.signature.hex)
case None | Some(_: PendingEvent) =>
Server.httpSuccess("[]")
}
}
}
}
}

View File

@ -1,5 +1,7 @@
package org.bitcoins.server
import java.time.Instant
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
@ -9,6 +11,7 @@ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.SchnorrNonce
import ujson._
import upickle.default._
@ -483,7 +486,76 @@ object OpReturnCommit extends ServerJsonModels {
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
}
// Oracle Models
case class CreateEvent(
label: String,
maturationTime: Instant,
outcomes: Vector[String])
object CreateEvent extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[CreateEvent] = {
jsArr.arr.toList match {
case labelJs :: maturationTimeJs :: outcomesJs :: Nil =>
Try {
val label = labelJs.str
val maturationTime: Instant =
Instant.ofEpochSecond(maturationTimeJs.num.toLong)
val outcomes = outcomesJs.arr.map(_.str).toVector
CreateEvent(label, maturationTime, outcomes)
}
case Nil =>
Failure(
new IllegalArgumentException("Missing label and outcome arguments"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 2"))
}
}
}
case class SignEvent(nonce: SchnorrNonce, outcome: String)
object SignEvent extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[SignEvent] = {
jsArr.arr.toList match {
case nonceJs :: outcomeJs :: Nil =>
Try {
val nonce = SchnorrNonce(nonceJs.str)
val outcome = outcomeJs.str
SignEvent(nonce, outcome)
}
case Nil =>
Failure(
new IllegalArgumentException("Missing nonce and outcome arguments"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 2"))
}
}
}
case class GetEvent(nonce: SchnorrNonce)
object GetEvent extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[GetEvent] = {
require(jsArr.arr.size == 1,
s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1")
Try {
val nonce = SchnorrNonce(jsArr.arr.head.str)
GetEvent(nonce)
}
}
}
trait ServerJsonModels {

View File

@ -79,7 +79,8 @@ lazy val `bitcoin-s` = project
appCommons,
appCommonsTest,
testkit,
zmq
zmq,
oracleServer
)
.settings(CommonSettings.settings: _*)
// crossScalaVersions must be set to Nil on the aggregating project
@ -240,6 +241,14 @@ lazy val appCommonsTest = project
.settings(CommonSettings.testSettings: _*)
.dependsOn(appCommons, testkit)
lazy val oracleServer = project
.in(file("app/oracle-server"))
.settings(CommonSettings.prodSettings: _*)
.dependsOn(
dlcOracle,
appServer
)
lazy val appServer = project
.in(file("app/server"))
.settings(CommonSettings.prodSettings: _*)

View File

@ -4,7 +4,7 @@ import java.nio.file.{Files, Path, Paths}
import com.typesafe.config._
import org.bitcoins.core.config._
import org.bitcoins.core.protocol.blockchain.ChainParams
import org.bitcoins.core.protocol.blockchain.BitcoinChainParams
import org.bitcoins.core.util.{BitcoinSLogger, StartStopAsync}
import slick.basic.DatabaseConfig
import slick.jdbc.JdbcProfile
@ -116,14 +116,14 @@ abstract class AppConfig extends StartStopAsync[Unit] with BitcoinSLogger {
protected[bitcoins] def moduleName: String
/** Chain parameters for the blockchain we're on */
lazy val chain: ChainParams = {
lazy val chain: BitcoinChainParams = {
val networkStr = config.getString("network")
BitcoinNetworks.fromString(networkStr).chainParams
}
/** The blockchain network we're on */
lazy val network: NetworkParameters = chain.network
lazy val network: BitcoinNetwork = chain.network
/**
* The underlying config that we derive the

View File

@ -180,7 +180,7 @@ case class DLCOracle(private val extPrivateKey: ExtPrivateKeyHardened)(implicit
case Some(value) =>
require(
value.attestationOpt.isEmpty,
s"Event already has been signed, attestation: ${value.attestationOpt.get}")
s"Event already has been signed, attestation: ${value.sigOpt.get.hex}")
Future.successful(value)
case None =>
Future.failed(

View File

@ -57,6 +57,18 @@ case class DLCOracleAppConfig(
FutureUtil.unit
}
def serverConf: Config = {
config.getConfig("server")
}
def rpcPortOpt: Option[Int] = {
if (serverConf.hasPath("rpcport")) {
Some(serverConf.getInt("rpcport"))
} else {
None
}
}
/** Checks if our oracle as a mnemonic seed associated with it */
def seedExists(): Boolean = {
WalletStorage.seedExists(seedPath)

View File

@ -315,6 +315,15 @@ object Deps {
Compile.akkaSlf4j
)
val oracleServer =
List(
Compile.newMicroPickle,
Compile.logback,
Compile.akkaActor,
Compile.akkaHttp,
Compile.akkaSlf4j
)
val eclairRpc = List(
Compile.akkaHttp,
Compile.akkaStream,