Add ability to make OP_RETURN commitments (#1417)

This commit is contained in:
Ben Carman 2020-05-18 14:27:05 -05:00 committed by GitHub
parent cec29e6894
commit 51fcb793be
7 changed files with 227 additions and 2 deletions

View file

@ -319,6 +319,39 @@ object ConsoleCli {
case other => other case other => other
})) }))
), ),
cmd("opreturncommit")
.action((_, conf) =>
conf.copy(command = OpReturnCommit("", hashMessage = false, None)))
.text("Send money to the given address")
.children(
arg[String]("message")
.text("message to put into OP_RETURN commitment")
.required()
.action((message, conf) =>
conf.copy(command = conf.command match {
case opReturnCommit: OpReturnCommit =>
opReturnCommit.copy(message = message)
case other => other
})),
opt[Unit]("hashMessage")
.text("should the message be hashed before commitment")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case opReturnCommit: OpReturnCommit =>
opReturnCommit.copy(hashMessage = true)
case other => other
})),
opt[SatoshisPerVirtualByte]("feerate")
.text("Fee rate in sats per virtual byte")
.optional()
.action((feeRate, conf) =>
conf.copy(command = conf.command match {
case opReturnCommit: OpReturnCommit =>
opReturnCommit.copy(feeRateOpt = Some(feeRate))
case other => other
}))
),
note(sys.props("line.separator") + "=== Network ==="), note(sys.props("line.separator") + "=== Network ==="),
cmd("getpeers") cmd("getpeers")
.action((_, conf) => conf.copy(command = GetPeers)) .action((_, conf) => conf.copy(command = GetPeers))
@ -501,6 +534,11 @@ object ConsoleCli {
up.writeJs(bitcoins), up.writeJs(bitcoins),
up.writeJs(feeRateOpt), up.writeJs(feeRateOpt),
up.writeJs(algo))) up.writeJs(algo)))
case OpReturnCommit(message, hashMessage, satoshisPerVirtualByte) =>
RequestParam("opreturncommit",
Seq(up.writeJs(message),
up.writeJs(hashMessage),
up.writeJs(satoshisPerVirtualByte)))
// height // height
case GetBlockCount => RequestParam("getblockcount") case GetBlockCount => RequestParam("getblockcount")
// filter count // filter count
@ -636,6 +674,11 @@ object CliCommand {
feeRateOpt: Option[SatoshisPerVirtualByte], feeRateOpt: Option[SatoshisPerVirtualByte],
algo: CoinSelectionAlgo) algo: CoinSelectionAlgo)
extends CliCommand extends CliCommand
case class OpReturnCommit(
message: String,
hashMessage: Boolean,
feeRateOpt: Option[SatoshisPerVirtualByte])
extends CliCommand
case object GetNewAddress extends CliCommand case object GetNewAddress extends CliCommand
case object GetUtxos extends CliCommand case object GetUtxos extends CliCommand
case object GetAddresses extends CliCommand case object GetAddresses extends CliCommand

View file

