Move DLC Oracle module to master (#2083)

This commit is contained in:
Ben Carman 2020-09-29 19:48:02 -05:00 committed by GitHub
parent 67b6c7b172
commit 2d34e2b690
16 changed files with 570 additions and 10 deletions

View File

@ -0,0 +1,25 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.crypto.StringFactory
sealed abstract class SigningVersion
object SigningVersion extends StringFactory[SigningVersion] {
/** Initial signing version that was created with krystal bull, not a part of any spec */
final case object Mock extends SigningVersion
val all: Vector[SigningVersion] = Vector(Mock)
override def fromStringOpt(str: String): Option[SigningVersion] = {
all.find(state => str.toLowerCase() == state.toString.toLowerCase)
}
override def fromString(string: String): SigningVersion = {
fromStringOpt(string) match {
case Some(state) => state
case None =>
sys.error(s"Could not find signing version for string=${string}")
}
}
}

View File

@ -10,6 +10,7 @@ flywayClean / aggregate := false
Test / flywayClean / aggregate := true
lazy val Benchmark = config("bench") extend Test
lazy val benchSettings: Seq[Def.SettingsDefinition] = {
//for scalameter
//https://scalameter.github.io/home/download/
@ -31,6 +32,7 @@ lazy val benchSettings: Seq[Def.SettingsDefinition] = {
import Projects._
lazy val crypto = project in file("crypto")
lazy val core = project in file("core") dependsOn crypto
lazy val bitcoindRpc = project
.in(file("bitcoind-rpc"))
.settings(CommonSettings.prodSettings: _*)
@ -57,6 +59,8 @@ lazy val `bitcoin-s` = project
dbCommonsTest,
feeProvider,
feeProviderTest,
dlcOracle,
dlcOracleTest,
bitcoindRpc,
bitcoindRpcTest,
bench,
@ -285,6 +289,7 @@ lazy val gui = project
)
lazy val chainDbSettings = dbFlywaySettings("chaindb")
lazy val chain = project
.in(file("chain"))
.settings(CommonSettings.prodSettings: _*)
@ -375,6 +380,7 @@ lazy val eclairRpcTest = project
.dependsOn(core % testAndCompile, testkit)
lazy val nodeDbSettings = dbFlywaySettings("nodedb")
lazy val node =
project
.in(file("node"))
@ -461,6 +467,7 @@ lazy val keyManagerTest = project
.dependsOn(keyManager, testkit)
lazy val walletDbSettings = dbFlywaySettings("walletdb")
lazy val wallet = project
.in(file("wallet"))
.settings(CommonSettings.prodSettings: _*)
@ -483,8 +490,27 @@ lazy val walletTest = project
.dependsOn(core % testAndCompile, testkit, wallet)
.enablePlugins(FlywayPlugin)
lazy val dlcOracle = project
.in(file("dlc-oracle"))
.settings(CommonSettings.prodSettings: _*)
.settings(
name := "bitcoin-s-dlc-oracle",
libraryDependencies ++= Deps.dlcOracle
)
.dependsOn(core, keyManager, dbCommons)
lazy val dlcOracleTest = project
.in(file("dlc-oracle-test"))
.settings(CommonSettings.testSettings: _*)
.settings(
name := "bitcoin-s-dlc-oracle-test",
libraryDependencies ++= Deps.dlcOracleTest
)
.dependsOn(core % testAndCompile, dlcOracle, testkit)
/** Given a database name, returns the appropriate
* Flyway settings we apply to a project (chain, node, wallet) */
* Flyway settings we apply to a project (chain, node, wallet)
*/
def dbFlywaySettings(dbName: String): List[Setting[_]] = {
lazy val DB_HOST = "localhost"
lazy val DB_NAME = s"${dbName}.sqlite"
@ -505,15 +531,16 @@ def dbFlywaySettings(dbName: String): List[Setting[_]] = {
db.createNewFile()
}
def makeNetworkSettings(directoryPath: String): List[Setting[_]] = List(
Test / flywayUrl := s"jdbc:sqlite:$directoryPath$DB_NAME",
Test / flywayLocations := List("nodedb/migration"),
Test / flywayUser := "nodedb",
Test / flywayPassword := "",
flywayUrl := s"jdbc:sqlite:$directoryPath$DB_NAME",
flywayUser := "nodedb",
flywayPassword := ""
)
def makeNetworkSettings(directoryPath: String): List[Setting[_]] =
List(
Test / flywayUrl := s"jdbc:sqlite:$directoryPath$DB_NAME",
Test / flywayLocations := List("nodedb/migration"),
Test / flywayUser := "nodedb",
Test / flywayPassword := "",
flywayUrl := s"jdbc:sqlite:$directoryPath$DB_NAME",
flywayUser := "nodedb",
flywayPassword := ""
)
lazy val mainnet = makeNetworkSettings(mainnetDir)

View File

@ -1,6 +1,7 @@
package org.bitcoins.db
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.ContractInfo
import org.bitcoins.commons.jsonmodels.dlc.SigningVersion
import org.bitcoins.core.config.{BitcoinNetwork, BitcoinNetworks}
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
@ -95,9 +96,19 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) {
implicit val sha256DigestBEMapper: BaseColumnType[Sha256DigestBE] =
MappedColumnType.base[Sha256DigestBE, String](_.hex, Sha256DigestBE.fromHex)
implicit val sha256DigestMapper: BaseColumnType[Sha256Digest] =
MappedColumnType.base[Sha256Digest, String](_.hex, Sha256Digest.fromHex)
implicit val ecPublicKeyMapper: BaseColumnType[ECPublicKey] =
MappedColumnType.base[ECPublicKey, String](_.hex, ECPublicKey.fromHex)
implicit val fieldElementMapper: BaseColumnType[FieldElement] =
MappedColumnType.base[FieldElement, String](_.hex, FieldElement.fromHex)
implicit val signingVersionMapper: BaseColumnType[SigningVersion] =
MappedColumnType.base[SigningVersion, String](_.toString,
SigningVersion.fromString)
implicit val schnorrPublicKeyMapper: BaseColumnType[SchnorrPublicKey] =
MappedColumnType
.base[SchnorrPublicKey, String](_.hex, SchnorrPublicKey.fromHex)

View File

@ -0,0 +1,16 @@
package org.bitcoins.dlc.oracle
import org.bitcoins.commons.jsonmodels.dlc.SigningVersion
import org.bitcoins.testkit.util.BitcoinSUnitTest
class SigningVersionTest extends BitcoinSUnitTest {
behavior of "SigningVersion"
it must "read from string" in {
SigningVersion.fromString("mock") must be(SigningVersion.Mock)
SigningVersion.fromString("MoCk") must be(SigningVersion.Mock)
SigningVersion.fromString("MOCK") must be(SigningVersion.Mock)
SigningVersion.fromStringOpt("not a real signing version") must be(None)
}
}

View File

@ -0,0 +1,32 @@
CREATE TABLE r_values
(
nonce TEXT NOT NULL,
label TEXT NOT NULL UNIQUE,
hd_purpose INTEGER NOT NULL,
coin INTEGER NOT NULL,
account_index INTEGER NOT NULL,
chain_type INTEGER NOT NULL,
key_index INTEGER NOT NULL,
commitment_signature TEXT NOT NULL,
PRIMARY KEY (nonce)
);
CREATE TABLE events
(
nonce TEXT NOT NULL,
label TEXT NOT NULL UNIQUE,
num_outcomes INTEGER NOT NULL,
signing_version TEXT NOT NULL,
attestation TEXT,
CONSTRAINT fk_label FOREIGN KEY (label) REFERENCES r_values (label) on update NO ACTION on delete NO ACTION,
PRIMARY KEY (nonce),
CONSTRAINT fk_nonce FOREIGN KEY (nonce) REFERENCES r_values (nonce) on update NO ACTION on delete NO ACTION
);
CREATE TABLE event_outcomes
(
nonce TEXT NOT NULL,
message TEXT NOT NULL,
hashed_message TEXT NOT NULL,
CONSTRAINT fk_nonce FOREIGN KEY (nonce) REFERENCES events (nonce) on update NO ACTION on delete NO ACTION
);

View File

@ -0,0 +1,32 @@
CREATE TABLE `r_values`
(
`nonce` VARCHAR(254) NOT NULL,
`label` VARCHAR(254) NOT NULL UNIQUE,
`hd_purpose` INTEGER NOT NULL,
`coin` INTEGER NOT NULL,
`account_index` INTEGER NOT NULL,
`chain_type` INTEGER NOT NULL,
`key_index` INTEGER NOT NULL,
`commitment_signature` VARCHAR(254) NOT NULL,
PRIMARY KEY (`nonce`)
);
CREATE TABLE `events`
(
`nonce` VARCHAR(254) NOT NULL,
`label` VARCHAR(254) NOT NULL UNIQUE,
`num_outcomes` INTEGER NOT NULL,
`signing_version` VARCHAR(254) NOT NULL,
`attestation` VARCHAR(254),
CONSTRAINT `fk_label` FOREIGN KEY (`label`) REFERENCES `r_values` (`label`) on update NO ACTION on delete NO ACTION,
PRIMARY KEY (`nonce`),
CONSTRAINT `fk_nonce` FOREIGN KEY (`nonce`) REFERENCES `r_values` (`nonce`) on update NO ACTION on delete NO ACTION
);
CREATE TABLE `event_outcomes`
(
`nonce` VARCHAR(254) NOT NULL,
`message` VARCHAR(254) NOT NULL,
`hashed_message` VARCHAR(254) NOT NULL,
CONSTRAINT `fk_nonce` FOREIGN KEY (`nonce`) REFERENCES `events` (`nonce`) on update NO ACTION on delete NO ACTION
);

View File

@ -0,0 +1,82 @@
package org.bitcoins.dlc.oracle
import java.nio.file.{Files, Path}
import com.typesafe.config.Config
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.db.{AppConfig, DbManagement, JdbcProfileComponent}
import org.bitcoins.dlc.oracle.storage._
import org.bitcoins.keymanager.WalletStorage
import scala.concurrent.{ExecutionContext, Future}
case class DLCOracleAppConfig(
private val directory: Path,
private val conf: Config*)(implicit val ec: ExecutionContext)
extends AppConfig
with DbManagement
with JdbcProfileComponent[DLCOracleAppConfig] {
import profile.api._
override def appConfig: DLCOracleAppConfig = this
override type ConfigType = DLCOracleAppConfig
override def newConfigOfType(
configOverrides: Seq[Config]): DLCOracleAppConfig =
DLCOracleAppConfig(directory, configOverrides: _*)
override def moduleName: String = "oracle"
override def baseDatadir: Path = directory
lazy val networkParameters: NetworkParameters = chain.network
/** The path to our encrypted mnemonic seed */
lazy val seedPath: Path = {
baseDatadir.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
}
override def start(): Future[Unit] = {
logger.debug(s"Initializing wallet setup")
if (Files.notExists(datadir)) {
Files.createDirectories(datadir)
}
val numMigrations = {
migrate()
}
logger.info(s"Applied $numMigrations to the wallet project")
FutureUtil.unit
}
/** Checks if our oracle as a mnemonic seed associated with it */
def seedExists(): Boolean = {
WalletStorage.seedExists(seedPath)
}
def exists(): Boolean = {
seedExists() &&
Files.exists(baseDatadir.resolve("oracle.sqlite"))
}
private val rValueTable: TableQuery[Table[_]] = {
RValueDAO()(ec, appConfig).table
}
private val eventTable: TableQuery[Table[_]] = {
EventDAO()(ec, appConfig).table
}
private val eventOutcomeTable: TableQuery[Table[_]] = {
EventOutcomeDAO()(ec, appConfig).table
}
override def allTables: List[TableQuery[Table[_]]] =
List(rValueTable, eventTable, eventOutcomeTable)
}

View File

@ -0,0 +1,73 @@
package org.bitcoins.dlc.oracle.storage
import org.bitcoins.commons.jsonmodels.dlc.SigningVersion
import org.bitcoins.crypto.{FieldElement, SchnorrNonce}
import org.bitcoins.db.{AppConfig, CRUD, DbCommonsColumnMappers, SlickUtil}
import slick.lifted.{ForeignKeyQuery, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class EventDAO()(implicit
val ec: ExecutionContext,
override val appConfig: AppConfig)
extends CRUD[EventDb, SchnorrNonce]
with SlickUtil[EventDb, SchnorrNonce] {
import profile.api._
private val mappers = new DbCommonsColumnMappers(profile)
import mappers._
override val table: TableQuery[EventTable] = TableQuery[EventTable]
private lazy val rValueTable: TableQuery[RValueDAO#RValueTable] =
RValueDAO().table
override def createAll(ts: Vector[EventDb]): Future[Vector[EventDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(
ids: Vector[SchnorrNonce]): Query[EventTable, EventDb, Seq] =
table.filter(_.nonce.inSet(ids))
override protected def findAll(
ts: Vector[EventDb]): Query[EventTable, EventDb, Seq] =
findByPrimaryKeys(ts.map(_.nonce))
def getPendingEvents: Future[Vector[EventDb]] = {
findAll().map(_.filter(_.attestationOpt.isEmpty))
}
class EventTable(tag: Tag) extends Table[EventDb](tag, schemaName, "events") {
def nonce: Rep[SchnorrNonce] = column("nonce", O.PrimaryKey)
def label: Rep[String] = column("label", O.Unique)
def numOutcomes: Rep[Long] = column("num_outcomes")
def signingVersion: Rep[SigningVersion] = column("signing_version")
def attestationOpt: Rep[Option[FieldElement]] = column("attestation")
def * : ProvenShape[EventDb] =
(nonce,
label,
numOutcomes,
signingVersion,
attestationOpt) <> (EventDb.tupled, EventDb.unapply)
def fk: ForeignKeyQuery[_, RValueDb] = {
foreignKey("fk_nonce",
sourceColumns = nonce,
targetTableQuery = rValueTable)(_.nonce)
}
def fkLabel: ForeignKeyQuery[_, RValueDb] = {
foreignKey("fk_label",
sourceColumns = label,
targetTableQuery = rValueTable)(_.label)
}
}
}

View File

@ -0,0 +1,15 @@
package org.bitcoins.dlc.oracle.storage
import org.bitcoins.commons.jsonmodels.dlc.SigningVersion
import org.bitcoins.crypto.{FieldElement, SchnorrDigitalSignature, SchnorrNonce}
case class EventDb(
nonce: SchnorrNonce,
label: String,
numOutcomes: Long,
signingVersion: SigningVersion,
attestationOpt: Option[FieldElement]) {
lazy val sigOpt: Option[SchnorrDigitalSignature] =
attestationOpt.map(SchnorrDigitalSignature(nonce, _))
}

View File

@ -0,0 +1,71 @@
package org.bitcoins.dlc.oracle.storage
import org.bitcoins.crypto.{SchnorrNonce, Sha256Digest}
import org.bitcoins.db.{AppConfig, CRUD, DbCommonsColumnMappers, SlickUtil}
import slick.lifted.{ForeignKeyQuery, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class EventOutcomeDAO()(implicit
val ec: ExecutionContext,
override val appConfig: AppConfig)
extends CRUD[EventOutcomeDb, (SchnorrNonce, String)]
with SlickUtil[EventOutcomeDb, (SchnorrNonce, String)] {
import profile.api._
private val mappers = new DbCommonsColumnMappers(profile)
import mappers._
override val table: TableQuery[EventOutcomeTable] =
TableQuery[EventOutcomeTable]
private lazy val eventTable: TableQuery[EventDAO#EventTable] =
EventDAO().table
override def createAll(
ts: Vector[EventOutcomeDb]): Future[Vector[EventOutcomeDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(ids: Vector[
(SchnorrNonce, String)]): Query[EventOutcomeTable, EventOutcomeDb, Seq] =
table
.filter(_.nonce.inSet(ids.map(_._1)))
.filter(_.message.inSet(ids.map(_._2)))
override protected def findAll(ts: Vector[EventOutcomeDb]): Query[
EventOutcomeTable,
EventOutcomeDb,
Seq] = {
val ids = ts.map(t => (t.nonce, t.message))
findByPrimaryKeys(ids)
}
def findByNonce(nonce: SchnorrNonce): Future[Vector[EventOutcomeDb]] = {
val query = table.filter(_.nonce === nonce)
safeDatabase.runVec(query.result.transactionally)
}
class EventOutcomeTable(tag: Tag)
extends Table[EventOutcomeDb](tag, schemaName, "event_outcomes") {
def nonce: Rep[SchnorrNonce] = column("nonce")
def message: Rep[String] = column("message")
def hashedMessage: Rep[Sha256Digest] = column("hashed_message")
def * : ProvenShape[EventOutcomeDb] =
(nonce,
message,
hashedMessage) <> (EventOutcomeDb.tupled, EventOutcomeDb.unapply)
def fk: ForeignKeyQuery[_, EventDb] = {
foreignKey("fk_nonce",
sourceColumns = nonce,
targetTableQuery = eventTable)(_.nonce)
}
}
}

View File

@ -0,0 +1,8 @@
package org.bitcoins.dlc.oracle.storage
import org.bitcoins.crypto.{SchnorrNonce, Sha256Digest}
case class EventOutcomeDb(
nonce: SchnorrNonce,
message: String,
hashedMessage: Sha256Digest)

View File

@ -0,0 +1,71 @@
package org.bitcoins.dlc.oracle.storage
import org.bitcoins.core.hd.{HDCoinType, HDPurpose}
import org.bitcoins.crypto.{SchnorrDigitalSignature, SchnorrNonce}
import org.bitcoins.db.{AppConfig, CRUD, DbCommonsColumnMappers, SlickUtil}
import slick.lifted.ProvenShape
import scala.concurrent.{ExecutionContext, Future}
case class RValueDAO()(implicit
val ec: ExecutionContext,
override val appConfig: AppConfig)
extends CRUD[RValueDb, SchnorrNonce]
with SlickUtil[RValueDb, SchnorrNonce] {
import profile.api._
private val mappers = new DbCommonsColumnMappers(profile)
import mappers._
override val table: TableQuery[RValueTable] = TableQuery[RValueTable]
override def createAll(ts: Vector[RValueDb]): Future[Vector[RValueDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(
ids: Vector[SchnorrNonce]): Query[RValueTable, RValueDb, Seq] =
table.filter(_.nonce.inSet(ids))
override protected def findAll(
ts: Vector[RValueDb]): Query[RValueTable, RValueDb, Seq] =
findByPrimaryKeys(ts.map(_.nonce))
def maxKeyIndex: Future[Option[Int]] = {
val query = table.map(_.keyIndex).max
safeDatabase.run(query.result.transactionally)
}
class RValueTable(tag: Tag)
extends Table[RValueDb](tag, schemaName, "r_values") {
def nonce: Rep[SchnorrNonce] = column("nonce", O.PrimaryKey)
def label: Rep[String] = column("label", O.Unique)
def purpose: Rep[HDPurpose] = column("hd_purpose")
def coinType: Rep[HDCoinType] = column("coin")
def accountIndex: Rep[Int] = column("account_index")
def chainType: Rep[Int] = column("chain_type")
def keyIndex: Rep[Int] = column("key_index")
def commitmentSignature: Rep[SchnorrDigitalSignature] =
column("commitment_signature")
def * : ProvenShape[RValueDb] =
(nonce,
label,
purpose,
coinType,
accountIndex,
chainType,
keyIndex,
commitmentSignature) <> (RValueDb.tupled, RValueDb.unapply)
}
}

View File

@ -0,0 +1,38 @@
package org.bitcoins.dlc.oracle.storage
import org.bitcoins.core.hd._
import org.bitcoins.crypto.{SchnorrDigitalSignature, SchnorrNonce}
case class RValueDb(
nonce: SchnorrNonce,
label: String,
purpose: HDPurpose,
accountCoin: HDCoinType,
accountIndex: Int,
chainType: Int,
keyIndex: Int,
commitmentSignature: SchnorrDigitalSignature) {
val path: BIP32Path = BIP32Path.fromString(
s"m/${purpose.constant}'/${accountCoin.toInt}'/$accountIndex'/$chainType'/$keyIndex'")
}
object RValueDbHelper {
def apply(
nonce: SchnorrNonce,
label: String,
account: HDAccount,
chainType: Int,
keyIndex: Int,
commitmentSignature: SchnorrDigitalSignature): RValueDb = {
RValueDb(nonce,
label,
account.purpose,
account.coin.coinType,
account.index,
chainType,
keyIndex,
commitmentSignature)
}
}

View File

@ -3,6 +3,8 @@ package org.bitcoins.keymanager
import java.nio.file.{Files, Path}
import java.util.NoSuchElementException
import org.bitcoins.core.crypto.BIP39Seed
import org.bitcoins.core.crypto.ExtKeyVersion.SegWitMainNetPriv
import org.bitcoins.core.util.TimeUtil
import org.bitcoins.crypto.AesPassword
import org.bitcoins.keymanager.ReadMnemonicError.{
@ -197,4 +199,29 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach {
}
}
it must "write and read an ExtPrivateKey from disk" in {
walletConf: WalletAppConfig =>
assert(!walletConf.seedExists())
val password = getBIP39PasswordOpt().getOrElse(BIP39Seed.EMPTY_PASSWORD)
val keyVersion = SegWitMainNetPriv
val writtenMnemonic = getAndWriteMnemonic(walletConf)
val expected = BIP39Seed
.fromMnemonic(mnemonic = writtenMnemonic.mnemonicCode,
password = password)
.toExtPrivateKey(keyVersion)
.toHardened
// should have been written by now
assert(walletConf.seedExists())
val seedPath = getSeedPath(walletConf)
val read =
WalletStorage.getPrivateKeyFromDisk(seedPath,
keyVersion,
passphrase,
Some(password))
assert(read == expected)
}
}

View File

@ -5,6 +5,7 @@ import java.time.Instant
import java.util.NoSuchElementException
import org.bitcoins.core.compat._
import org.bitcoins.core.crypto._
import org.bitcoins.crypto.{AesEncryptedData, AesIV, AesPassword, AesSalt}
import org.slf4j.LoggerFactory
import scodec.bits.ByteVector
@ -194,6 +195,26 @@ object WalletStorage {
case CompatRight(value) => Right(value)
}
}
def getPrivateKeyFromDisk(
seedPath: Path,
privKeyVersion: ExtKeyPrivVersion,
passphrase: AesPassword,
bip39PasswordOpt: Option[String]): ExtPrivateKeyHardened = {
val mnemonicCode = decryptMnemonicFromDisk(seedPath, passphrase) match {
case Left(error) => sys.error(error.toString)
case Right(mnemonic) => mnemonic.mnemonicCode
}
val seed = bip39PasswordOpt match {
case Some(pw) =>
BIP39Seed.fromMnemonic(mnemonic = mnemonicCode, password = pw)
case None =>
BIP39Seed.fromMnemonic(mnemonic = mnemonicCode,
password = BIP39Seed.EMPTY_PASSWORD)
}
seed.toExtPrivateKey(privKeyVersion).toHardened
}
}
sealed trait ReadMnemonicError { self: Error => }

View File

@ -398,4 +398,15 @@ object Deps {
Test.akkaTestkit
)
val dlcOracle =
List(
Compile.newMicroJson,
Compile.logback
)
val dlcOracleTest =
List(
Compile.newMicroJson,
Compile.logback
)
}