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
}))
),
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 ==="),
cmd("getpeers")
.action((_, conf) => conf.copy(command = GetPeers))
@ -501,6 +534,11 @@ object ConsoleCli {
up.writeJs(bitcoins),
up.writeJs(feeRateOpt),
up.writeJs(algo)))
case OpReturnCommit(message, hashMessage, satoshisPerVirtualByte) =>
RequestParam("opreturncommit",
Seq(up.writeJs(message),
up.writeJs(hashMessage),
up.writeJs(satoshisPerVirtualByte)))
// height
case GetBlockCount => RequestParam("getblockcount")
// filter count
@ -636,6 +674,11 @@ object CliCommand {
feeRateOpt: Option[SatoshisPerVirtualByte],
algo: CoinSelectionAlgo)
extends CliCommand
case class OpReturnCommit(
message: String,
hashMessage: Boolean,
feeRateOpt: Option[SatoshisPerVirtualByte])
extends CliCommand
case object GetNewAddress extends CliCommand
case object GetUtxos 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 {
val route =
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 {
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) =>
Rescan.fromJsArr(arr) match {
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.protocol.BitcoinAddress
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.FundWalletUtil.FundedWallet
import org.bitcoins.wallet.api.CoinSelector
import org.scalatest.{Assertion, FutureOutcome}
import scodec.bits.ByteVector
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 =>
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.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.script.ScriptPubKey
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.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.TxoState
@ -19,7 +23,7 @@ import org.bitcoins.core.wallet.utxo.TxoState.{
ConfirmedReceived,
PendingConfirmationsReceived
}
import org.bitcoins.crypto.ECPublicKey
import org.bitcoins.crypto.{CryptoUtil, ECPublicKey}
import org.bitcoins.keymanager.KeyManagerParams
import org.bitcoins.keymanager.bip39.BIP39KeyManager
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.internal._
import org.bitcoins.wallet.models.{SpendingInfoDb, _}
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
@ -320,6 +325,30 @@ abstract class Wallet
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(
outputs: Vector[TransactionOutput],
feeRate: FeeUnit,

View file

@ -553,6 +553,22 @@ trait WalletApi extends WalletLogger {
} 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]
/**