@ -703,6 +703,29 @@ class RoutesSpec
} }
} }
"make an OP_RETURN commitment" in {
val message = "Never gonna give you up, never gonna let you down"
(mockWalletApi
.makeOpReturnCommitment(_: String, _: Boolean, _: FeeUnit))
.expects(message, false, *)
.returning(Future.successful(EmptyTransaction))
(mockNode.broadcastTransaction _)
.expects(EmptyTransaction)
.returning(FutureUtil.unit)
.anyNumberOfTimes()
val route = walletRoutes.handleCommand(
ServerCommand("opreturncommit", Arr(message, Bool(false), Num(4))))
Post() ~> route ~> check {
contentType shouldEqual `application/json`
responseAs[String] shouldEqual """{"result":"0000000000000000000000000000000000000000000000000000000000000000","error":null}"""
}
}
"return the peer list" in { "return the peer list" in {
val route = val route =
nodeRoutes.handleCommand(ServerCommand("getpeers", Arr())) nodeRoutes.handleCommand(ServerCommand("getpeers", Arr()))

View file

@ -308,6 +308,38 @@ object SendWithAlgo extends ServerJsonModels {
} }
case class OpReturnCommit(
message: String,
hashMessage: Boolean,
feeRateOpt: Option[SatoshisPerVirtualByte])
object OpReturnCommit extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[OpReturnCommit] = {
jsArr.arr.toList match {
case messageJs :: hashMessageJs :: feeRateOptJs :: Nil =>
Try {
val message = messageJs.str
val hashMessage = hashMessageJs.bool
val feeRateOpt =
nullToOpt(feeRateOptJs).map(satsPerVBytes =>
SatoshisPerVirtualByte(Satoshis(satsPerVBytes.num.toLong)))
OpReturnCommit(message, hashMessage, feeRateOpt)
}
case Nil =>
Failure(
new IllegalArgumentException(
"Missing message, hashMessage, and fee rate arguments"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
}
trait ServerJsonModels { trait ServerJsonModels {
def jsToBitcoinAddress(js: Value): BitcoinAddress = { def jsToBitcoinAddress(js: Value): BitcoinAddress = {

View file

@ -149,6 +149,24 @@ case class WalletRoutes(wallet: WalletApi, node: Node)(
} }
} }
case ServerCommand("opreturncommit", arr) =>
OpReturnCommit.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(
OpReturnCommit(message, hashMessage, satoshisPerVirtualByteOpt)) =>
complete {
// TODO dynamic fees based off mempool and recent blocks
val feeRate =
satoshisPerVirtualByteOpt.getOrElse(SatoshisPerByte(100.satoshis))
wallet.makeOpReturnCommitment(message, hashMessage, feeRate).map {
tx =>
node.broadcastTransaction(tx)
Server.httpSuccess(tx.txIdBE)
}
}
}
case ServerCommand("rescan", arr) => case ServerCommand("rescan", arr) =>
Rescan.fromJsArr(arr) match { Rescan.fromJsArr(arr) match {
case Failure(exception) => case Failure(exception) =>

View file

@ -4,11 +4,15 @@ import org.bitcoins.commons.jsonmodels.wallet.CoinSelectionAlgo
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.SatoshisPerByte import org.bitcoins.core.script.constant.{BytesToPushOntoStack, ScriptConstant}
import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.wallet.fee.{SatoshisPerByte, SatoshisPerVirtualByte}
import org.bitcoins.crypto.CryptoUtil
import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
import org.bitcoins.wallet.api.CoinSelector import org.bitcoins.wallet.api.CoinSelector
import org.scalatest.{Assertion, FutureOutcome} import org.scalatest.{Assertion, FutureOutcome}
import scodec.bits.ByteVector
import scala.concurrent.Future import scala.concurrent.Future
@ -114,6 +118,66 @@ class WalletSendingTest extends BitcoinSWalletTest {
} }
} }
def testOpReturnCommitment(
wallet: Wallet,
hashMessage: Boolean): Future[Assertion] = {
val message = "ben was here"
for {
tx <- wallet.makeOpReturnCommitment(message,
hashMessage = hashMessage,
SatoshisPerVirtualByte.one)
outgoingTxDbOpt <- wallet.outgoingTxDAO.read(tx.txIdBE)
} yield {
val opReturnOutputOpt = tx.outputs.find(_.value == 0.satoshis)
assert(opReturnOutputOpt.isDefined, "Missing output with 0 value")
val opReturnOutput = opReturnOutputOpt.get
val messageBytes = if (hashMessage) {
CryptoUtil.sha256(ByteVector(message.getBytes)).bytes
} else {
ByteVector(message.getBytes)
}
val expectedAsm =
Seq(OP_RETURN,
BytesToPushOntoStack(messageBytes.size),
ScriptConstant(messageBytes))
assert(opReturnOutput.scriptPubKey.asm == expectedAsm)
assert(outgoingTxDbOpt.isDefined, "Missing outgoing tx in database")
val outgoingTxDb = outgoingTxDbOpt.get
assert(outgoingTxDb.sentAmount == 0.satoshis)
val changeOutput = tx.outputs.find(_.value > 0.satoshis).get
assert(
outgoingTxDb.actualFee + changeOutput.value == outgoingTxDb.inputAmount)
}
}
it should "correctly make a hashed OP_RETURN commitment" in { fundedWallet =>
testOpReturnCommitment(fundedWallet.wallet, hashMessage = true)
}
it should "correctly make an unhashed OP_RETURN commitment" in {
fundedWallet =>
testOpReturnCommitment(fundedWallet.wallet, hashMessage = false)
}
it should "fail to make an OP_RETURN commitment that is too long" in {
fundedWallet =>
val wallet = fundedWallet.wallet
recoverToSucceededIf[IllegalArgumentException] {
wallet.makeOpReturnCommitment(
"This message is much too long and is over 80 bytes, the limit for OP_RETURN. It should cause an error.",
hashMessage = false,
feeRate)
}
}
it should "fail to send to a different network address" in { fundedWallet => it should "fail to send to a different network address" in { fundedWallet =>
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet

View file

@ -11,7 +11,11 @@ import org.bitcoins.core.currency._
import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurposes} import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurposes}
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.constant.ScriptConstant
import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.util.BitcoinScriptUtil
import org.bitcoins.core.wallet.builder.BitcoinTxBuilder import org.bitcoins.core.wallet.builder.BitcoinTxBuilder
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
@ -19,7 +23,7 @@ import org.bitcoins.core.wallet.utxo.TxoState.{
ConfirmedReceived, ConfirmedReceived,
PendingConfirmationsReceived PendingConfirmationsReceived
} }
import org.bitcoins.crypto.ECPublicKey import org.bitcoins.crypto.{CryptoUtil, ECPublicKey}
import org.bitcoins.keymanager.KeyManagerParams import org.bitcoins.keymanager.KeyManagerParams
import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.keymanager.util.HDUtil import org.bitcoins.keymanager.util.HDUtil
@ -27,6 +31,7 @@ import org.bitcoins.wallet.api._
import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.internal._ import org.bitcoins.wallet.internal._
import org.bitcoins.wallet.models.{SpendingInfoDb, _} import org.bitcoins.wallet.models.{SpendingInfoDb, _}
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@ -320,6 +325,30 @@ abstract class Wallet
sendToOutputs(destinations, feeRate, fromAccount, reserveUtxos) sendToOutputs(destinations, feeRate, fromAccount, reserveUtxos)
} }
override def makeOpReturnCommitment(
message: String,
hashMessage: Boolean,
feeRate: FeeUnit,
fromAccount: AccountDb): Future[Transaction] = {
val messageToUse = if (hashMessage) {
CryptoUtil.sha256(ByteVector(message.getBytes)).bytes
} else {
if (message.length > 80) {
throw new IllegalArgumentException(
s"Message cannot be greater than 80 characters, it should be hashed, got $message")
} else ByteVector(message.getBytes)
}
val asm = Seq(OP_RETURN) ++ BitcoinScriptUtil.calculatePushOp(messageToUse) :+ ScriptConstant(
messageToUse)
val scriptPubKey = ScriptPubKey(asm)
val output = TransactionOutput(0.satoshis, scriptPubKey)
sendToOutputs(Vector(output), feeRate, fromAccount, reserveUtxos = false)
}
def sendToOutputs( def sendToOutputs(
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit, feeRate: FeeUnit,

View file

@ -553,6 +553,22 @@ trait WalletApi extends WalletLogger {
} yield tx } yield tx
} }
def makeOpReturnCommitment(
message: String,
hashMessage: Boolean,
feeRate: FeeUnit,
fromAccount: AccountDb): Future[Transaction]
def makeOpReturnCommitment(
message: String,
hashMessage: Boolean,
feeRate: FeeUnit): Future[Transaction] = {
for {
account <- getDefaultAccount()
tx <- makeOpReturnCommitment(message, hashMessage, feeRate, account)
} yield tx
}
def createNewAccount(keyManagerParams: KeyManagerParams): Future[Wallet] def createNewAccount(keyManagerParams: KeyManagerParams): Future[Wallet]
/** /**