From 93852aa438e26faf6b0d8f3c90c71f07049de0dd Mon Sep 17 00:00:00 2001 From: Ben Carman Date: Sat, 3 Oct 2020 15:24:02 -0500 Subject: [PATCH] Add DLC Oracle Server Endpoints (#2105) --- .../commons/serializers/Picklers.scala | 10 +- app/cli/src/main/resources/logback.xml | 1 + .../scala/org/bitcoins/cli/CliReaders.scala | 23 ++- .../scala/org/bitcoins/cli/ConsoleCli.scala | 135 +++++++++++++++++- app/oracle-server/oracle-server.sbt | 27 ++++ .../bitcoins/oracle/server/OracleRoutes.scala | 116 +++++++++++++++ .../bitcoins/server/ServerJsonModels.scala | 72 ++++++++++ build.sbt | 11 +- .../scala/org/bitcoins/db/AppConfig.scala | 6 +- .../org/bitcoins/dlc/oracle/DLCOracle.scala | 2 +- .../dlc/oracle/DLCOracleAppConfig.scala | 12 ++ project/Deps.scala | 9 ++ 12 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 app/cli/src/main/resources/logback.xml create mode 100644 app/oracle-server/oracle-server.sbt create mode 100644 app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleRoutes.scala diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala index f10cb629f0..9b418b4896 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala @@ -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) diff --git a/app/cli/src/main/resources/logback.xml b/app/cli/src/main/resources/logback.xml new file mode 100644 index 0000000000..adfa02c68c --- /dev/null +++ b/app/cli/src/main/resources/logback.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala index 54a6d2e3fb..8412156875 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala @@ -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 diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index e7ce0d3b02..7ce430e383 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -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 } diff --git a/app/oracle-server/oracle-server.sbt b/app/oracle-server/oracle-server.sbt new file mode 100644 index 0000000000..72cd75fcde --- /dev/null +++ b/app/oracle-server/oracle-server.sbt @@ -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) diff --git a/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleRoutes.scala b/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleRoutes.scala new file mode 100644 index 0000000000..e4eabd8d2a --- /dev/null +++ b/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleRoutes.scala @@ -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("[]") + } + } + } + } +} diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 83e2b70e82..7e6aef0c07 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -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 { diff --git a/build.sbt b/build.sbt index 286ea626ee..4b915906aa 100644 --- a/build.sbt +++ b/build.sbt @@ -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: _*) diff --git a/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala b/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala index 51bcfa8561..7fe343fcb5 100644 --- a/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala +++ b/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala @@ -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 diff --git a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala index 7fc1ab1189..4b0998a082 100644 --- a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala +++ b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala @@ -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( diff --git a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala index dc98bd89aa..e4a14b6408 100644 --- a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala +++ b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala @@ -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) diff --git a/project/Deps.scala b/project/Deps.scala index 59b0234350..e949edfdec 100644 --- a/project/Deps.scala +++ b/project/Deps.scala @@ -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,