From 18986b620d49d8b87f5b6dae444caaf6e27ad287 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Sat, 8 Dec 2018 11:03:24 -0500 Subject: [PATCH] Lightning Network (#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implementation of LnCurrencyUnit Fix unary and unneeded comments. Refactor and change arithmetic to use PicoBitcoins. Add property based testing for LnCurrencyUnits Refactor LnCurrencyUnits after code review Fix case and change LnPolicy to val Remove division and add Unit tests * Add additional unit tests and deserialization * WIP: Implement LnHumanReadablePart (#190) * Initial Implementation of LnHumanReadablePart * Add unit tests and improve deserialization from string * Refactor LnParams and LnHrp. Add requirements for instantiating LnHrp. * Clean up and re-organize things Re-working LnHumanReadablePart.fromString Fix unnecessary pattern match Removing test case * Created eclairRpc project (#193) Added getinfo functionality Added connect functionality Added most of the rpcs Added send and checkpayment functionality Added updaterelayfee functionality Fixed compile errors Ran scalafmt Added DaemonInstance and start/stop methods Added TestUtil Added open test Fixed typo in allUpdates Fixed ChannelResult Add eclair prefix to rpc stuff open channel unit test passing Adding instructions to grab default eclair in build Add zmq config to bitcoin.conf rename test log files, bump timeouts on connections Add eclair-rpc README, rework some RpcUtil/TestUtil stuff for async fixing bug in precious block, addressing code review Address more code review comments * Add NodeId, NodeUri, ChannelId (#196) refactor json serializing methods to SerializerUtil, add more types * Adding LnCurrencyUnit types to rpc api, fixing bug where eclair tests were not binding to a random port for zmq (#198) Remove start stuff * Adding more rpc tests, testing open, payment over channel, and closing of the channel (#199) Add checkpayment tests Address code review, create EclairTestUtil.createNodPair * Two way eclair transactions sanity test (#200) * Added a test for sending payments in both directions * Updated travis bitcoin core version * Initial LnInvoice Implementation (#194) Start typing some ln invoice stuff Add support for Fallback Address encoding Part 1: Breaking out Bech32 specific functions into a util class, don't embed in Bech32Address re-naming fromBase8ToBase5 -> from8BitTo5bit Part 1: Breaking out Bech32 specific functions into a util class, don't embed in Bech32Address rework ln invoices tags fix more method names in Bech32 Rename ScriptPubKeyTag -> NodeIdTag All invoice tags tests passing except weird serialization order one Address code review, add some more comments rename 'LnInvoiceTags' -> 'LnInvoiceTaggedFields' create a UInt5 type to represent all of the bech32 data structures Passing all serialization in the BOLT11 examples First cut at deserialization * Adding bitcoin-s types to the eclair-rpc, fixing bug with decoding numbers, refactoring more things (#204) * Switch bech32 p2wpkh hash from RipdeMd160 -> Sha256Hash160Digest (#206) * Add testkit project / dependency (#209) fix core-gen build.sbt name * add correct dependencies to testkit (#210) * Get dep name right (#211) * Add serialization symmetry property for LnInvoice, fixing various bugs in LnInvoice data structures, adding generators for various LnInvoice data structures (#217) * Reworking AuthCredentials and Instances so that we can read from config files (#218) add core files that were missing * Reworking a lot of testkit data structures to be more helpful for testing (#219) Add missing EclairApi file remove noisy log * Rebase onto master, fix testkit compile issues * Simplify LnCurrencyUnit, add MilliSatoshis, refactor EclairRpc to use… (#226) * Simplify LnCurrencyUnit, add MilliSatoshis, refactor EclairRpc to use MilliSatoshis * Add some helper functions around millisatoshis for comparing them to other things * more tests / helper methods, at generator for millisatoshis * Fix typo * Fix comparison operators for millisatoshis, add Writes for MilliSatos… (#227) * Fix comparison operators for millisatoshis, add Writes for MilliSatoshis in JsonWriters * re-add comparison operators to LnCurrencyUnit for convinience * Add millisatoshi reads (#228) * Updating version of eclair to https://github.com/ACINQ/eclair/releases/download/v0.2-beta8/eclair-node-0.2-beta8-52821b8.jar (#229) * Derive nodeId from ln invoice signature, move nodeid case class into … (#230) * Derive nodeId from ln invoice signature, move nodeid case class into the core project * Add missing assert * Fix null pointer exception that could occurred during requiring the invoice's signature to valid. This could occurr if a user tried to construct an invoice with an invalid signature (#233) * Turn down logging / remove logging (#235) * Cleaned up eclair conf (#237) * Cleaned up eclair conf * Added test for bad auth and Reads for LnInvoice * WIP: rebase onto master with new compiler opts fix more compiler warnings with testkit * fix new compiler warnings for scalac 2.12.x on ln (#253) * fix new compiler warnings for scalac 2.12.x on ln * fix missing p2wpkhoutput in rawoutput testkit/CreditingTxGen.scala * First cut at code review for the ln branch (#258) Fix bug in parsing the LnTagPrefix.CltvExpiry, add properties that check if the NodeIdTag is given explicitly to the invoice remove dumb invariants revert version * 2018 12 4 ln code review rd2 (#259) * Amend EclairRpc test case for confirming that channel is closed * Add final check to test case to make sure the bitcoind wallet received funds when closing channel * Address Torkel's code review * Addresses some review on #256 (#260) * Docstring cleanup, small nits * Refactors some redudant data, nested if => switch * Fixes SO error by reversing remowal of `new` * Fixes a couple of bugs * map.get instead of list.find * StringBuilder in HRP * Rework NetworkParam to LnParam * Cleanup * Renames file to match trait/object name * Docstring cleanup, pure formatting * Simplifies a few expressions, doesn't change semantics * Adds overloaded findRoute method instead of Either[NodeId, LnInvoice] * Eclair cleanup * Address concerns from Chris * Type annotation to match case * Address nadav's code review --- build.sbt | 24 +- .../core/gen/ln/LnCurrencyUnitGen.scala | 53 ++ .../bitcoins/core/gen/ln/LnInvoiceGen.scala | 167 +++++ .../org/bitcoins/core/gen/ln/LnRouteGen.scala | 49 ++ .../core/crypto/DERSignatureUtilTest.scala | 2 +- .../core/crypto/ECPublicKeyTest.scala | 12 +- .../TransactionSignatureCheckerTest.scala | 6 - .../TransactionSignatureCreatorTest.scala | 2 +- .../TransactionSignatureSerializerTest.scala | 3 +- .../org/bitcoins/core/number/Int32Spec.scala | 1 - .../org/bitcoins/core/number/UInt32Spec.scala | 2 +- .../org/bitcoins/core/number/UInt5Test.scala | 53 ++ .../org/bitcoins/core/number/UInt64Spec.scala | 1 - .../core/protocol/AddressFactoryTest.scala | 4 +- .../bitcoins/core/protocol/AddressTest.scala | 1 - .../bitcoins/core/protocol/Bech32Spec.scala | 8 +- .../bitcoins/core/protocol/Bech32Test.scala | 69 ++- .../core/protocol/BitcoinAddressTest.scala | 1 - .../core/protocol/blockchain/BlockTest.scala | 5 - .../blockchain/MerkleBlockTests.scala | 2 +- .../protocol/ln/LnHumanReadablePartTest.scala | 61 ++ .../core/protocol/ln/LnInvoiceUnitTest.scala | 380 ++++++++++++ .../ln/currency/LnCurrencyUnitTest.scala | 167 +++++ .../ln/currency/MilliSatoshisTest.scala | 58 ++ .../script/CLTVScriptPubKeySpec.scala | 8 +- .../script/CLTVScriptPubKeyTest.scala | 2 +- .../protocol/script/CSVScriptPubKeyTest.scala | 4 +- .../MultiSignatureScriptSignatureTest.scala | 2 +- .../script/P2PKHScriptSignatureTest.scala | 2 +- .../script/P2PKScriptPubKeyTest.scala | 2 +- .../script/P2PKScriptSignatureTest.scala | 2 +- .../script/P2SHScriptSignatureSpec.scala | 1 - .../script/ScriptPubKeyFactoryTest.scala | 2 +- .../protocol/script/ScriptPubKeyTest.scala | 6 +- .../script/ScriptSignatureFactoryTest.scala | 1 - .../protocol/script/ScriptSignatureTest.scala | 2 +- .../testprotocol/SignatureHashTestCase.scala | 1 - .../transaction/TransactionInputTest.scala | 2 +- .../transaction/TransactionOutputTest.scala | 1 - .../transaction/TransactionTest.scala | 9 +- .../script/ScriptConstantFactoryTest.scala | 2 +- .../script/ScriptOperationFactoryTest.scala | 2 +- .../script/ScriptProgramFactoryTest.scala | 1 - .../ArithmeticInterpreterTest.scala | 6 +- .../ArithmeticOperationsFactoryTest.scala | 2 +- .../bitwise/BitwiseInterpreterTest.scala | 3 +- .../BytesToPushOntoStackFactoryTest.scala | 2 +- .../constant/ScriptNumberFactoryTest.scala | 2 +- .../script/constant/ScriptNumberSpec.scala | 1 - .../constant/ScriptNumberUtilTest.scala | 2 +- .../ControlOperationsFactoryTest.scala | 2 +- .../ControlOperationsInterpreterTest.scala | 5 +- .../control/ControlOperationsTest.scala | 5 +- .../script/crypto/CryptoInterpreterTest.scala | 4 +- .../crypto/CryptoOperationsFactoryTest.scala | 2 +- .../core/script/flag/ScriptFlagsTest.scala | 2 +- .../interpreter/ScriptInterpreterTest.scala | 7 +- .../testprotocol/CoreTestCase.scala | 1 - .../ScriptSignatureCoreTestCase.scala | 2 +- .../LocktimeOperationFactoryTest.scala | 2 +- .../script/splice/SpliceInterpreterTest.scala | 3 +- .../splice/SpliceOperationFactoryTest.scala | 2 +- .../script/stack/StackInterpreterTest.scala | 5 +- .../stack/StackOperationFactoryTest.scala | 2 +- .../script/stack/StackOperationsTest.scala | 2 +- .../RawBitcoinSerializerHelperTest.scala | 2 +- .../serializers/RawSerializerHelperSpec.scala | 2 +- .../script/RawScriptSignatureParserTest.scala | 3 +- .../RawTransactionInputParserTest.scala | 2 +- .../bitcoins/core/util/CryptoUtilTest.scala | 31 +- .../bitcoins/core/util/NumberUtilSpec.scala | 24 +- .../core/util/testprotocol/ConfigParams.scala | 1 - .../core/crypto/DERSignatureUtil.scala | 5 - .../core/crypto/ECDigitalSignature.scala | 46 +- .../org/bitcoins/core/crypto/ECKey.scala | 23 +- .../org/bitcoins/core/number/NumberType.scala | 67 +- .../org/bitcoins/core/protocol/Address.scala | 201 ++---- .../protocol/ln/LnHumanReadablePart.scala | 134 ++++ .../bitcoins/core/protocol/ln/LnInvoice.scala | 329 ++++++++++ .../core/protocol/ln/LnInvoiceSignature.scala | 57 ++ .../bitcoins/core/protocol/ln/LnParams.scala | 87 +++ .../bitcoins/core/protocol/ln/LnPolicy.scala | 52 ++ .../core/protocol/ln/LnTagPrefix.scala | 86 +++ .../core/protocol/ln/LnTaggedFields.scala | 206 ++++++ .../bitcoins/core/protocol/ln/LnTags.scala | 295 +++++++++ .../core/protocol/ln/ShortChannelId.scala | 17 + .../core/protocol/ln/channel/ChannelId.scala | 42 ++ .../protocol/ln/channel/ChannelState.scala | 54 ++ .../protocol/ln/currency/LnCurrencyUnit.scala | 234 +++++++ .../protocol/ln/currency/LnMultiplier.scala | 26 + .../protocol/ln/currency/MilliSatoshis.scala | 126 ++++ .../core/protocol/ln/fee/FeeBaseMSat.scala | 33 + .../core/protocol/ln/node/NodeId.scala | 32 + .../core/protocol/ln/routing/LnRoute.scala | 81 +++ .../core/protocol/ln/util/LnUtil.scala | 79 +++ .../core/protocol/script/ScriptPubKey.scala | 20 +- .../script/crypto/CryptoInterpreter.scala | 2 +- .../scala/org/bitcoins/core/util/Bech32.scala | 239 +++++++ .../org/bitcoins/core/util/BitcoinSUtil.scala | 7 +- .../org/bitcoins/core/util/CryptoUtil.scala | 39 ++ .../org/bitcoins/core/util/NumberUtil.scala | 48 +- eclair-rpc/README.md | 27 + .../bitcoins/eclair/rpc/api/EclairApi.scala | 73 +++ .../eclair/rpc/client/EclairRpcClient.scala | 535 ++++++++++++++++ .../eclair/rpc/config/ConfigUtil.scala | 14 + .../rpc/config/EclairAuthCredentials.scala | 103 +++ .../eclair/rpc/config/EclairInstance.scala | 85 +++ .../eclair/rpc/json/EclairModels.scala | 197 ++++++ .../eclair/rpc/json/JsonReaders.scala | 132 ++++ .../bitcoins/eclair/rpc/network/NodeUri.scala | 46 ++ .../eclair/rpc/network/PeerState.scala | 15 + .../bitcoins/eclair/network/NodeUriTest.scala | 47 ++ .../eclair/rpc/EclairRpcClientTest.scala | 362 +++++++++++ project/Deps.scala | 20 +- .../main/scala/org/bitcoins/rpc/RpcUtil.scala | 83 ++- .../rpc/config/BitcoindAuthCredentials.scala | 10 +- .../rpc/config/BitcoindInstance.scala | 16 +- .../rpc/serializers/JsonWriters.scala | 5 + .../rpc/serializers/SerializerUtil.scala | 68 ++ .../scala/org/bitcoins/rpc/RpcUtilTest.scala | 40 +- .../bitcoins/core/gen/AddressGenerator.scala | 34 + .../gen/BlockchainElementsGenerator.scala | 97 +++ .../core/gen/BloomFilterGenerators.scala | 38 ++ .../core/gen/ChainParamsGenerator.scala | 16 + .../bitcoins/core/gen/CreditingTxGen.scala | 182 ++++++ .../bitcoins/core/gen/CryptoGenerators.scala | 104 ++++ .../core/gen/CurrencyUnitGenerator.scala | 59 ++ .../bitcoins/core/gen/MerkleGenerators.scala | 67 ++ .../bitcoins/core/gen/NumberGenerator.scala | 93 +++ .../bitcoins/core/gen/ScriptGenerators.scala | 544 ++++++++++++++++ .../bitcoins/core/gen/StringGenerators.scala | 58 ++ .../core/gen/TransactionGenerators.scala | 584 ++++++++++++++++++ .../bitcoins/core/gen/WitnessGenerators.scala | 252 ++++++++ .../eclair/rpc/EclairRpcTestUtil.scala | 343 ++++++++++ .../bitcoins/rpc/BitcoindRpcTestUtil.scala | 280 +++++++++ 135 files changed, 8230 insertions(+), 378 deletions(-) create mode 100644 core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnCurrencyUnitGen.scala create mode 100644 core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnInvoiceGen.scala create mode 100644 core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnRouteGen.scala create mode 100644 core-test/src/test/scala/org/bitcoins/core/number/UInt5Test.scala create mode 100644 core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePartTest.scala create mode 100644 core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala create mode 100644 core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnitTest.scala create mode 100644 core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshisTest.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoiceSignature.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnPolicy.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/ShortChannelId.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelId.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelState.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnit.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnMultiplier.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshis.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/fee/FeeBaseMSat.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/node/NodeId.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/routing/LnRoute.scala create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/ln/util/LnUtil.scala create mode 100644 core/src/main/scala/org/bitcoins/core/util/Bech32.scala create mode 100644 eclair-rpc/README.md create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/ConfigUtil.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairAuthCredentials.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairInstance.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/EclairModels.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/JsonReaders.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/NodeUri.scala create mode 100644 eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/PeerState.scala create mode 100644 eclair-rpc/src/test/scala/org/bitcoins/eclair/network/NodeUriTest.scala create mode 100644 eclair-rpc/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala create mode 100644 rpc/src/main/scala/org/bitcoins/rpc/serializers/SerializerUtil.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/AddressGenerator.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/BlockchainElementsGenerator.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/BloomFilterGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/ChainParamsGenerator.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/CreditingTxGen.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/CryptoGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/CurrencyUnitGenerator.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/MerkleGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/NumberGenerator.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/ScriptGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/StringGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/TransactionGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/core/gen/WitnessGenerators.scala create mode 100644 testkit/src/main/scala/org/bitcoins/eclair/rpc/EclairRpcTestUtil.scala create mode 100644 testkit/src/main/scala/org/bitcoins/rpc/BitcoindRpcTestUtil.scala diff --git a/build.sbt b/build.sbt index 865802001a..34aafba085 100644 --- a/build.sbt +++ b/build.sbt @@ -31,6 +31,7 @@ lazy val commonSettings = List( scalacOptions in Test := testCompilerOpts, assemblyOption in assembly := (assemblyOption in assembly).value.copy(includeScala = false) ) + lazy val root = project .in(file(".")) .aggregate( @@ -40,7 +41,9 @@ lazy val root = project coreTest, zmq, rpc, - bench + bench, + eclairRpc, + testkit ) .settings(commonSettings: _*) @@ -100,4 +103,23 @@ lazy val bench = project .settings(libraryDependencies ++= Deps.bench) .dependsOn(core) +lazy val eclairRpc = project + .in(file("eclair-rpc")) + .enablePlugins() + .settings(commonSettings: _*) + .dependsOn( + core, + rpc + ) + +lazy val testkit = project + .in(file("testkit")) + .enablePlugins() + .settings(commonSettings: _*) + .dependsOn( + core, + rpc, + eclairRpc + ) + publishArtifact in root := false diff --git a/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnCurrencyUnitGen.scala b/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnCurrencyUnitGen.scala new file mode 100644 index 0000000000..deca0cc4de --- /dev/null +++ b/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnCurrencyUnitGen.scala @@ -0,0 +1,53 @@ +package org.bitcoins.core.gen.ln + +import org.bitcoins.core.gen.NumberGenerator +import org.bitcoins.core.protocol.ln._ +import org.bitcoins.core.protocol.ln.currency._ +import org.scalacheck.Gen + +trait LnCurrencyUnitGen { + + def milliBitcoin: Gen[MilliBitcoins] = for { + amount <- Gen.choose(MilliBitcoins.min.toLong, MilliBitcoins.max.toLong) + } yield MilliBitcoins(amount) + + def microBitcoin: Gen[MicroBitcoins] = for { + amount <- Gen.choose(MicroBitcoins.min.toLong, MicroBitcoins.max.toLong) + } yield MicroBitcoins(amount) + + def nanoBitcoin: Gen[NanoBitcoins] = for { + amount <- Gen.choose(NanoBitcoins.min.toLong, NanoBitcoins.max.toLong) + } yield NanoBitcoins(amount) + + def picoBitcoin: Gen[PicoBitcoins] = for { + amount <- Gen.choose(PicoBitcoins.min.toLong, PicoBitcoins.max.toLong) + } yield PicoBitcoins(amount) + + def positivePicoBitcoin: Gen[PicoBitcoins] = { + Gen.choose(0, PicoBitcoins.max.toLong).map(PicoBitcoins(_)) + } + + def lnCurrencyUnit: Gen[LnCurrencyUnit] = Gen.oneOf(milliBitcoin, microBitcoin, nanoBitcoin, picoBitcoin) + + def lnCurrencyUnitOpt: Gen[Option[LnCurrencyUnit]] = { + Gen.option(lnCurrencyUnit) + } + + def positiveLnCurrencyUnit: Gen[LnCurrencyUnit] = { + lnCurrencyUnit.suchThat(_ >= LnCurrencyUnits.zero) + } + + def realisticLnInvoice: Gen[LnCurrencyUnit] = { + positiveLnCurrencyUnit.suchThat(_.toMSat <= LnPolicy.maxAmountMSat) + } + + def negativeLnCurrencyUnit: Gen[LnCurrencyUnit] = { + lnCurrencyUnit.suchThat(_ < LnCurrencyUnits.zero) + } + + def milliSatoshis: Gen[MilliSatoshis] = for { + i64 <- NumberGenerator.uInt64 + } yield MilliSatoshis(i64.toBigInt) +} + +object LnCurrencyUnitGen extends LnCurrencyUnitGen \ No newline at end of file diff --git a/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnInvoiceGen.scala b/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnInvoiceGen.scala new file mode 100644 index 0000000000..93d9d6236f --- /dev/null +++ b/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnInvoiceGen.scala @@ -0,0 +1,167 @@ +package org.bitcoins.core.gen.ln + +import org.bitcoins.core.crypto.{ECPrivateKey, ECPublicKey} +import org.bitcoins.core.gen._ +import org.bitcoins.core.number.{UInt64, UInt8} +import org.bitcoins.core.protocol.ln.LnTag.NodeIdTag +import org.bitcoins.core.protocol.ln._ +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.core.util.NumberUtil +import org.scalacheck.Gen + +sealed abstract class LnInvoiceGen { + + /** + * Generates a [[org.bitcoins.core.protocol.ln.LnHumanReadablePart]] + * that does not contain a amount + * @return + */ + def lnHrpNoAmt: Gen[LnHumanReadablePart] = { + ChainParamsGenerator.lnNetworkParams.flatMap { + lnParam => + LnHumanReadablePart.fromLnParams(lnParam) + } + } + + /** + * Generates a [[org.bitcoins.core.protocol.ln.LnHumanReadablePart]] + * with an amount encoded + */ + def lnHrpAmt: Gen[LnHumanReadablePart] = { + ChainParamsGenerator.lnNetworkParams.flatMap { lnParam => + LnCurrencyUnitGen.realisticLnInvoice.map { lcu => + LnHumanReadablePart.fromParamsAmount( + network = lnParam, + amount = Some(lcu)) + } + } + } + + def lnHrp: Gen[LnHumanReadablePart] = { + Gen.oneOf(lnHrpAmt, lnHrpNoAmt) + } + + + def nodeId: Gen[NodeId] = { + CryptoGenerators.publicKey.map(NodeId(_)) + } + + def paymentHashTag: Gen[LnTag.PaymentHashTag] = { + CryptoGenerators.sha256Digest.map { hash => + LnTag.PaymentHashTag(hash) + } + } + + def descriptionTag: Gen[LnTag.DescriptionTag] = { + StringGenerators.genString.map { description => + LnTag.DescriptionTag(description) + } + } + + def descriptionHashTag: Gen[LnTag.DescriptionHashTag] = { + descriptionTag.map { desc => + desc.descriptionHashTag + } + } + + def descriptionOrDescriptionHashTag: Gen[Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag]] = { + if (scala.util.Random.nextBoolean()) { + descriptionTag.map(Left(_)) + } else { + descriptionHashTag.map(Right(_)) + } + } + + def expiryTime: Gen[LnTag.ExpiryTimeTag] = { + NumberGenerator.uInt32s.map { u32 => + LnTag.ExpiryTimeTag(u32) + } + } + + def cltvExpiry: Gen[LnTag.MinFinalCltvExpiry] = { + NumberGenerator.uInt32s.map { u32 => + LnTag.MinFinalCltvExpiry(u32) + + } + } + + def fallbackAddress: Gen[LnTag.FallbackAddressTag] = { + AddressGenerator.address.map { addr => + LnTag.FallbackAddressTag(addr) + } + } + + def nodeIdTag(nodeId: NodeId): Gen[LnTag.NodeIdTag] = { + LnTag.NodeIdTag(nodeId) + } + + def routingInfo: Gen[LnTag.RoutingInfo] = { + LnRouteGen.routes + .map(rs => LnTag.RoutingInfo(rs)) + } + + /** Generated a tagged fields with an explicit [[LnTag.NodeIdTag]] + * */ + def taggedFields(nodeIdOpt: Option[NodeId]): Gen[LnTaggedFields] = for { + paymentHash <- paymentHashTag + descOrHashTag <- descriptionOrDescriptionHashTag + + //optional fields + expiryTime <- Gen.option(expiryTime) + cltvExpiry <- Gen.option(cltvExpiry) + fallbackAddress <- Gen.option(fallbackAddress) + routes <- Gen.option(routingInfo) + } yield LnTaggedFields( + paymentHash = paymentHash, + descriptionOrHash = descOrHashTag, + expiryTime = expiryTime, + cltvExpiry = cltvExpiry, + fallbackAddress = fallbackAddress, + nodeId = nodeIdOpt.map(NodeIdTag(_)), + routingInfo = routes) + + def signatureVersion: Gen[UInt8] = { + Gen.choose(0, 3).map(UInt8(_)) + } + + def lnInvoiceSignature: Gen[LnInvoiceSignature] = for { + sig <- CryptoGenerators.digitalSignature + version <- signatureVersion + } yield LnInvoiceSignature(version, sig) + + + def invoiceTimestamp: Gen[UInt64] = { + Gen.choose(0, LnInvoice.MAX_TIMESTAMP_U64.toLong).map(UInt64(_)) + } + + def lnInvoice(privateKey: ECPrivateKey): Gen[LnInvoice] = for { + hrp <- lnHrp + //timestamp is 35 bits according to BOLT11 + timestamp <- invoiceTimestamp + nodeIdOpt <- Gen.option(NodeId(privateKey.publicKey)) + tags <- taggedFields(nodeIdOpt) + } yield { + val signature = LnInvoice.buildLnInvoiceSignature( + hrp = hrp, + timestamp = timestamp, + lnTags = tags, + privateKey = privateKey + ) + + LnInvoice( + hrp = hrp, + timestamp = timestamp, + lnTags = tags, + signature = signature) + } + + + def lnInvoice: Gen[LnInvoice] = { + CryptoGenerators.privateKey.flatMap { p => + val i = lnInvoice(p) + i + } + } +} + +object LnInvoiceGen extends LnInvoiceGen diff --git a/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnRouteGen.scala b/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnRouteGen.scala new file mode 100644 index 0000000000..71d920fe8d --- /dev/null +++ b/core-gen/src/test/scala/org/bitcoins/core/gen/ln/LnRouteGen.scala @@ -0,0 +1,49 @@ +package org.bitcoins.core.gen.ln + +import org.bitcoins.core.gen.{ CryptoGenerators, NumberGenerator } +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.protocol.ln.ShortChannelId +import org.bitcoins.core.protocol.ln.currency.MilliSatoshis +import org.bitcoins.core.protocol.ln.fee.{ FeeBaseMSat, FeeProportionalMillionths } +import org.bitcoins.core.protocol.ln.routing.LnRoute +import org.scalacheck.Gen + +trait LnRouteGen { + def shortChannelId: Gen[ShortChannelId] = { + NumberGenerator.uInt64s.map { u64 => + ShortChannelId(u64) + } + } + + def feeBaseMSat: Gen[FeeBaseMSat] = for { + //note that the feebase msat is only 4 bytes + u32 <- NumberGenerator.uInt32s + } yield { + val ms = MilliSatoshis(u32.toBigInt) + FeeBaseMSat(ms) + } + + def feeProportionalMillionths: Gen[FeeProportionalMillionths] = for { + fee <- NumberGenerator.uInt32s + } yield FeeProportionalMillionths(fee) + + def route: Gen[LnRoute] = for { + pubKey <- CryptoGenerators.publicKey + id <- shortChannelId + baseFee <- feeBaseMSat + feeProp <- feeProportionalMillionths + cltvExpiryDelta <- NumberGenerator.positiveShort + } yield LnRoute( + pubkey = pubKey, + shortChannelID = id, + feeBaseMsat = baseFee, + feePropMilli = feeProp, + cltvExpiryDelta = cltvExpiryDelta) + + def routes: Gen[Vector[LnRoute]] = { + Gen.choose(1, 5) + .flatMap(n => Gen.listOfN(n, route).map(_.toVector)) + } +} + +object LnRouteGen extends LnRouteGen \ No newline at end of file diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/DERSignatureUtilTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/DERSignatureUtilTest.scala index 09d37c22ea..4b045da084 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/DERSignatureUtilTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/DERSignatureUtilTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.crypto -import org.bitcoins.core.util.{ BitcoinSUtil, NumberUtil } +import org.bitcoins.core.util.NumberUtil import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/ECPublicKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/ECPublicKeyTest.scala index 93b9f0bbd0..1f364392aa 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/ECPublicKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/ECPublicKeyTest.scala @@ -1,9 +1,8 @@ package org.bitcoins.core.crypto -import java.math.BigInteger - import org.bitcoinj.core.Sha256Hash -import org.bitcoins.core.util.BitcoinSUtil +import org.bitcoins.core.gen.CryptoGenerators +import org.scalatest.prop.PropertyChecks import org.scalatest.{ FlatSpec, MustMatchers } import scodec.bits.ByteVector @@ -54,4 +53,11 @@ class ECPublicKeyTest extends FlatSpec with MustMatchers { bitcoinsSignature.bytes.toArray) must be(true) } + it must "have serialization symmetry from ECPublicKey -> ECPoint -> ECPublicKey" in { + PropertyChecks.forAll(CryptoGenerators.publicKey) { pubKey => + val p = pubKey.toPoint + val pub2 = ECPublicKey.fromPoint(p, pubKey.isCompressed) + assert(pubKey == pub2) + } + } } diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCheckerTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCheckerTest.scala index f71a8f3bda..6f53bf5eca 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCheckerTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCheckerTest.scala @@ -1,11 +1,5 @@ package org.bitcoins.core.crypto -import org.bitcoins.core.policy.Policy -import org.bitcoins.core.protocol.script.ScriptSignature -import org.bitcoins.core.protocol.transaction._ -import org.bitcoins.core.script.constant.ScriptConstant -import org.bitcoins.core.script.flag.ScriptVerifyDerSig -import org.bitcoins.core.util._ import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCreatorTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCreatorTest.scala index 2b5e669ae3..b01e5f14b5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCreatorTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureCreatorTest.scala @@ -5,7 +5,7 @@ import org.bitcoins.core.number.UInt32 import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.transaction._ -import org.bitcoins.core.script.{ PreExecutionScriptProgram, ScriptProgram } +import org.bitcoins.core.script.PreExecutionScriptProgram import org.bitcoins.core.script.crypto.HashType import org.bitcoins.core.script.interpreter.ScriptInterpreter import org.bitcoins.core.script.result.ScriptOk diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureSerializerTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureSerializerTest.scala index 09e640af5b..2c7e943260 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureSerializerTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/TransactionSignatureSerializerTest.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.crypto -import org.bitcoins.core.currency.Bitcoins -import org.bitcoins.core.currency.{ CurrencyUnits, Satoshis } +import org.bitcoins.core.currency.{ Bitcoins, CurrencyUnits, Satoshis } import org.bitcoins.core.number.{ Int32, Int64, UInt32 } import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.script._ diff --git a/core-test/src/test/scala/org/bitcoins/core/number/Int32Spec.scala b/core-test/src/test/scala/org/bitcoins/core/number/Int32Spec.scala index da287c0ee1..82470e1b7c 100644 --- a/core-test/src/test/scala/org/bitcoins/core/number/Int32Spec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/number/Int32Spec.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.number import org.bitcoins.core.gen.NumberGenerator -import org.bitcoins.core.util.BitcoinSLogger import org.scalacheck.{ Prop, Properties } import scala.util.Try diff --git a/core-test/src/test/scala/org/bitcoins/core/number/UInt32Spec.scala b/core-test/src/test/scala/org/bitcoins/core/number/UInt32Spec.scala index 2de2457f76..67ddf59a6e 100644 --- a/core-test/src/test/scala/org/bitcoins/core/number/UInt32Spec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/number/UInt32Spec.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.number import org.bitcoins.core.gen.NumberGenerator -import org.bitcoins.core.util.{ BitcoinSLogger, NumberUtil } +import org.bitcoins.core.util.BitcoinSLogger import org.scalacheck.{ Gen, Prop, Properties } import scala.util.Try diff --git a/core-test/src/test/scala/org/bitcoins/core/number/UInt5Test.scala b/core-test/src/test/scala/org/bitcoins/core/number/UInt5Test.scala new file mode 100644 index 0000000000..fa3909be32 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/number/UInt5Test.scala @@ -0,0 +1,53 @@ +package org.bitcoins.core.number + +import org.bitcoins.core.gen.NumberGenerator +import org.scalatest.prop.PropertyChecks +import org.scalatest.{ FlatSpec, MustMatchers } +import org.slf4j.LoggerFactory + +class UInt5Test extends FlatSpec with MustMatchers with PropertyChecks { + + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) + + behavior of "UInt5" + + it must "convert a byte to a UInt5 correctly" in { + UInt5.fromByte(0.toByte) must be(UInt5.zero) + UInt5(1.toByte) must be(UInt5.one) + + UInt5(31.toByte) must be(UInt5.max) + } + + it must "not allow negative numbers" in { + intercept[IllegalArgumentException] { + UInt5(-1) + } + } + + it must "not allow numbers more than 31" in { + intercept[IllegalArgumentException] { + UInt5(32) + } + } + + it must "have serialization symmetry" in { + forAll(NumberGenerator.uInt5) { u5 => + val u52 = UInt5.fromHex(u5.hex) + u52 == u5 + } + } + + it must "uint5 -> byte -> uint5" in { + forAll(NumberGenerator.uInt5) { u5 => + val byte = u5.byte + UInt5.fromByte(byte) == u5 + } + } + + it must "uint5 -> uint8 -> uint5" in { + forAll(NumberGenerator.uInt5) { u5 => + val u8 = u5.toUInt8 + u8.toUInt5 == u5 + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/number/UInt64Spec.scala b/core-test/src/test/scala/org/bitcoins/core/number/UInt64Spec.scala index 73ba02ea44..9dea642a12 100644 --- a/core-test/src/test/scala/org/bitcoins/core/number/UInt64Spec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/number/UInt64Spec.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.number import org.bitcoins.core.gen.NumberGenerator -import org.bitcoins.core.util.BitcoinSLogger import org.scalacheck.{ Prop, Properties } import scala.util.Try diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/AddressFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/AddressFactoryTest.scala index 2ed3c462e8..30b7ce4591 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/AddressFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/AddressFactoryTest.scala @@ -1,10 +1,8 @@ package org.bitcoins.core.protocol -import org.bitcoins.core.util.{ Base58, BitcoinSUtil, TestUtil } +import org.bitcoins.core.util.{ Base58, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } -import scala.util.Success - /** * Created by chris on 3/30/16. */ diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala index caabc4a430..6c4e0b05b5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala @@ -1,6 +1,5 @@ package org.bitcoins.core.protocol -import org.bitcoins.core.util.{ BitcoinSLogger, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala index e14eb07d37..cd653ccff8 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala @@ -1,8 +1,8 @@ package org.bitcoins.core.protocol import org.bitcoins.core.gen.{ AddressGenerator, ChainParamsGenerator, ScriptGenerators } -import org.bitcoins.core.util.BitcoinSLogger -import org.scalacheck.{ Gen, Prop, Properties } +import org.bitcoins.core.util.{ Bech32, BitcoinSLogger } +import org.scalacheck.{ Prop, Properties } import scala.annotation.tailrec import scala.util.{ Random, Success } @@ -14,7 +14,7 @@ class Bech32Spec extends Properties("Bech32Spec") { Prop.forAll(ScriptGenerators.witnessScriptPubKey, ChainParamsGenerator.networkParams) { case ((witSPK, _), network) => val addr = Bech32Address(witSPK, network) - val spk = addr.flatMap(a => Bech32Address.fromStringToWitSPK(a.value)) + val spk = Bech32Address.fromStringToWitSPK(addr.value) spk == Success(witSPK) } } @@ -47,7 +47,7 @@ class Bech32Spec extends Properties("Bech32Spec") { @tailrec private def pickReplacementChar(oldChar: Char): Char = { val rand = Math.abs(Random.nextInt) - val newChar = Bech32Address.charset(rand % Bech32Address.charset.size) + val newChar = Bech32.charset(rand % Bech32.charset.size) //make sure we don't pick the same char we are replacing in the bech32 address if (oldChar == newChar) pickReplacementChar(oldChar) else newChar diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala index 378916a797..3aa652bed9 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala @@ -2,14 +2,16 @@ package org.bitcoins.core.protocol import org.bitcoins.core.config.{ MainNet, TestNet3 } import org.bitcoins.core.crypto.ECPublicKey -import org.bitcoins.core.number.UInt8 +import org.bitcoins.core.number.{ UInt5, UInt8 } import org.bitcoins.core.protocol.script._ +import org.bitcoins.core.util.Bech32 import org.scalatest.{ FlatSpec, MustMatchers } +import org.slf4j.LoggerFactory import scala.util.{ Success, Try } class Bech32Test extends FlatSpec with MustMatchers { - + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) "Bech32" must "validly encode the test vectors from bitcoin core correctly" in { val valid = Seq( "A12UEL5L", @@ -46,66 +48,75 @@ class Bech32Test extends FlatSpec with MustMatchers { val key = ECPublicKey("0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798".toLowerCase) val p2wpkh = P2WPKHWitnessSPKV0(key) val addr = Bech32Address(p2wpkh, TestNet3) - addr.map(_.value) must be(Success("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")) + addr.value must be("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") //decode - val decoded = addr.flatMap(a => Bech32Address.fromStringToWitSPK(a.value)) + val decoded = Bech32Address.fromStringToWitSPK(addr.value) decoded must be(Success(p2wpkh)) val p2wpkhMain = Bech32Address(p2wpkh, MainNet) - p2wpkhMain.map(_.value) must be(Success("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) + p2wpkhMain.value must be("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") - val mp2wpkhDecoded = p2wpkhMain.flatMap(a => Bech32Address.fromStringToWitSPK(a.value)) + val mp2wpkhDecoded = Bech32Address.fromStringToWitSPK(p2wpkhMain.value) mp2wpkhDecoded must be(Success(p2wpkh)) val p2pk = P2PKScriptPubKey(key) val p2wsh = P2WSHWitnessSPKV0(p2pk) val addr1 = Bech32Address(p2wsh, TestNet3) - addr1.map(_.value) must be(Success("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")) + addr1.value must be("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7") //decode - val decoded1 = addr1.flatMap(a => Bech32Address.fromStringToWitSPK(a.value)) + val decoded1 = Bech32Address.fromStringToWitSPK(addr1.value) decoded1 must be(Success(p2wsh)) val p2wshMain = Bech32Address(p2wsh, MainNet) - p2wshMain.map(_.value) must be(Success("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - val mp2wshDecoded = p2wshMain.flatMap(a => Bech32Address.fromStringToWitSPK(a.value)) + p2wshMain.value must be("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") + val mp2wshDecoded = Bech32Address.fromStringToWitSPK(p2wshMain.value) mp2wshDecoded must be(Success(p2wsh)) } + it must "expand the human readable part correctly" in { + Bech32Address.hrpExpand(bc) must be(Vector(UInt5(3), UInt5(3), UInt5(0), UInt5(2), UInt5(3))) + + Bech32Address.hrpExpand(tb) must be(Vector(UInt5(3), UInt5(3), UInt5(0), UInt5(20), UInt5(2))) + } + it must "encode 0 byte correctly" in { - val addr = Bech32Address(bc, Seq(UInt8.zero)) + val addr = Bech32Address(bc, Vector(UInt5.zero)) addr.value must be("bc1q9zpgru") } it must "create the correct checksum for a 0 byte address" in { - val checksum = Bech32Address.createChecksum(bc, Seq(UInt8.zero)) - checksum must be(Seq(5, 2, 1, 8, 3, 28).map(i => UInt8(i.toShort))) - checksum.map(ch => Bech32Address.charset(ch.toInt)).mkString must be("9zpgru") + val checksum = Bech32Address.createChecksum(bc, Vector(UInt5.zero)) + checksum must be(Seq(5, 2, 1, 8, 3, 28).map(i => UInt5(i.toByte))) + checksum.map(ch => Bech32.charset(ch.toInt)).mkString must be("9zpgru") } - it must "encode base 8 to base 5" in { + it must "encode from uint8 to uint5" in { val z = UInt8.zero - val encoded = Bech32Address.encode(Seq(z)) - encoded.map(Bech32Address.encodeToString(_)) must be(Success("qq")) + val fz = UInt5.zero + val encoded = Bech32.from8bitTo5bit(Vector(z)) - val encoded1 = Bech32Address.encode(Seq(z, UInt8.one)) - encoded1 must be(Success(Seq(z, z, z, UInt8(16.toShort)))) + Bech32.encode5bitToString(encoded) must be("qq") + + val encoded1 = Bech32.from8bitTo5bit(Vector(z, UInt8.one)) + encoded1 must be(Seq(fz, fz, fz, UInt5(16.toByte))) //130.toByte == -126 - val encoded2 = Bech32Address.encode(Seq(130).map(i => UInt8(i.toShort))) - encoded2 must be(Success(Seq(16, 8).map(i => UInt8(i.toShort)))) + val encoded2 = Bech32.from8bitTo5bit(Vector(130).map(i => UInt8(i.toShort))) + encoded2 must be(Seq(16, 8).map(i => UInt5(i.toByte))) //130.toByte == -126 - val encoded3 = Bech32Address.encode(Seq(255, 255).map(i => UInt8(i.toShort))) - encoded3 must be(Success(Seq(31, 31, 31, 16).map(i => UInt8(i.toShort)))) + val encoded3 = Bech32.from8bitTo5bit(Vector(255, 255).map(i => UInt8(i.toShort))) + encoded3 must be(Seq(31, 31, 31, 16).map(i => UInt5(i.toByte))) - val encoded4 = Bech32Address.encode(Seq(255, 255, 255, 255).map(i => UInt8(i.toShort))) - encoded4 must be(Success(Seq(31, 31, 31, 31, 31, 31, 24).map(i => UInt8(i.toShort)))) + val encoded4 = Bech32.from8bitTo5bit(Vector(255, 255, 255, 255).map(i => UInt8(i.toShort))) + encoded4 must be(Seq(31, 31, 31, 31, 31, 31, 24).map(i => UInt5(i.toByte))) - val encoded5 = Bech32Address.encode(Seq(255, 255, 255, 255, 255).map(i => UInt8(i.toShort))) - encoded5 must be(Success(Seq(31, 31, 31, 31, 31, 31, 31, 31).map(i => UInt8(i.toShort)))) + val encoded5 = Bech32.from8bitTo5bit(Vector(255, 255, 255, 255, 255).map(i => UInt8(i.toShort))) + encoded5 must be(Seq(31, 31, 31, 31, 31, 31, 31, 31).map(i => UInt5(i.toByte))) - val encoded6 = Bech32Address.encode(Seq(255, 255, 255, 255, 255, 255).map(i => UInt8(i.toShort))) - encoded6 must be(Success(Seq(31, 31, 31, 31, 31, 31, 31, 31, 31, 28).map(i => UInt8(i.toShort)))) + val encoded6 = Bech32.from8bitTo5bit(Vector(255, 255, 255, 255, 255, 255).map(i => UInt8(i.toByte))) + + encoded6 must be(Seq(31, 31, 31, 31, 31, 31, 31, 31, 31, 28).map(i => UInt5(i.toByte))) } } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala index db788ea1bc..e2140e6fbc 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala @@ -3,7 +3,6 @@ package org.bitcoins.core.protocol import org.bitcoins.core.config.MainNet import org.bitcoins.core.crypto.Sha256Hash160Digest import org.bitcoins.core.protocol.script.ScriptPubKey -import org.bitcoins.core.util.Base58 import org.scalatest.{ FlatSpec, MustMatchers } import scala.util.Try diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/BlockTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/BlockTest.scala index eb4110db12..8065937080 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/BlockTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/BlockTest.scala @@ -1,10 +1,5 @@ package org.bitcoins.core.protocol.blockchain -import java.io.File - -import org.bitcoins.core.protocol.CompactSizeUInt -import org.bitcoins.core.serializers.script.RawScriptSignatureParser -import org.bitcoins.core.util.BitcoinSLogger import org.scalatest.{ FlatSpec, MustMatchers } import org.slf4j.LoggerFactory diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/MerkleBlockTests.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/MerkleBlockTests.scala index 00634b0f06..8525f2b8ca 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/MerkleBlockTests.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/blockchain/MerkleBlockTests.scala @@ -4,7 +4,7 @@ import org.bitcoins.core.bloom._ import org.bitcoins.core.crypto.{ DoubleSha256Digest, ECPublicKey } import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.transaction.TransactionOutPoint -import org.bitcoins.core.util.{ BitcoinSUtil, _ } +import org.bitcoins.core.util.BitcoinSUtil import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePartTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePartTest.scala new file mode 100644 index 0000000000..08b7ebdebb --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePartTest.scala @@ -0,0 +1,61 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.config.{ MainNet, RegTest, TestNet3 } +import org.bitcoins.core.protocol.ln.LnParams._ +import org.bitcoins.core.protocol.ln.currency.{ LnCurrencyUnits, MilliBitcoins } +import org.scalatest.{ FlatSpec, MustMatchers } + +import scala.util.Try + +class LnHumanReadablePartTest extends FlatSpec with MustMatchers { + val mBtc = MilliBitcoins(1) + val mBtcOpt = Some(mBtc) + it must "match the correct hrp with the correct network" in { + LnHumanReadablePart(MainNet) must be(LnHumanReadablePart(LnBitcoinMainNet)) + LnHumanReadablePart(TestNet3) must be(LnHumanReadablePart(LnBitcoinTestNet)) + LnHumanReadablePart(RegTest) must be(LnHumanReadablePart(LnBitcoinRegTest)) + + LnHumanReadablePart(MainNet, mBtc) must be(LnHumanReadablePart(LnBitcoinMainNet, mBtcOpt)) + LnHumanReadablePart(TestNet3, mBtc) must be(LnHumanReadablePart(LnBitcoinTestNet, mBtcOpt)) + LnHumanReadablePart(RegTest, mBtc) must be(LnHumanReadablePart(LnBitcoinRegTest, mBtcOpt)) + } + + it must "correctly serialize the hrp to string" in { + LnHumanReadablePart(LnBitcoinMainNet, mBtcOpt).toString must be("lnbc1m") + LnHumanReadablePart(LnBitcoinTestNet, mBtcOpt).toString must be("lntb1m") + LnHumanReadablePart(LnBitcoinRegTest, mBtcOpt).toString must be("lnbcrt1m") + + LnHumanReadablePart(LnBitcoinMainNet).toString must be("lnbc") + LnHumanReadablePart(LnBitcoinTestNet).toString must be("lntb") + LnHumanReadablePart(LnBitcoinRegTest).toString must be("lnbcrt") + } + + it must "fail to create hrp from invalid amount" in { + val tooBig = Some(MilliBitcoins(LnPolicy.maxAmountMSat.toBigInt + 1)) + val zero = Some(LnCurrencyUnits.zero) + val tooSmall = Some(MilliBitcoins(-1)) + + Try(LnHumanReadablePart(LnBitcoinMainNet, tooBig)).isFailure must be(true) + Try(LnHumanReadablePart(LnBitcoinMainNet, zero)).isFailure must be(true) + Try(LnHumanReadablePart(LnBitcoinMainNet, tooSmall)).isFailure must be(true) + + } + + it must "deserialize hrp from string" in { + + LnHumanReadablePart.fromString("lnbc").get must be(LnHumanReadablePart(LnBitcoinMainNet)) + LnHumanReadablePart.fromString("lntb").get must be(LnHumanReadablePart(LnBitcoinTestNet)) + LnHumanReadablePart.fromString("lnbcrt").get must be(LnHumanReadablePart(LnBitcoinRegTest)) + + LnHumanReadablePart.fromString("lnbc1m").get must be(LnHumanReadablePart(LnBitcoinMainNet, mBtcOpt)) + LnHumanReadablePart.fromString("lntb1m").get must be(LnHumanReadablePart(LnBitcoinTestNet, mBtcOpt)) + LnHumanReadablePart.fromString("lnbcrt1m").get must be(LnHumanReadablePart(LnBitcoinRegTest, mBtcOpt)) + } + + it must "fail to deserialize hrp from invalid string" in { + LnHumanReadablePart.fromString("invalid").isFailure must be(true) + LnHumanReadablePart.fromString("lnbc9000").isFailure must be(true) + LnHumanReadablePart.fromString("lnbc90z0m").isFailure must be(true) + + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala new file mode 100644 index 0000000000..440388f7d9 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala @@ -0,0 +1,380 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.crypto._ +import org.bitcoins.core.gen.ln.LnInvoiceGen +import org.bitcoins.core.number.{ UInt32, UInt64, UInt8 } +import org.bitcoins.core.protocol.ln.LnParams.{ LnBitcoinMainNet, LnBitcoinTestNet } +import org.bitcoins.core.protocol.ln.currency.{ MicroBitcoins, MilliBitcoins, MilliSatoshis } +import org.bitcoins.core.protocol.ln.fee.{ FeeBaseMSat, FeeProportionalMillionths } +import org.bitcoins.core.protocol.ln.routing.LnRoute +import org.bitcoins.core.protocol.{ Bech32Address, P2PKHAddress, P2SHAddress } +import org.bitcoins.core.util.CryptoUtil +import org.scalatest.prop.PropertyChecks +import org.scalatest.{ FlatSpec, MustMatchers } +import org.slf4j.LoggerFactory +import scodec.bits.ByteVector + +class LnInvoiceUnitTest extends FlatSpec with MustMatchers with PropertyChecks { + behavior of "LnInvoice" + + private val logger = LoggerFactory.getLogger(getClass.getSimpleName) + + val hrpEmpty = LnHumanReadablePart(LnBitcoinMainNet) + val hrpMicro = LnHumanReadablePart(LnBitcoinMainNet, Some(MicroBitcoins(2500))) + val hrpMilli = LnHumanReadablePart(LnBitcoinMainNet, Some(MilliBitcoins(20))) + val hrpTestNetMilli = LnHumanReadablePart(LnBitcoinTestNet, Some(MilliBitcoins(20))) + val time = UInt64(1496314658) + + val paymentHash = Sha256Digest.fromHex("0001020304050607080900010203040506070809000102030405060708090102") + val paymentTag = LnTag.PaymentHashTag(paymentHash) + + val description = { + ("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, " + + "one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, " + + "and one slice of watermelon").getBytes() + } + val descriptionHash = CryptoUtil.sha256(ByteVector(description)) + + val descpriptionHashTag = Right(LnTag.DescriptionHashTag(descriptionHash)) + + it must "parse BOLT11 example 1" in { + //BOLT11 Example #1 + + val descriptionTagE = Left(LnTag.DescriptionTag("Please consider supporting this project")) + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descriptionTagE) + + val sigData = "6c6e62630b25fe64410d00004080c1014181c20240004080c1014181c20240004080c1014181c202404081a1fa83632b0b9b29031b7b739b4b232b91039bab83837b93a34b733903a3434b990383937b532b1ba0" + val hashSigData = Sha256Digest.fromHex("c3d4e83f646fa79a393d75277b1d858db1d1f7ab7137dcb7835db2ecd518e1c9") + + val signature = ECDigitalSignature.fromRS("38ec6891345e204145be8a3a99de38e98a39d6a569434e1845c8af7205afcfcc7f425fcd1463e93c32881ead0d6e356d467ec8c02553f9aab15e5738b11f127f") + val version = UInt8.zero + val lnSig = LnInvoiceSignature(version, signature) + + val invoice = LnInvoice(hrpEmpty, time, lnTags, lnSig) + + invoice.signatureData.toHex must be(sigData) + + val serialized = invoice.toString + serialized must be("lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w") + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get.toString must be(invoice.toString) + } + + it must "parse BOLT11 example 2" in { + //BOLT11 Example #2 + + val descriptionTagE = Left(LnTag.DescriptionTag("1 cup coffee")) + val expiryTimeTag = LnTag.ExpiryTimeTag(UInt32(60)) + val lnTags = LnTaggedFields( + paymentTag, + descriptionOrHash = descriptionTagE, + expiryTime = Some(expiryTimeTag)) + + val signature = ECDigitalSignature.fromRS("e89639ba6814e36689d4b91bf125f10351b55da057b00647a8dabaeb8a90c95f160f9d5a6e0f79d1fc2b964238b944e2fa4aa677c6f020d466472ab842bd750e") + val version = UInt8.one + val lnSig = LnInvoiceSignature(version, signature) + + val invoice = LnInvoice(hrpMicro, time, lnTags, lnSig) + + val serialized = invoice.toString + + serialized must be("lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp") + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get.toString must be(invoice.toString) + } + + it must "parse BOLT11 example 3" in { + //BOLT11 Example #3 - Description field does not encode correctly due to Japanese letters + + val descriptionTagE = Left(LnTag.DescriptionTag("ナンセンス 1杯")) + val expiryTag = LnTag.ExpiryTimeTag(UInt32(60)) + val lnTags = LnTaggedFields( + paymentTag, descriptionTagE, None, + Some(expiryTag), None, None, + None) + + val signature = ECDigitalSignature.fromRS("259f04511e7ef2aa77f6ff04d51b4ae9209504843e5ab9672ce32a153681f687515b73ce57ee309db588a10eb8e41b5a2d2bc17144ddf398033faa49ffe95ae6") + val version = UInt8.zero + val lnSig = LnInvoiceSignature(version, signature) + + val invoice = LnInvoice(hrpMicro, time, lnTags, lnSig) + + val serialized = invoice.toString + + invoice.toString must be("lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny") + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get must be(invoice) + } + + it must "parse BOLT11 example 4" in { + //BOLT11 Example #4 + + val descriptionHash = Sha256Digest.fromHex("3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1") + val descriptionHashTagE = Right(LnTag.DescriptionHashTag(descriptionHash)) + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descriptionHashTagE, + None, None, None, + None, None) + + val signature = ECDigitalSignature.fromRS("c63486e81f8c878a105bc9d959af1973854c4dc552c4f0e0e0c7389603d6bdc67707bf6be992a8ce7bf50016bb41d8a9b5358652c4960445a170d049ced4558c") + val version = UInt8.zero + val lnSig = LnInvoiceSignature(version, signature) + + val invoice = LnInvoice(hrpMilli, time, lnTags, lnSig) + + val serialized = invoice.toString + + invoice.toString must be("lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7") + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get must be(invoice) + } + + it must "parse BOLT11 example 5" in { + //BOLT11 Example #5 + + val descriptionHash = Sha256Digest.fromHex("3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1") + val descriptionHashTagE = Right(LnTag.DescriptionHashTag(descriptionHash)) + val fallbackAddr = LnTag.FallbackAddressTag(P2PKHAddress.fromString("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP").get) + + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descriptionHashTagE, + fallbackAddress = Some(fallbackAddr)) + + val signature = ECDigitalSignature.fromRS("b6c42b8a61e0dc5823ea63e76ff148ab5f6c86f45f9722af0069c7934daff70d5e315893300774c897995e3a7476c8193693d144a36e2645a0851e6ebafc9d0a") + val version = UInt8.one + val lnSig = LnInvoiceSignature(version, signature) + + val invoice = LnInvoice(hrpTestNetMilli, time, lnTags, lnSig) + + val serialized = invoice.toString + + serialized must be("lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98kmzzhznpurw9sgl2v0nklu2g4d0keph5t7tj9tcqd8rexnd07ux4uv2cjvcqwaxgj7v4uwn5wmypjd5n69z2xm3xgksg28nwht7f6zsp2mh7qm") + //In example #5, the order in which tags are encoded in the invoice has been changed to demonstrate the ability to move tags as needed. + //For that reason, the example #5 output we are matching against has been modified to fit the order in which we encode our invoices. + //TODO: Add checksum data to check + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get.toString must be(serialized) + } + it must "parse BOLT11 example 6" in { + val expected = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj" + + val fallbackAddr = LnTag.FallbackAddressTag(P2PKHAddress.fromString("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T").get) + + val signature = ECDigitalSignature.fromRS( + "91675cb3fad8e9d915343883a49242e074474e26d42c7ed914655689a8074553733e8e4ea5ce9b85f69e40d755a55014536b12323f8b220600c94ef2b9c51428") + val lnInvoiceSig = LnInvoiceSignature( + version = UInt8.zero, + signature = signature) + + val route1 = LnRoute( + pubkey = ECPublicKey.fromHex("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), + shortChannelID = ShortChannelId.fromHex("0102030405060708"), + feeBaseMsat = FeeBaseMSat(MilliSatoshis.one), + feePropMilli = FeeProportionalMillionths(UInt32(20)), + cltvExpiryDelta = 3) + + val route2 = LnRoute( + pubkey = ECPublicKey.fromHex("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), + shortChannelID = ShortChannelId.fromHex("030405060708090a"), + feeBaseMsat = FeeBaseMSat(MilliSatoshis(2)), + feePropMilli = FeeProportionalMillionths(UInt32(30)), + cltvExpiryDelta = 4) + + val route = LnTag.RoutingInfo(Vector(route1, route2)) + + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descpriptionHashTag, + fallbackAddress = Some(fallbackAddr), + routingInfo = Some(route)) + + val lnInvoice = LnInvoice( + hrp = hrpMilli, + timestamp = time, + lnTags = lnTags, + signature = lnInvoiceSig) + + val serialized = lnInvoice.toString + serialized must be(expected) + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get.toString must be(serialized) + } + + it must "parse BOLT11 example 7 (p2sh fallback addr)" in { + val expected = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq" + + "hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxl" + + "dph2q7z9kmrgvr7xlaqm47apw3d48zm203kzcq357a4ls9al2ea73r8jcceyjtya6fu5wzzpe50zrge6ulk" + + "4nvjcpxlekvmxl6qcs9j3tz0469gqsjurz5" + + val fallbackAddr = LnTag.FallbackAddressTag(P2SHAddress.fromString("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX").get) + + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descpriptionHashTag, + fallbackAddress = Some(fallbackAddr)) + + val signature = ECDigitalSignature.fromRS("b6c6860fc6ff41bafba1745b538b6a7c6c2c0234f76bf817bf567be88cf2c632492c9dd279470841cd1e21a33ae7ed59b25809bf9b3366fe81881651589f5d15") + val lnInvoiceSig = LnInvoiceSignature( + signature = signature, + version = UInt8.zero) + val lnInvoice = LnInvoice( + hrp = hrpMilli, + timestamp = time, + lnTags = lnTags, + signature = lnInvoiceSig) + + val serialized = lnInvoice.toString + + lnInvoice.toString must be(expected) + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get.toString must be(serialized) + } + + it must "parse BOLT11 example 7 (p2wpkh fallback addr)" in { + //this test does not pass because bitcoin-s does not support p2wpkh currently + + val expected = "lnbc20m1pvjluez" + + "pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq" + + "hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs" + + "fppqw508d6qejxtdg4y5r3zarvary0c5xw7kepvrhrm9s57hejg0p66" + + "2ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfu" + + "wzam7yr8e690nd2ypcq9hlkdwdvycqe4x4ch" + + val fallbackAddr = LnTag.FallbackAddressTag(Bech32Address.fromString("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4").get) + + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descpriptionHashTag, + fallbackAddress = Some(fallbackAddr)) + + val signature = ECDigitalSignature.fromRS("c8583b8f65853d7cc90f0eb4ae0e92a606f89caf4f7d65048142d7bbd4e5f3623ef407a75458e4b20f00efbc734f1c2eefc419f3a2be6d51038016ffb35cd613") + + val lnInvoiceSig = LnInvoiceSignature( + signature = signature, + version = UInt8.zero) + + val lnInvoice = LnInvoice( + hrp = hrpMilli, + timestamp = time, + lnTags = lnTags, + signature = lnInvoiceSig) + + val serialized = lnInvoice.toString + + serialized must be(expected) + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get must be(lnInvoice) + } + + it must "parse BOLT11 example 8 (p2wsh fallback addr)" in { + val expected = "lnbc20m1pvjluez" + + "pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq" + + "hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs" + + "fp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3" + + "q28j0v3rwgy9pvjnd48ee2pl8xrpxysd5g44td63g6xcjcu003j3qe8" + + "878hluqlvl3km8rm92f5stamd3jw763n3hck0ct7p8wwj463cqm8cxgy" + + val fallbackAddr = LnTag.FallbackAddressTag(Bech32Address.fromString("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3").get) + + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = descpriptionHashTag, + fallbackAddress = Some(fallbackAddr)) + + val signature = ECDigitalSignature.fromRS("51e4f6446e410a164a6da9f39507e730c26241b4456ab6ea28d1b12c71ef8ca20c9cfe3dffc07d9f8db671ecaa4d20beedb193bda8ce37c59f85f82773a55d47") + + val lnInvoiceSig = LnInvoiceSignature( + signature = signature, + version = UInt8.zero) + + val lnInvoice = LnInvoice( + hrp = hrpMilli, + timestamp = time, + lnTags = lnTags, + signature = lnInvoiceSig) + + val serialized = lnInvoice.toString + + lnInvoice.toString must be(expected) + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get must be(lnInvoice) + } + + it must "deserialize and reserialize a invoice with a explicity expiry time" in { + //from eclair + val bech32 = "lnbcrt1m1pd6ssf3pp5mqcepx6yzx7uu0uagw5x3c7kqhnpwr3mfn844hjux8tlza6ztr7sdqqxqrrss0rl3gzer9gfc54fs84rd4xk6g8nf0syharnnyljc9za933memdzxrjz0v2v94ntuhdxduk3z0nlmpmznryvvvl4gzgu28kjkm4ey98gpmyhjfa" + + val invoiceT = LnInvoice.fromString(bech32) + + val deserialized = invoiceT.get.toString + + deserialized must be(bech32) + } + + it must "have serialization symmetry for LnHrps" in { + forAll(LnInvoiceGen.lnHrp) { hrp => + LnHumanReadablePart.fromString(hrp.toString).get == hrp + } + } + + it must "have serialization symmetry for the invoices" in { + + forAll(LnInvoiceGen.lnInvoice) { invoice => + LnInvoice.fromString(invoice.toString).get == invoice + } + } + + it must "fail to create an invoice if the digital signature is invalid" in { + intercept[IllegalArgumentException] { + val sig = EmptyDigitalSignature + val tags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = Right(LnTag.DescriptionHashTag(descriptionHash))) + val lnSig = LnInvoiceSignature( + version = UInt8.zero, + signature = sig) + LnInvoice( + hrp = hrpEmpty, + timestamp = UInt64.zero, + lnTags = tags, + signature = lnSig) + } + } + + it must "create a valid digital signature for an invoice" in { + val privKey = ECPrivateKey.freshPrivateKey + + val tags = LnTaggedFields( + paymentHash = paymentTag, + descriptionOrHash = Right(LnTag.DescriptionHashTag(descriptionHash))) + + val invoice = LnInvoice.build( + hrp = hrpEmpty, + lnTags = tags, + privateKey = privKey) + + assert(invoice.isValidSignature()) + } +} \ No newline at end of file diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnitTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnitTest.scala new file mode 100644 index 0000000000..c32671108e --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnitTest.scala @@ -0,0 +1,167 @@ +package org.bitcoins.core.protocol.ln.currency + +import org.bitcoins.core.currency.Satoshis +import org.bitcoins.core.protocol.ln.LnPolicy +import org.scalatest.{ FlatSpec, MustMatchers } + +class LnCurrencyUnitTest extends FlatSpec with MustMatchers { + it must "serialize MilliBitcoins to string" in { + val milliBitcoins = MilliBitcoins(1000) + milliBitcoins.toEncodedString must be("1000m") + } + + it must "serialize MicroBitcoins to string" in { + val microBitcoins = MicroBitcoins(1000) + microBitcoins.toEncodedString must be("1000u") + } + + it must "serialize NanoBitcoins to string" in { + val nanoBitcoins = NanoBitcoins(1000) + nanoBitcoins.toEncodedString must be("1000n") + } + + it must "serialize PicoBitcoins to string" in { + val picoBitcoins = PicoBitcoins(1000) + picoBitcoins.toEncodedString must be("1000p") + } + + it must "deserialize MilliBitcoins from string" in { + val input = "1000m" + LnCurrencyUnits.fromEncodedString(input).get must be(MilliBitcoins(1000)) + } + + it must "deserialize MicroBitcoins from string" in { + val input = "1000u" + LnCurrencyUnits.fromEncodedString(input).get must be(MicroBitcoins(1000)) + } + + it must "deserialize NanoBitcoins from string" in { + val input = "1000n" + LnCurrencyUnits.fromEncodedString(input).get must be(NanoBitcoins(1000)) + } + + it must "deserialize PicoBitcoins from string" in { + val input = "1000p" + LnCurrencyUnits.fromEncodedString(input).get must be(PicoBitcoins(1000)) + } + + it must "fail to deserialize an invalid amount" in { + val input = "10000000000000000m" + LnCurrencyUnits.fromEncodedString(input).isFailure must be(true) + } + + it must "fail to deserialize an invalid number" in { + val input = "10z00m" + LnCurrencyUnits.fromEncodedString(input).isFailure must be(true) + } + + it must "fail to deserialize an invalid currency denomination" in { + val input = "1000z" + LnCurrencyUnits.fromEncodedString(input).isFailure must be(true) + } + + it must "have the correct maximum and minimum number representation for MilliBitcoins" in { + MilliBitcoins.max must be(MilliBitcoins(9223372036L)) + MilliBitcoins.min must be(MilliBitcoins(-9223372036L)) + } + + it must "have the correct maximum and minimum number representation for MicroBitcoins" in { + MicroBitcoins.max must be(MicroBitcoins(9223372036854L)) + MicroBitcoins.min must be(MicroBitcoins(-9223372036854L)) + } + + it must "have the correct maximum and minimum number representation for NanoBitcoins" in { + NanoBitcoins.max must be(NanoBitcoins(9223372036854775L)) + NanoBitcoins.min must be(NanoBitcoins(-9223372036854775L)) + } + + it must "have the correct maximum and minimum number representation for PicoBitcoins" in { + PicoBitcoins.max must be(PicoBitcoins(9223372036854775807L)) + PicoBitcoins.min must be(PicoBitcoins(-9223372036854775808L)) + } + + it must "round pico bitcoins to satoshis correctly" in { + PicoBitcoins.one.toSatoshis must be(Satoshis.zero) + + PicoBitcoins(9999).toSatoshis must be(Satoshis.zero) + + PicoBitcoins(10000).toSatoshis must be(Satoshis.one) + + PicoBitcoins(19999).toSatoshis must be(Satoshis.one) + } + + it must "convert units to the correct pico bitcoins amount" in { + + val expectedNano = BigInt(10).pow(3) + NanoBitcoins.one.toPicoBitcoins must be(PicoBitcoins(expectedNano)) + + val expectedMicro = BigInt(10).pow(6) + MicroBitcoins.one.toPicoBitcoins must be(PicoBitcoins(expectedMicro)) + + val expectedMilli = BigInt(10).pow(9) + MilliBitcoins.one.toPicoBitcoins must be(PicoBitcoins(expectedMilli)) + } + + it must "fail to create a MilliBitcoin outside of the maximum range" in { + intercept[IllegalArgumentException] { + MilliBitcoins(LnPolicy.maxMilliBitcoins + 1) + } + } + + it must "fail to create a MicroBitcoin outside of the maximum range" in { + intercept[IllegalArgumentException] { + MicroBitcoins(LnPolicy.maxMicroBitcoins + 1) + } + } + + it must "fail to create a NanoBitcoin outside of the maximum range" in { + intercept[IllegalArgumentException] { + NanoBitcoins(LnPolicy.maxNanoBitcoins + 1) + } + } + + it must "fail to create a PicoBitcion outside of the maximum range" in { + intercept[IllegalArgumentException] { + PicoBitcoins(LnPolicy.maxPicoBitcoins + 1) + } + } + + it must "fail to create a MilliBitcoin outside of the minimum range" in { + intercept[IllegalArgumentException] { + MilliBitcoins(LnPolicy.minMilliBitcoins - 1) + } + } + + it must "fail to create a MicroBitcoin outside of the minimum range" in { + intercept[IllegalArgumentException] { + MicroBitcoins(LnPolicy.minMicroBitcoins - 1) + } + } + + it must "fail to create a NanoBitcoin outside of the minimum range" in { + intercept[IllegalArgumentException] { + NanoBitcoins(LnPolicy.minNanoBitcoins - 1) + } + } + + it must "fail to create a PicoBitcion outside of the minimum range" in { + intercept[IllegalArgumentException] { + PicoBitcoins(LnPolicy.minPicoBitcoins - 1) + } + } + + it must "have the correct representation for 0" in { + MilliBitcoins.zero must be(MilliBitcoins(0)) + MicroBitcoins.zero must be(MicroBitcoins(0)) + NanoBitcoins.zero must be(NanoBitcoins(0)) + PicoBitcoins.zero must be(PicoBitcoins(0)) + LnCurrencyUnits.zero must be(PicoBitcoins(0)) + } + + it must "have the correct representation for 1" in { + MilliBitcoins.one must be(MilliBitcoins(1)) + MicroBitcoins.one must be(MicroBitcoins(1)) + NanoBitcoins.one must be(NanoBitcoins(1)) + PicoBitcoins.one must be(PicoBitcoins(1)) + } +} \ No newline at end of file diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshisTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshisTest.scala new file mode 100644 index 0000000000..48b619ef35 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshisTest.scala @@ -0,0 +1,58 @@ +package org.bitcoins.core.protocol.ln.currency + +import org.bitcoins.core.gen.CurrencyUnitGenerator +import org.bitcoins.core.gen.ln.LnCurrencyUnitGen +import org.scalacheck.Gen +import org.scalatest.prop.PropertyChecks +import org.scalatest.{FlatSpec, MustMatchers} +import org.slf4j.LoggerFactory + +class MilliSatoshisTest extends FlatSpec with MustMatchers { + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) + behavior of "MilliSatoshis" + + it must "convert pico bitcoins to msat correctly" in { + + MilliSatoshis.fromPico(PicoBitcoins.zero) must be(MilliSatoshis.zero) + MilliSatoshis.fromPico(PicoBitcoins.one) must be(MilliSatoshis.zero) + MilliSatoshis.fromPico(PicoBitcoins(9)) must be(MilliSatoshis.zero) + + MilliSatoshis.fromPico(PicoBitcoins(10)) must be(MilliSatoshis.one) + + MilliSatoshis.fromPico(PicoBitcoins(19)) must be(MilliSatoshis.one) + + MilliSatoshis.fromPico(PicoBitcoins(20)) must be(MilliSatoshis(2)) + + MilliSatoshis.fromPico(PicoBitcoins(101)) must be(MilliSatoshis(10)) + + MilliSatoshis.fromPico(PicoBitcoins(110)) must be(MilliSatoshis(11)) + } + + it must "covert from a ln currency unit -> millisatoshis -> lnCurrencyUnit" in { + + PropertyChecks.forAll(LnCurrencyUnitGen.positivePicoBitcoin) { pb => + val underlying = pb.toBigInt + //we lose the last digit of precision converting + //PicoBitcoins -> MilliSatoshis + //this is the expected answer + val expected = (underlying / 10) * 10 + val expectedPico = PicoBitcoins(expected) + + val pico = PicoBitcoins(underlying) + + val msat = MilliSatoshis(pico) + + val lnCurrencyUnit = msat.toLnCurrencyUnit + + assert(expectedPico == lnCurrencyUnit) + } + } + + it must "convert sat -> msat -> sat" in { + PropertyChecks.forAll(CurrencyUnitGenerator.positiveRealistic) { sat => + val msat = MilliSatoshis(sat) + assert(msat.toSatoshis == sat) + + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeySpec.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeySpec.scala index b53ba65e9c..886e5573ff 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeySpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeySpec.scala @@ -1,11 +1,7 @@ package org.bitcoins.core.protocol.script -import org.bitcoins.core.gen.{ TransactionGenerators, ScriptGenerators } -import org.bitcoins.core.script.ScriptProgram -import org.bitcoins.core.script.interpreter.ScriptInterpreter -import org.bitcoins.core.script.result.ScriptOk -import org.bitcoins.core.util.BitcoinSLogger -import org.scalacheck.{ Properties, Prop } +import org.bitcoins.core.gen.ScriptGenerators +import org.scalacheck.{ Prop, Properties } /** * Created by tom on 8/23/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeyTest.scala index c1d56b8a05..68c7dc0fdb 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/CLTVScriptPubKeyTest.scala @@ -2,7 +2,7 @@ package org.bitcoins.core.protocol.script import org.bitcoins.core.crypto.ECPrivateKey import org.bitcoins.core.script.bitwise.OP_EQUALVERIFY -import org.bitcoins.core.script.constant.{ ScriptConstant, ScriptNumber, BytesToPushOntoStack, ScriptToken } +import org.bitcoins.core.script.constant.{ BytesToPushOntoStack, ScriptConstant, ScriptNumber, ScriptToken } import org.bitcoins.core.script.crypto.{ OP_CHECKSIG, OP_HASH160 } import org.bitcoins.core.script.locktime.OP_CHECKLOCKTIMEVERIFY import org.bitcoins.core.script.stack.{ OP_DROP, OP_DUP } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/CSVScriptPubKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/CSVScriptPubKeyTest.scala index 74762ffbbc..4b69303605 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/CSVScriptPubKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/CSVScriptPubKeyTest.scala @@ -2,12 +2,12 @@ package org.bitcoins.core.protocol.script import org.bitcoins.core.crypto.ECPrivateKey import org.bitcoins.core.script.bitwise.OP_EQUALVERIFY -import org.bitcoins.core.script.constant.{ ScriptConstant, ScriptNumber, BytesToPushOntoStack, ScriptToken } +import org.bitcoins.core.script.constant.{ BytesToPushOntoStack, ScriptConstant, ScriptNumber, ScriptToken } import org.bitcoins.core.script.crypto.{ OP_CHECKSIG, OP_HASH160 } import org.bitcoins.core.script.locktime.OP_CHECKSEQUENCEVERIFY import org.bitcoins.core.script.stack.{ OP_DROP, OP_DUP } import org.bitcoins.core.util.TestUtil -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by tom on 9/21/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/MultiSignatureScriptSignatureTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/MultiSignatureScriptSignatureTest.scala index 533e0e99a1..5084b5fe46 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/MultiSignatureScriptSignatureTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/MultiSignatureScriptSignatureTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.protocol.script -import org.bitcoins.core.util.{ BitcoinSLogger, TransactionTestUtil } +import org.bitcoins.core.util.TransactionTestUtil import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKHScriptSignatureTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKHScriptSignatureTest.scala index d13634e10a..c185215ad8 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKHScriptSignatureTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKHScriptSignatureTest.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.protocol.script import org.bitcoins.core.crypto.ECDigitalSignature -import org.bitcoins.core.script.crypto.{ HashType, SIGHASH_ALL } +import org.bitcoins.core.script.crypto.HashType import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } import scodec.bits.ByteVector diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptPubKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptPubKeyTest.scala index baef488525..2030dae32f 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptPubKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptPubKeyTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.protocol.script -import org.bitcoins.core.crypto.{ ECPublicKey } +import org.bitcoins.core.crypto.ECPublicKey import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptSignatureTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptSignatureTest.scala index 6b4b056c08..3efb51b65c 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptSignatureTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2PKScriptSignatureTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.protocol.script -import org.bitcoins.core.crypto.{ ECDigitalSignature } +import org.bitcoins.core.crypto.ECDigitalSignature import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2SHScriptSignatureSpec.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2SHScriptSignatureSpec.scala index 411d671e4b..37c1a15d2d 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2SHScriptSignatureSpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/P2SHScriptSignatureSpec.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.protocol.script import org.bitcoins.core.gen.ScriptGenerators -import org.bitcoins.core.util.BitcoinSLogger import org.scalacheck.{ Prop, Properties } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyFactoryTest.scala index fa87b6ed94..a9f6b3ebc9 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.protocol.script -import org.bitcoins.core.util.{ TestUtil, BitcoinjConversions, BitcoinJTestUtil, BitcoinSUtil } +import org.bitcoins.core.util.{ BitcoinSUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala index a02ff651f3..e38fba06b6 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala @@ -1,12 +1,10 @@ package org.bitcoins.core.protocol.script -import org.bitcoins.core.crypto.ECPrivateKey import org.bitcoins.core.gen.CryptoGenerators import org.bitcoins.core.script.bitwise.OP_EQUALVERIFY import org.bitcoins.core.script.constant._ -import org.bitcoins.core.script.crypto.{ OP_CHECKSIG, OP_CODESEPARATOR, OP_HASH160 } -import org.bitcoins.core.script.locktime.{ OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY } -import org.bitcoins.core.script.stack.{ OP_DROP, OP_DUP } +import org.bitcoins.core.script.crypto.{ OP_CHECKSIG, OP_HASH160 } +import org.bitcoins.core.script.stack.OP_DUP import org.bitcoins.core.util.{ CryptoUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureFactoryTest.scala index 1c71506a7f..f6abdf21d6 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureFactoryTest.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.protocol.script import org.bitcoins.core.crypto._ -import org.bitcoins.core.script.constant.ScriptConstant import org.bitcoins.core.util.{ BitcoinSUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } import scodec.bits.ByteVector diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureTest.scala index 18d618151e..bebf7d9dd6 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptSignatureTest.scala @@ -6,7 +6,7 @@ import org.bitcoins.core.number.Int32 import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.script.testprotocol.SignatureHashTestCase import org.bitcoins.core.protocol.transaction.{ BaseTransaction, Transaction, TransactionOutput, WitnessTransaction } -import org.bitcoins.core.script.crypto.{ HashType, SIGHASH_ALL, SIGHASH_SINGLE } +import org.bitcoins.core.script.crypto.{ HashType, SIGHASH_ALL } import org.bitcoins.core.serializers.script.RawScriptSignatureParser import org.bitcoins.core.util.{ BitcoinSLogger, BitcoinSUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/testprotocol/SignatureHashTestCase.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/testprotocol/SignatureHashTestCase.scala index 7d3b712e96..da5d6bcd21 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/testprotocol/SignatureHashTestCase.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/testprotocol/SignatureHashTestCase.scala @@ -4,7 +4,6 @@ import org.bitcoins.core.crypto.DoubleSha256Digest import org.bitcoins.core.number.{ Int32, UInt32 } import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.transaction.Transaction -import org.bitcoins.core.script.constant.{ ScriptToken, ScriptConstant } import org.bitcoins.core.script.crypto.HashType /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionInputTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionInputTest.scala index 051d62582e..46367f8202 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionInputTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionInputTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.protocol.transaction -import org.bitcoins.core.protocol.script.{ EmptyScriptSignature, P2PKHScriptSignature, P2PKScriptSignature } +import org.bitcoins.core.protocol.script.{ EmptyScriptSignature, P2PKScriptSignature } import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionOutputTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionOutputTest.scala index e00e8a0e3d..685603f144 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionOutputTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionOutputTest.scala @@ -2,7 +2,6 @@ package org.bitcoins.core.protocol.transaction import org.bitcoins.core.currency.CurrencyUnits import org.bitcoins.core.protocol.script.EmptyScriptPubKey -import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionTest.scala index fd2ac36546..b0385a2ef9 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/transaction/TransactionTest.scala @@ -1,17 +1,18 @@ package org.bitcoins.core.protocol.transaction -import org.bitcoins.core.crypto.{ BaseTxSigComponent, TxSigComponent, WitnessTxSigComponentP2SH, WitnessTxSigComponentRaw } +import org.bitcoins.core.crypto.{ BaseTxSigComponent, WitnessTxSigComponentP2SH, WitnessTxSigComponentRaw } import org.bitcoins.core.currency.CurrencyUnits import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.transaction.testprotocol.CoreTransactionTestCase import org.bitcoins.core.protocol.transaction.testprotocol.CoreTransactionTestCaseProtocol._ -import org.bitcoins.core.script.{ PreExecutionScriptProgram, ScriptProgram } +import org.bitcoins.core.script.PreExecutionScriptProgram import org.bitcoins.core.script.interpreter.ScriptInterpreter import org.bitcoins.core.script.result.ScriptOk import org.bitcoins.core.serializers.transaction.RawBaseTransactionParser -import org.bitcoins.core.util.{ BitcoinSLogger, BitcoinSUtil, CryptoUtil, TestUtil } +import org.bitcoins.core.util.{ BitcoinSUtil, CryptoUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } +import org.slf4j.LoggerFactory import spray.json._ import scala.io.Source @@ -20,7 +21,7 @@ import scala.io.Source * Created by chris on 7/14/15. */ class TransactionTest extends FlatSpec with MustMatchers { - private val logger = BitcoinSLogger.logger + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) "Transaction" must "derive the correct txid from the transaction contents" in { //https://btc.blockr.io/api/v1/tx/raw/cddda897b0e9322937ee1f4fd5d6147d60f04a0f4d3b461e4f87066ac3918f2a diff --git a/core-test/src/test/scala/org/bitcoins/core/script/ScriptConstantFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/ScriptConstantFactoryTest.scala index 69cc5f6b0a..ce932426cc 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/ScriptConstantFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/ScriptConstantFactoryTest.scala @@ -2,7 +2,7 @@ package org.bitcoins.core.script import org.bitcoins.core.script.constant.ScriptConstant import org.bitcoins.core.util.BitcoinSUtil -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 4/1/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/ScriptOperationFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/ScriptOperationFactoryTest.scala index 3428c784ee..69a97cbaa4 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/ScriptOperationFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/ScriptOperationFactoryTest.scala @@ -8,7 +8,7 @@ import org.bitcoins.core.script.crypto.OP_RIPEMD160 import org.bitcoins.core.script.locktime.OP_CHECKLOCKTIMEVERIFY import org.bitcoins.core.script.splice.OP_SUBSTR import org.bitcoins.core.script.stack.OP_TOALTSTACK -import org.bitcoins.core.util.{ BitcoinSUtil } +import org.bitcoins.core.util.BitcoinSUtil import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/script/ScriptProgramFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/ScriptProgramFactoryTest.scala index 3804b5c212..d6ac3bf6f5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/ScriptProgramFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/ScriptProgramFactoryTest.scala @@ -3,7 +3,6 @@ package org.bitcoins.core.script import org.bitcoins.core.crypto.BaseTxSigComponent import org.bitcoins.core.currency.CurrencyUnits import org.bitcoins.core.number.UInt32 -import org.bitcoins.core.protocol.script.SigVersionBase import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.script.constant.{ OP_0, OP_1 } import org.bitcoins.core.script.flag.ScriptFlagFactory diff --git a/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticInterpreterTest.scala index 44e1b0e496..7d33d9f0df 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticInterpreterTest.scala @@ -1,9 +1,9 @@ package org.bitcoins.core.script.arithmetic -import org.bitcoins.core.script.result._ -import org.bitcoins.core.script.flag.ScriptFlag -import org.bitcoins.core.script.{ ExecutedScriptProgram, ExecutionInProgressScriptProgram, ScriptProgram } import org.bitcoins.core.script.constant._ +import org.bitcoins.core.script.flag.ScriptFlag +import org.bitcoins.core.script.result._ +import org.bitcoins.core.script.{ ExecutedScriptProgram, ExecutionInProgressScriptProgram, ScriptProgram } import org.bitcoins.core.util.{ ScriptProgramTestUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticOperationsFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticOperationsFactoryTest.scala index 6b9452b7f4..1f5f020d60 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticOperationsFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/arithmetic/ArithmeticOperationsFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.arithmetic -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/8/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/bitwise/BitwiseInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/bitwise/BitwiseInterpreterTest.scala index 5bb7a0991a..dec555a7fe 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/bitwise/BitwiseInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/bitwise/BitwiseInterpreterTest.scala @@ -1,9 +1,8 @@ package org.bitcoins.core.script.bitwise -import org.bitcoins.core.script.{ ExecutedScriptProgram, ScriptProgram } -import org.bitcoins.core.script.arithmetic.OP_NUMEQUAL import org.bitcoins.core.script.constant._ import org.bitcoins.core.script.result.ScriptErrorInvalidStackOperation +import org.bitcoins.core.script.{ ExecutedScriptProgram, ScriptProgram } import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/script/constant/BytesToPushOntoStackFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/constant/BytesToPushOntoStackFactoryTest.scala index 253f2ecd4a..acef66e404 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/constant/BytesToPushOntoStackFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/constant/BytesToPushOntoStackFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.constant -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/9/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberFactoryTest.scala index 3ec478d70f..742a725fad 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberFactoryTest.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.script.constant import org.bitcoins.core.util.BitcoinSUtil -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 4/4/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberSpec.scala b/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberSpec.scala index 0c8b45abc9..4765a7dc67 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberSpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberSpec.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.script.constant import org.bitcoins.core.gen.NumberGenerator -import org.bitcoins.core.util.BitcoinSLogger import org.scalacheck.{ Prop, Properties } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberUtilTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberUtilTest.scala index 26af2189ee..1d2ba44896 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberUtilTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/constant/ScriptNumberUtilTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.constant -import org.bitcoins.core.util.{ BitcoinSUtil, NumberUtil } +import org.bitcoins.core.util.BitcoinSUtil import org.scalatest.{ FlatSpec, MustMatchers } /** diff --git a/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsFactoryTest.scala index 2795860122..c6ee2e1d6d 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.control -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/8/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsInterpreterTest.scala index 6be769bcf7..2f923abb9a 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsInterpreterTest.scala @@ -1,15 +1,12 @@ package org.bitcoins.core.script.control -import org.bitcoins.core.serializers.script.ScriptParser import org.bitcoins.core.script.ScriptProgram import org.bitcoins.core.script.arithmetic.OP_ADD import org.bitcoins.core.script.bitwise.OP_EQUAL import org.bitcoins.core.script.constant._ -import org.bitcoins.core.script.crypto.OP_CHECKSIG -import org.bitcoins.core.script.locktime.OP_CHECKSEQUENCEVERIFY import org.bitcoins.core.script.reserved.{ OP_RESERVED, OP_VER } import org.bitcoins.core.script.result.{ ScriptErrorInvalidStackOperation, ScriptErrorOpReturn } -import org.bitcoins.core.script.stack.OP_DROP +import org.bitcoins.core.serializers.script.ScriptParser import org.bitcoins.core.util._ import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsTest.scala index 8557d388fd..11360d85cf 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/control/ControlOperationsTest.scala @@ -1,9 +1,6 @@ package org.bitcoins.core.script.control -import org.bitcoins.core.script.arithmetic.OP_ADD -import org.bitcoins.core.script.bitwise.OP_EQUAL -import org.bitcoins.core.script.constant.{ OP_2, OP_1, OP_0 } -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/6/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoInterpreterTest.scala index d6cc035d35..51fea66099 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoInterpreterTest.scala @@ -3,13 +3,13 @@ package org.bitcoins.core.script.crypto import org.bitcoins.core.crypto.BaseTxSigComponent import org.bitcoins.core.currency.CurrencyUnits import org.bitcoins.core.number.UInt32 -import org.bitcoins.core.protocol.script.{ P2SHScriptSignature, ScriptPubKey, ScriptSignature, SigVersionBase } +import org.bitcoins.core.protocol.script.ScriptSignature import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.script._ import org.bitcoins.core.script.constant._ import org.bitcoins.core.script.flag.{ ScriptFlagFactory, ScriptVerifyDerSig, ScriptVerifyNullDummy } import org.bitcoins.core.script.result._ -import org.bitcoins.core.util.{ BitcoinSLogger, ScriptProgramTestUtil, TestUtil, TransactionTestUtil } +import org.bitcoins.core.util.{ BitcoinSLogger, ScriptProgramTestUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } import scala.util.Try diff --git a/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoOperationsFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoOperationsFactoryTest.scala index 7176d36940..1e148bab0d 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoOperationsFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/crypto/CryptoOperationsFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.crypto -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/8/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/flag/ScriptFlagsTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/flag/ScriptFlagsTest.scala index 3d2ab2da8d..4529b71fea 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/flag/ScriptFlagsTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/flag/ScriptFlagsTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.flag -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 3/23/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/interpreter/ScriptInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/interpreter/ScriptInterpreterTest.scala index 2135c75788..28f7716a0c 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/interpreter/ScriptInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/interpreter/ScriptInterpreterTest.scala @@ -3,13 +3,14 @@ package org.bitcoins.core.script.interpreter import org.bitcoins.core.crypto.{ BaseTxSigComponent, WitnessTxSigComponentP2SH, WitnessTxSigComponentRaw } import org.bitcoins.core.currency.CurrencyUnits import org.bitcoins.core.protocol.script._ -import org.bitcoins.core.protocol.transaction.{ Transaction, TransactionOutput, WitnessTransaction } -import org.bitcoins.core.script.{ PreExecutionScriptProgram, ScriptProgram } +import org.bitcoins.core.protocol.transaction.{ TransactionOutput, WitnessTransaction } +import org.bitcoins.core.script.PreExecutionScriptProgram import org.bitcoins.core.script.flag.ScriptFlagFactory import org.bitcoins.core.script.interpreter.testprotocol.CoreTestCase import org.bitcoins.core.script.interpreter.testprotocol.CoreTestCaseProtocol._ import org.bitcoins.core.util._ import org.scalatest.{ FlatSpec, MustMatchers } +import org.slf4j.LoggerFactory import spray.json._ import scala.io.Source @@ -18,7 +19,7 @@ import scala.util.Try * Created by chris on 1/6/16. */ class ScriptInterpreterTest extends FlatSpec with MustMatchers { - private def logger = BitcoinSLogger.logger + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) "ScriptInterpreter" must "evaluate all the scripts from the bitcoin core script_tests.json" in { val source = Source.fromURL(getClass.getResource("/script_tests.json")) diff --git a/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/CoreTestCase.scala b/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/CoreTestCase.scala index dcf7107388..40dd03d9fc 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/CoreTestCase.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/CoreTestCase.scala @@ -2,7 +2,6 @@ package org.bitcoins.core.script.interpreter.testprotocol import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.protocol.script.{ ScriptPubKey, ScriptSignature, ScriptWitness } -import org.bitcoins.core.script.constant.ScriptToken import org.bitcoins.core.script.result.ScriptResult /** diff --git a/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/ScriptSignatureCoreTestCase.scala b/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/ScriptSignatureCoreTestCase.scala index 3e1e6de0ee..9d7e407a03 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/ScriptSignatureCoreTestCase.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/interpreter/testprotocol/ScriptSignatureCoreTestCase.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.interpreter.testprotocol -import org.bitcoins.core.protocol.script.{ ScriptSignature, ScriptPubKey } +import org.bitcoins.core.protocol.script.ScriptSignature import org.bitcoins.core.script.constant.ScriptToken /** diff --git a/core-test/src/test/scala/org/bitcoins/core/script/locktime/LocktimeOperationFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/locktime/LocktimeOperationFactoryTest.scala index 41626a8778..75c41bf461 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/locktime/LocktimeOperationFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/locktime/LocktimeOperationFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.locktime -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/8/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceInterpreterTest.scala index d98d146951..36574a39e7 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceInterpreterTest.scala @@ -1,9 +1,8 @@ package org.bitcoins.core.script.splice -import org.bitcoins.core.script.{ ExecutedScriptProgram, ScriptProgram } -import org.bitcoins.core.script.bitwise.OP_EQUAL import org.bitcoins.core.script.constant._ import org.bitcoins.core.script.result.ScriptErrorInvalidStackOperation +import org.bitcoins.core.script.{ ExecutedScriptProgram, ScriptProgram } import org.bitcoins.core.util.TestUtil import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceOperationFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceOperationFactoryTest.scala index 49c1e3c173..639557cd2e 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceOperationFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/splice/SpliceOperationFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.splice -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/22/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/stack/StackInterpreterTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/stack/StackInterpreterTest.scala index 66b2d88b91..dbc0378cde 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/stack/StackInterpreterTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/stack/StackInterpreterTest.scala @@ -1,11 +1,10 @@ package org.bitcoins.core.script.stack -import org.bitcoins.core.script.{ ExecutedScriptProgram, ScriptProgram } -import org.bitcoins.core.script.bitwise.OP_EQUAL import org.bitcoins.core.script.constant._ +import org.bitcoins.core.script.result._ +import org.bitcoins.core.script.{ ExecutedScriptProgram, ScriptProgram } import org.bitcoins.core.util.{ BitcoinSUtil, ScriptProgramTestUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } -import org.bitcoins.core.script.result._ /** * Created by chris on 1/6/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationFactoryTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationFactoryTest.scala index 96851df483..21bb8cd8cb 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationFactoryTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationFactoryTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.stack -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/8/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationsTest.scala b/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationsTest.scala index 1e5db4893f..97ccd7f3b5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationsTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/script/stack/StackOperationsTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.script.stack -import org.scalatest.{ MustMatchers, FlatSpec } +import org.scalatest.{ FlatSpec, MustMatchers } /** * Created by chris on 1/6/16. diff --git a/core-test/src/test/scala/org/bitcoins/core/serializers/RawBitcoinSerializerHelperTest.scala b/core-test/src/test/scala/org/bitcoins/core/serializers/RawBitcoinSerializerHelperTest.scala index 5e325544ef..a898cbf01e 100644 --- a/core-test/src/test/scala/org/bitcoins/core/serializers/RawBitcoinSerializerHelperTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/serializers/RawBitcoinSerializerHelperTest.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.serializers import org.bitcoins.core.number.UInt64 -import org.bitcoins.core.protocol.{ CompactSizeUInt, NetworkElement } +import org.bitcoins.core.protocol.CompactSizeUInt import org.bitcoins.core.protocol.transaction.{ EmptyTransactionOutput, TransactionInput, TransactionOutput } import org.bitcoins.core.serializers.transaction.{ RawTransactionInputParser, RawTransactionOutputParser } import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/serializers/RawSerializerHelperSpec.scala b/core-test/src/test/scala/org/bitcoins/core/serializers/RawSerializerHelperSpec.scala index c9ad9a394a..beecee8037 100644 --- a/core-test/src/test/scala/org/bitcoins/core/serializers/RawSerializerHelperSpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/serializers/RawSerializerHelperSpec.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.serializers import org.bitcoins.core.gen.TransactionGenerators -import org.bitcoins.core.protocol.transaction.{ Transaction, TransactionInput, TransactionOutput } +import org.bitcoins.core.protocol.transaction.TransactionOutput import org.scalacheck.{ Prop, Properties } import scodec.bits.ByteVector diff --git a/core-test/src/test/scala/org/bitcoins/core/serializers/script/RawScriptSignatureParserTest.scala b/core-test/src/test/scala/org/bitcoins/core/serializers/script/RawScriptSignatureParserTest.scala index fd34b292fa..d025c4ede5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/serializers/script/RawScriptSignatureParserTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/serializers/script/RawScriptSignatureParserTest.scala @@ -2,8 +2,7 @@ package org.bitcoins.core.serializers.script import org.bitcoins.core.protocol.script.ScriptSignature import org.bitcoins.core.script.constant._ -import org.bitcoins.core.script.crypto.OP_CHECKMULTISIG -import org.bitcoins.core.util.{ BitcoinSLogger, BitcoinSUtil, TestUtil } +import org.bitcoins.core.util.{ BitcoinSUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } import scodec.bits.ByteVector diff --git a/core-test/src/test/scala/org/bitcoins/core/serializers/transaction/RawTransactionInputParserTest.scala b/core-test/src/test/scala/org/bitcoins/core/serializers/transaction/RawTransactionInputParserTest.scala index f87c65d787..98ef008db1 100644 --- a/core-test/src/test/scala/org/bitcoins/core/serializers/transaction/RawTransactionInputParserTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/serializers/transaction/RawTransactionInputParserTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.serializers.transaction -import org.bitcoins.core.number.{ UInt32, UInt64 } +import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.transaction.{ TransactionConstants, TransactionInput } import org.bitcoins.core.util.{ BitcoinSLogger, BitcoinSUtil, TestUtil } import org.scalatest.{ FlatSpec, MustMatchers } diff --git a/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala b/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala index 1035e50d2c..08694e9f40 100644 --- a/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala @@ -1,12 +1,15 @@ package org.bitcoins.core.util +import org.bitcoins.core.gen.CryptoGenerators +import org.scalatest.prop.PropertyChecks import org.scalatest.{ FlatSpec, MustMatchers } +import org.slf4j.LoggerFactory /** * Created by chris on 1/26/16. */ class CryptoUtilTest extends FlatSpec with MustMatchers { - + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) "CryptoUtil" must "perform a SHA-1 hash" in { val hash = CryptoUtil.sha1("") val expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709" @@ -55,4 +58,28 @@ class CryptoUtilTest extends FlatSpec with MustMatchers { CryptoUtil.sha256Hash160(hex).hex must be(expected) CryptoUtil.sha256Hash160(hex).flip.flip.hex must be(expected) } -} + + it must "recover the compressed and uncompressed public key from a message" in { + PropertyChecks.forAll(CryptoGenerators.privateKey, CryptoGenerators.sha256Digest) { + case (privKey, hash) => + val pubKey = privKey.publicKey + val uncompressed = pubKey.decompressed + val message = hash.bytes + val sig = privKey.sign(message) + val (recovPub1, recovPub2) = CryptoUtil.recoverPublicKey(sig, message) + assert(recovPub1 == pubKey || recovPub2 == pubKey) + } + } + + it must "be able to recover and verify a siganture for a message" in { + PropertyChecks.forAll(CryptoGenerators.privateKey, CryptoGenerators.sha256Digest) { + case (privKey, hash) => + val message = hash.bytes + val sig = privKey.sign(message) + val (recovPub1, recovPub2) = CryptoUtil.recoverPublicKey(sig, message) + assert(recovPub1.verify(message, sig) && recovPub2.verify(message, sig)) + } + } + + +} \ No newline at end of file diff --git a/core-test/src/test/scala/org/bitcoins/core/util/NumberUtilSpec.scala b/core-test/src/test/scala/org/bitcoins/core/util/NumberUtilSpec.scala index 2a252332c2..ae02492fd0 100644 --- a/core-test/src/test/scala/org/bitcoins/core/util/NumberUtilSpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/util/NumberUtilSpec.scala @@ -1,8 +1,8 @@ package org.bitcoins.core.util import org.bitcoins.core.gen.NumberGenerator -import org.bitcoins.core.number.{ UInt32, UInt8 } -import org.scalacheck.{ Gen, Prop, Properties } +import org.bitcoins.core.number.UInt8 +import org.scalacheck.{ Prop, Properties } /** * Created by chris on 6/20/16. @@ -25,20 +25,12 @@ class NumberUtilSpec extends Properties("NumberUtilSpec") { NumberUtil.toLong(BitcoinSUtil.encodeHex(long)) == long } - property("converBits symmetry") = { - Prop.forAllNoShrink(Gen.choose(1, 8), NumberGenerator.uInt8s) { - case (to, u8s: Seq[UInt8]) => - //TODO: in the future make this a generated value instead of fixed to 8 - //but the trick is we need to make sure that the u8s generated are valid numbers in the 'from' base - val u32From = UInt32(8.toShort) - val u32To = UInt32(to.toShort) - val converted = NumberUtil.convertUInt8s(u8s, u32From, u32To, true) - val original = converted.flatMap(c => NumberUtil.convertUInt8s(c, u32To, u32From, false)) - if (original.isFailure) { - throw original.failed.get - } else { - original.get == u8s - } + property("convertBits symmetry") = { + Prop.forAllNoShrink(NumberGenerator.uInt8s) { + case (u8s: Seq[UInt8]) => + val u5s = NumberUtil.convertUInt8sToUInt5s(u8s.toVector) + val original: Vector[UInt8] = NumberUtil.convertUInt5sToUInt8(u5s = u5s) + original == u8s } } } diff --git a/core-test/src/test/scala/org/bitcoins/core/util/testprotocol/ConfigParams.scala b/core-test/src/test/scala/org/bitcoins/core/util/testprotocol/ConfigParams.scala index 347df6ea98..072aec5a9a 100644 --- a/core-test/src/test/scala/org/bitcoins/core/util/testprotocol/ConfigParams.scala +++ b/core-test/src/test/scala/org/bitcoins/core/util/testprotocol/ConfigParams.scala @@ -1,6 +1,5 @@ package org.bitcoins.core.util.testprotocol -import org.bitcoins.core.util.BitcoinSLogger import spray.json._ /** diff --git a/core/src/main/scala/org/bitcoins/core/crypto/DERSignatureUtil.scala b/core/src/main/scala/org/bitcoins/core/crypto/DERSignatureUtil.scala index b70250cb0e..8010e4ecee 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/DERSignatureUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/DERSignatureUtil.scala @@ -1,6 +1,5 @@ package org.bitcoins.core.crypto -import org.bitcoins.core.util.{ BitcoinSLogger, BitcoinSUtil } import org.bouncycastle.asn1.{ ASN1InputStream, ASN1Integer, DLSequence } import scodec.bits.ByteVector @@ -11,7 +10,6 @@ import scala.util.{ Failure, Success, Try } */ sealed abstract class DERSignatureUtil { - private val logger = BitcoinSLogger.logger /** * Checks if this signature is encoded to DER correctly * https://crypto.stackexchange.com/questions/1795/how-can-i-convert-a-der-ecdsa-signature-to-asn-1 @@ -63,7 +61,6 @@ sealed abstract class DERSignatureUtil { * @return */ def decodeSignature(bytes: ByteVector): (BigInt, BigInt) = { - logger.debug("Signature to decode: " + BitcoinSUtil.encodeHex(bytes)) val asn1InputStream = new ASN1InputStream(bytes.toArray) //TODO: this is nasty, is there any way to get rid of all this casting??? //TODO: Not 100% this is completely right for signatures that are incorrectly DER encoded @@ -84,7 +81,6 @@ sealed abstract class DERSignatureUtil { } case Failure(_) => default } - logger.debug("r: " + r) val s: ASN1Integer = Try(seq.getObjectAt(1).asInstanceOf[ASN1Integer]) match { case Success(s) => //this is needed for a bug inside of bouncy castle where zero length values throw an exception @@ -95,7 +91,6 @@ sealed abstract class DERSignatureUtil { } case Failure(_) => default } - logger.debug("s: " + s) asn1InputStream.close() (r.getPositiveValue, s.getPositiveValue) } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/ECDigitalSignature.scala b/core/src/main/scala/org/bitcoins/core/crypto/ECDigitalSignature.scala index c8f2db7caf..61dcc823a7 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ECDigitalSignature.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ECDigitalSignature.scala @@ -36,10 +36,28 @@ sealed abstract class ECDigitalSignature { def decodeSignature: (BigInt, BigInt) = DERSignatureUtil.decodeSignature(this) /** Represents the r value found in a elliptic curve digital signature */ - def r = decodeSignature._1 + def r: BigInt = decodeSignature._1 /** Represents the s value found in a elliptic curve digital signature */ - def s = decodeSignature._2 + def s: BigInt = decodeSignature._2 + + /** + * Creates a ByteVector with only + * the 32byte r value and 32 byte s value + * in the vector + */ + def toRawRS: ByteVector = { + + val rBytes = r.toByteArray.takeRight(32) + val sBytes = s.toByteArray.takeRight(32) + + val rPadded = ByteVector(rBytes).padLeft(32) + val sPadded = ByteVector(sBytes).padLeft(32) + + require(rPadded.size == 32, s"rPadded.size ${rPadded.size}") + require(sPadded.size == 32, s"sPadded.size ${sPadded.size}") + rPadded ++ sPadded + } } @@ -98,4 +116,28 @@ object ECDigitalSignature extends Factory[ECDigitalSignature] { fromBytes(bytes) } + + /** + * Reads a 64 byte bytevector and assumes + * the first 32 bytes in the R value, + * the second 32 is the value + */ + def fromRS(byteVector: ByteVector): ECDigitalSignature = { + require( + byteVector.length == 64, + s"Incorrect size for reading a ECDigital signature from a bytevec, got ${byteVector.length}") + val r = BigInt(byteVector.take(32).toArray) + val s = BigInt(byteVector.takeRight(32).toArray) + fromRS(r, s) + } + + /** + * Reads a 64 byte bytevector and assumes + * the first 32 bytes in the R value, + * the second 32 is the value + */ + def fromRS(hex: String): ECDigitalSignature = { + val bytes = BitcoinSUtil.decodeHex(hex) + fromRS(bytes) + } } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala b/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala index 6475e6e10a..20d67890ec 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala @@ -12,6 +12,7 @@ import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.generators.ECKeyPairGenerator import org.bouncycastle.crypto.params.{ ECKeyGenerationParameters, ECPrivateKeyParameters, ECPublicKeyParameters } import org.bouncycastle.crypto.signers.{ ECDSASigner, HMacDSAKCalculator } +import org.bouncycastle.math.ec.ECPoint import scodec.bits.ByteVector import scala.annotation.tailrec @@ -263,7 +264,6 @@ sealed abstract class ECPublicKey extends BaseECKey { signature match { case EmptyDigitalSignature => signer.verifySignature(data.toArray, java.math.BigInteger.valueOf(0), java.math.BigInteger.valueOf(0)) case _: ECDigitalSignature => - logger.debug("Public key bytes: " + BitcoinSUtil.encodeHex(bytes)) val rBigInteger: BigInteger = new BigInteger(signature.r.toString()) val sBigInteger: BigInteger = new BigInteger(signature.s.toString()) signer.verifySignature(data.toArray, rBigInteger, sBigInteger) @@ -286,6 +286,15 @@ sealed abstract class ECPublicKey extends BaseECKey { ECPublicKey.fromBytes(ByteVector(decompressed)) } else this } + + /** Decodes a [[org.bitcoins.core.crypto.ECPublicKey]] in bitcoin-s + * to a [[ECPoint]] data structure that is internal to the + * bouncy castle library + * @return + */ + def toPoint: ECPoint = { + CryptoParams.curve.getCurve.decodePoint(bytes.toArray) + } } object ECPublicKey extends Factory[ECPublicKey] { @@ -320,4 +329,16 @@ object ECPublicKey extends Factory[ECPublicKey] { * [[https://github.com/bitcoin/bitcoin/blob/27765b6403cece54320374b37afb01a0cfe571c3/src/pubkey.h#L158]] */ def isValid(bytes: ByteVector): Boolean = bytes.nonEmpty + + + /** Creates a [[org.bitcoins.core.crypto.ECPublicKey]] from the [[org.bouncycastle.math.ec.ECPoint]] + * data structure used internally inside of bouncy castle + * @param p + * @param isCompressed + * @return + */ + def fromPoint(p: ECPoint, isCompressed: Boolean = true): ECPublicKey = { + val bytes = p.getEncoded(isCompressed) + ECPublicKey.fromBytes(ByteVector(bytes)) + } } diff --git a/core/src/main/scala/org/bitcoins/core/number/NumberType.scala b/core/src/main/scala/org/bitcoins/core/number/NumberType.scala index da8b2854ae..1cb6fa2547 100644 --- a/core/src/main/scala/org/bitcoins/core/number/NumberType.scala +++ b/core/src/main/scala/org/bitcoins/core/number/NumberType.scala @@ -92,12 +92,34 @@ sealed abstract class SignedNumber[T <: Number[T]] extends Number[T] */ sealed abstract class UnsignedNumber[T <: Number[T]] extends Number[T] + +/** This number type is useful for dealing with [[org.bitcoins.core.util.Bech32]] + * related applications. The native encoding for Bech32 is a 5 bit number which + * is what this abstraction is meant to be used for + */ +sealed abstract class UInt5 extends UnsignedNumber[UInt5] { + override def apply: A => UInt5 = UInt5(_) + + override def andMask: BigInt = 0x1f + + def byte: Byte = toInt.toByte + + def toUInt8: UInt8 = UInt8(toInt) + + override def hex: String = toUInt8.hex +} + sealed abstract class UInt8 extends UnsignedNumber[UInt8] { override def apply: A => UInt8 = UInt8(_) override def hex = BitcoinSUtil.encodeHex(toInt.toShort).slice(2, 4) override def andMask = 0xff + + def toUInt5: UInt5 = { + //this will throw if not in range of a UInt5, come back and look later + UInt5(toInt) + } } /** @@ -166,6 +188,47 @@ trait BaseNumbers[T] { def max: T } +object UInt5 extends Factory[UInt5] with BaseNumbers[UInt5] { + private case class UInt5Impl(underlying: BigInt) extends UInt5 { + require(underlying.toInt >= 0, s"Cannot create UInt5 from $underlying") + require(underlying <= 31, s"Cannot create UInt5 from $underlying") + } + + lazy val zero = UInt5(0.toByte) + lazy val one = UInt5(1.toByte) + + lazy val min = zero + lazy val max = UInt5(31.toByte) + + def apply(byte: Byte): UInt5 = fromByte(byte) + + def apply(bigInt: BigInt): UInt5 = { + + require( + bigInt.toByteArray.size == 1, + s"To create a uint5 from a BigInt it must be less than 32. Got: ${bigInt.toString}") + + UInt5.fromByte(bigInt.toByteArray.head) + } + + override def fromBytes(bytes: ByteVector): UInt5 = { + require(bytes.size == 1, s"To create a uint5 from a ByteVector it must be of size one ${bytes.length}") + UInt5.fromByte(bytes.head) + } + + def fromByte(byte: Byte): UInt5 = { + UInt5Impl(BigInt(byte)) + } + + def toUInt5(b: Byte): UInt5 = { + fromByte(b) + } + + def toUInt5s(bytes: ByteVector): Vector[UInt5] = { + bytes.toArray.map(toUInt5(_)).toVector + } +} + object UInt8 extends Factory[UInt8] with BaseNumbers[UInt8] { private case class UInt8Impl(underlying: BigInt) extends UInt8 { require(isValid(underlying), "Invalid range for a UInt8, got: " + underlying) @@ -200,8 +263,8 @@ object UInt8 extends Factory[UInt8] with BaseNumbers[UInt8] { ByteVector(us.map(toByte(_))) } - def toUInt8s(bytes: ByteVector): Seq[UInt8] = { - bytes.toSeq.map { b: Byte => toUInt8(b) } + def toUInt8s(bytes: ByteVector): Vector[UInt8] = { + bytes.toArray.map(toUInt8).toVector } def checkBounds(res: BigInt): UInt8 = { diff --git a/core/src/main/scala/org/bitcoins/core/protocol/Address.scala b/core/src/main/scala/org/bitcoins/core/protocol/Address.scala index 03e7f95a2f..6a65638cf0 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/Address.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/Address.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.protocol import org.bitcoins.core.config.{ MainNet, RegTest, TestNet3, _ } -import org.bitcoins.core.crypto.{ ECPublicKey, HashDigest, Sha256Digest, Sha256Hash160Digest } -import org.bitcoins.core.number.{ UInt32, UInt8 } +import org.bitcoins.core.crypto._ +import org.bitcoins.core.number.{ UInt5, UInt8 } import org.bitcoins.core.protocol.script._ import org.bitcoins.core.script.constant.ScriptConstant import org.bitcoins.core.util._ @@ -65,24 +65,33 @@ sealed abstract class Bech32Address extends BitcoinAddress { def hrp: HumanReadablePart - def data: Seq[UInt8] + def data: Vector[UInt5] override def networkParameters = hrp.network.get override def value: String = { - val checksum = Bech32Address.createChecksum(hrp, data) - val all = data ++ checksum - val encoding = Bech32Address.encodeToString(all) - hrp.toString + Bech32Address.separator + encoding + val all: Vector[UInt5] = data ++ checksum + val encoding = Bech32.encode5bitToString(all) + + hrp.toString + Bech32.separator + encoding } + def checksum: Vector[UInt5] = Bech32Address.createChecksum(hrp, data) + override def scriptPubKey: WitnessScriptPubKey = { Bech32Address.fromStringToWitSPK(value).get } - override def hash: Sha256Digest = { + override def hash: HashDigest = { val byteVector = BitcoinSUtil.toByteVector(scriptPubKey.witnessProgram) - Sha256Digest(byteVector) + scriptPubKey match { + case _: P2WPKHWitnessSPKV0 => + Sha256Hash160Digest(byteVector) + case _: P2WSHWitnessSPKV0 => + Sha256Digest(byteVector) + case _: UnassignedWitnessScriptPubKey => + throw new IllegalArgumentException(s"Cannot parse the hash of an unassigned witness scriptpubkey for bech32 address") + } } override def toString = "Bech32Address(" + value + ")" @@ -90,130 +99,71 @@ sealed abstract class Bech32Address extends BitcoinAddress { } object Bech32Address extends AddressFactory[Bech32Address] { - private case class Bech32AddressImpl(hrp: HumanReadablePart, data: Seq[UInt8]) extends Bech32Address { - verifyChecksum(hrp, UInt8.toBytes(data)) + private case class Bech32AddressImpl(hrp: HumanReadablePart, data: Vector[UInt5]) extends Bech32Address { + //require(verifyChecksum(hrp, data), "checksum did not pass") } - /** Separator used to separate the hrp & data parts of a bech32 addr */ - val separator = '1' - def apply( witSPK: WitnessScriptPubKey, - networkParameters: NetworkParameters): Try[Bech32Address] = { + networkParameters: NetworkParameters): Bech32Address = { //we don't encode the wit version or pushop for program into base5 val prog = UInt8.toUInt8s(witSPK.asmBytes.tail.tail) - val encoded = Bech32Address.encode(prog) + val encoded = Bech32.from8bitTo5bit(prog) val hrp = networkParameters match { case _: MainNet => bc case _: TestNet3 | _: RegTest => tb } - val witVersion = witSPK.witnessVersion.version.toLong.toShort - encoded.map(e => Bech32Address(hrp, Seq(UInt8(witVersion)) ++ e)) + val witVersion = witSPK.witnessVersion.version.toInt.toByte + Bech32Address(hrp, Vector(UInt5(witVersion)) ++ encoded) } - def apply(hrp: HumanReadablePart, data: Seq[UInt8]): Bech32Address = { + def apply(hrp: HumanReadablePart, data: Vector[UInt5]): Bech32Address = { Bech32AddressImpl(hrp, data) } /** Returns a base 5 checksum as specified by BIP173 */ - def createChecksum(hrp: HumanReadablePart, bytes: Seq[UInt8]): Seq[UInt8] = { - val values: Seq[UInt8] = hrpExpand(hrp) ++ bytes - val z = UInt8.zero - val polymod: Long = polyMod(values ++ Seq(z, z, z, z, z, z)) ^ 1 - //[(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - val result: Seq[UInt8] = 0.until(6).map { i => - //((polymod >> five * (five - u)) & UInt8(31.toShort)) - UInt8(((polymod >> 5 * (5 - i)) & 31).toShort) - } - result + def createChecksum(hrp: HumanReadablePart, bytes: Vector[UInt5]): Vector[UInt5] = { + val values = hrpExpand(hrp) ++ bytes + Bech32.createChecksum(values) } - def hrpExpand(hrp: HumanReadablePart): Seq[UInt8] = { - val x: ByteVector = hrp.bytes.map { b: Byte => - (b >> 5).toByte - } - val withZero: ByteVector = x ++ ByteVector.low(1) - - val y: ByteVector = hrp.bytes.map { char => - (char & 0x1f).toByte - } - val result = UInt8.toUInt8s(withZero ++ y) - result + def hrpExpand(hrp: HumanReadablePart): Vector[UInt5] = { + Bech32.hrpExpand(hrp.bytes) } - private def generators: Seq[Long] = Seq( - UInt32("3b6a57b2").toLong, - UInt32("26508e6d").toLong, UInt32("1ea119fa").toLong, - UInt32("3d4233dd").toLong, UInt32("2a1462b3").toLong) - - def polyMod(bytes: Seq[UInt8]): Long = { - var chk: Long = 1 - bytes.map { v => - val b = chk >> 25 - //chk = (chk & 0x1ffffff) << 5 ^ v - chk = (chk & 0x1ffffff) << 5 ^ v.toLong - 0.until(5).map { i: Int => - //chk ^= GEN[i] if ((b >> i) & 1) else 0 - if (((b >> i) & 1) == 1) { - chk = chk ^ generators(i) - } - } - } - chk - } - - def verifyChecksum(hrp: HumanReadablePart, data: ByteVector): Boolean = { - val u8s = UInt8.toUInt8s(data) - verifyCheckSum(hrp, u8s) - } - - def verifyCheckSum(hrp: HumanReadablePart, u8s: Seq[UInt8]): Boolean = { - polyMod(hrpExpand(hrp) ++ u8s) == 1 - } - - private val u32Five = UInt32(5) - private val u32Eight = UInt32(8) - - /** Converts a byte array from base 8 to base 5 */ - def encode(bytes: Seq[UInt8]): Try[Seq[UInt8]] = { - NumberUtil.convertUInt8s(bytes, u32Eight, u32Five, true) - } - /** Decodes a byte array from base 5 to base 8 */ - def decodeToBase8(b: Seq[UInt8]): Try[Seq[UInt8]] = { - NumberUtil.convertUInt8s(b, u32Five, u32Eight, false) + def verifyChecksum(hrp: HumanReadablePart, u5s: Seq[UInt5]): Boolean = { + val data = hrpExpand(hrp) ++ u5s + val checksum = Bech32.polyMod(data) + checksum == 1 } /** Tries to convert the given string a to a [[org.bitcoins.core.protocol.script.WitnessScriptPubKey]] */ def fromStringToWitSPK(string: String): Try[WitnessScriptPubKey] = { val decoded = fromString(string) decoded.flatMap { - case bec32Addr => - val bytes = UInt8.toBytes(bec32Addr.data) - val (v, prog) = (bytes.head, bytes.tail) - val convertedProg = NumberUtil.convertBytes(prog, u32Five, u32Eight, false) - val progBytes = convertedProg.map(UInt8.toBytes(_)) - val witVersion = WitnessVersion(v) - progBytes.flatMap { prog => - val pushOp = BitcoinScriptUtil.calculatePushOp(prog) - witVersion match { - case Some(v) => - WitnessScriptPubKey(Seq(v.version) ++ pushOp ++ Seq(ScriptConstant(prog))) match { - case Some(spk) => Success(spk) - case None => Failure(new IllegalArgumentException("Failed to decode bech32 into a witSPK")) - } - case None => Failure(new IllegalArgumentException("Witness version was not valid, got: " + v)) - } - + case bech32Addr => + val bytes = bech32Addr.data + val (v, _) = (bytes.head, bytes.tail) + val convertedProg = NumberUtil.convertUInt5sToUInt8(bytes.tail) + val progBytes = UInt8.toBytes(convertedProg) + val witVersion = WitnessVersion(v.toInt) + val pushOp = BitcoinScriptUtil.calculatePushOp(progBytes) + witVersion match { + case Some(v) => + val witSPK = WitnessScriptPubKey(List(v.version) ++ pushOp ++ List(ScriptConstant(progBytes))) + witSPK match { + case Some(spk) => Success(spk) + case None => Failure(new IllegalArgumentException("Failed to decode bech32 into a witSPK")) + } + case None => Failure(new IllegalArgumentException("Witness version was not valid, got: " + v)) } + } } - /** Takes a base32 byte array and encodes it to a string */ - def encodeToString(b: Seq[UInt8]): String = { - b.map(b => charset(b.toInt)).mkString - } + /** Decodes bech32 string to the [[HumanReadablePart]] & data part */ override def fromString(str: String): Try[Bech32Address] = { - val sepIndexes = str.zipWithIndex.filter(_._1 == separator) + val sepIndexes = str.zipWithIndex.filter(_._1 == Bech32.separator) if (str.size > 90 || str.size < 8) { Failure(new IllegalArgumentException("bech32 payloads must be betwee 8 and 90 chars, got: " + str.size)) } else if (sepIndexes.isEmpty) { @@ -225,18 +175,18 @@ object Bech32Address extends AddressFactory[Bech32Address] { Failure(new IllegalArgumentException("Hrp/data too short")) } else { val hrpValid = checkHrpValidity(hrp) - val dataValid = checkDataValidity(data) - val isChecksumValid: Try[ByteVector] = hrpValid.flatMap { h => - dataValid.flatMap { d => + val dataValid = Bech32.checkDataValidity(data) + val isChecksumValid: Try[Vector[UInt5]] = hrpValid.flatMap { h: HumanReadablePart => + dataValid.flatMap { d: Vector[UInt5] => if (verifyChecksum(h, d)) { - if (d.size < 6) Success(ByteVector.empty) + if (d.size < 6) Success(Vector.empty) else Success(d.take(d.size - 6)) } else Failure(new IllegalArgumentException("Checksum was invalid on the bech32 address")) } } - isChecksumValid.flatMap { d: ByteVector => - val u8s = UInt8.toUInt8s(d) - hrpValid.map(h => Bech32Address(h, u8s)) + + isChecksumValid.flatMap { d: Vector[UInt5] => + hrpValid.map(h => Bech32Address(h, d)) } } } @@ -277,37 +227,6 @@ object Bech32Address extends AddressFactory[Bech32Address] { } } - /** - * Takes in the data portion of a bech32 address and decodes it to a byte array - * It also checks the validity of the data portion according to BIP173 - */ - def checkDataValidity(data: String): Try[ByteVector] = { - @tailrec - def loop(remaining: List[Char], accum: ByteVector, hasUpper: Boolean, hasLower: Boolean): Try[ByteVector] = remaining match { - case Nil => Success(accum.reverse) - case h :: t => - if (!charset.contains(h.toLower)) { - Failure(new IllegalArgumentException("Invalid character in data of bech32 address, got: " + h)) - } else { - if ((h.isUpper && hasLower) || (h.isLower && hasUpper)) { - Failure(new IllegalArgumentException("Cannot have mixed case for bech32 address")) - } else { - val byte = charset.indexOf(h.toLower).toByte - require(byte >= 0 && byte < 32, "Not in valid range, got: " + byte) - loop(t, byte +: accum, h.isUpper || hasUpper, h.isLower || hasLower) - } - } - } - val payload: Try[ByteVector] = loop(data.toCharArray.toList, ByteVector.empty, - false, false) - payload - } - - /** https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 */ - def charset: Seq[Char] = Seq('q', 'p', 'z', 'r', 'y', '9', 'x', '8', - 'g', 'f', '2', 't', 'v', 'd', 'w', '0', - 's', '3', 'j', 'n', '5', '4', 'k', 'h', - 'c', 'e', '6', 'm', 'u', 'a', '7', 'l') } object P2PKHAddress extends AddressFactory[P2PKHAddress] { @@ -435,7 +354,7 @@ object BitcoinAddress extends AddressFactory[BitcoinAddress] { override def fromScriptPubKey(spk: ScriptPubKey, np: NetworkParameters): Try[BitcoinAddress] = spk match { case p2pkh: P2PKHScriptPubKey => Success(P2PKHAddress(p2pkh, np)) case p2sh: P2SHScriptPubKey => Success(P2SHAddress(p2sh, np)) - case witSPK: WitnessScriptPubKey => Bech32Address(witSPK, np) + case witSPK: WitnessScriptPubKey => Success(Bech32Address(witSPK, np)) case x @ (_: P2PKScriptPubKey | _: MultiSignatureScriptPubKey | _: LockTimeScriptPubKey | _: EscrowTimeoutScriptPubKey | _: NonStandardScriptPubKey | _: WitnessCommitment | _: UnassignedWitnessScriptPubKey | EmptyScriptPubKey) => diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala new file mode 100644 index 0000000000..8969fc900d --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala @@ -0,0 +1,134 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.config.NetworkParameters +import org.bitcoins.core.protocol.ln.LnParams._ +import org.bitcoins.core.protocol.ln.currency.{ LnCurrencyUnit, LnCurrencyUnits } +import org.bitcoins.core.util.Bech32 +import scodec.bits.ByteVector + +import scala.util.matching.Regex +import scala.util.{ Failure, Success, Try } + +sealed abstract class LnHumanReadablePart { + require( + amount.isEmpty || amount.get.toBigInt > 0, + s"Invoice amount must be greater then 0, got $amount") + require( + amount.isEmpty || amount.get.toMSat <= LnPolicy.maxAmountMSat, + s"Invoice amount must not exceed ${LnPolicy.maxAmountMSat}, got ${amount.get.toMSat}") + + def network: LnParams + + def amount: Option[LnCurrencyUnit] + + def bytes: ByteVector = { + network.invoicePrefix ++ amount + .map(_.encodedBytes) + .getOrElse(ByteVector.empty) + } + + override def toString: String = { + val b = StringBuilder.newBuilder + val prefixChars = network.invoicePrefix.toArray.map(_.toChar) + prefixChars.foreach(b.append) + + val amt = amount.map(_.toEncodedString).getOrElse("") + b.append(amt) + + b.toString() + } +} + +object LnHumanReadablePart { + + /** Prefix for generating a LN invoice on the Bitcoin MainNet */ + case class lnbc(override val amount: Option[LnCurrencyUnit]) extends LnHumanReadablePart { + override def network: LnParams = LnBitcoinMainNet + } + + /** Prefix for generating a LN invoice on the Bitcoin TestNet3 */ + case class lntb(override val amount: Option[LnCurrencyUnit]) extends LnHumanReadablePart { + override def network: LnParams = LnBitcoinTestNet + } + + /** Prefix for genearting a LN invoice on the Bitcoin RegTest */ + case class lnbcrt(override val amount: Option[LnCurrencyUnit]) extends LnHumanReadablePart { + def network: LnParams = LnBitcoinRegTest + } + + def apply(network: NetworkParameters): LnHumanReadablePart = { + val lnNetwork= LnParams.fromNetworkParameters(network) + LnHumanReadablePart.fromLnParams(lnNetwork) + } + + def apply(network: NetworkParameters, amount: LnCurrencyUnit): LnHumanReadablePart = { + val lnNetwork = LnParams.fromNetworkParameters(network) + LnHumanReadablePart(lnNetwork, Some(amount)) + } + + def apply(network: LnParams): LnHumanReadablePart = { + fromLnParams(network) + } + + /** + * Will return a [[org.bitcoins.core.protocol.ln.LnHumanReadablePart LnHumanReadablePart]] + * without a [[org.bitcoins.core.protocol.ln.currency.LnCurrencyUnit LnCurrencyUnit]] encoded in the invoice + */ + def fromLnParams(network: LnParams): LnHumanReadablePart = { + LnHumanReadablePart(network, None) + } + + /** + * Will return a [[org.bitcoins.core.protocol.ln.LnHumanReadablePart LnHumanReadablePart]] + * with the provide [[org.bitcoins.core.protocol.ln.currency.LnCurrencyUnit LnCurrencyUnit]] encoded in the invoice + */ + def apply(network: LnParams, amount: Option[LnCurrencyUnit]): LnHumanReadablePart = { + fromParamsAmount(network, amount) + } + + def fromParamsAmount(network: LnParams, amount: Option[LnCurrencyUnit]): LnHumanReadablePart = { + network match { + case LnParams.LnBitcoinMainNet => lnbc(amount) + case LnParams.LnBitcoinTestNet => lntb(amount) + case LnParams.LnBitcoinRegTest => lnbcrt(amount) + } + } + + /** + * First two chars MUST be 'ln' + * Next chars must be the BIP173 currency prefixes. For more information, see + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#human-readable-part BOLT11]] + * and + * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Specification BIP173]] + */ + def fromString(input: String): Try[LnHumanReadablePart] = { + val hrpIsValidT = Bech32.checkHrpValidity(input, parse) + hrpIsValidT + } + + private def parse(input: String): Try[LnHumanReadablePart] = { + //Select all of the letters, until we hit a number, as the network + val networkPattern: Regex = "^[a-z]*".r + val networkStringOpt = networkPattern.findFirstIn(input) + val lnParamsOpt = networkStringOpt.flatMap(LnParams.fromPrefixString(_)) + + if (lnParamsOpt.isEmpty) { + Failure(new IllegalArgumentException(s"Could not parse a valid network prefix, got ${input}")) + } else { + + val lnParams = lnParamsOpt.get + val prefixSize = lnParams.invoicePrefix.size.toInt + val amountString = input.slice(prefixSize, input.size) + val amount = LnCurrencyUnits.fromEncodedString(amountString).toOption + + //If we are able to parse something as an amount, but are unable to convert it to a LnCurrencyUnit, we should fail. + if (amount.isEmpty && !amountString.isEmpty) { + Failure(new IllegalArgumentException(s"Parsed an amount, " + + s"but could not convert to a valid currency, got: $amountString")) + } else { + Success(LnHumanReadablePart(lnParams, amount)) + } + } + } + +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala new file mode 100644 index 0000000000..3ff1a1739e --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala @@ -0,0 +1,329 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.crypto.{ECPrivateKey, Sha256Digest} +import org.bitcoins.core.number.{UInt5, UInt64, UInt8} +import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, PicoBitcoins} +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.core.protocol.ln.util.LnUtil +import org.bitcoins.core.util._ +import scodec.bits.ByteVector + +import scala.util.{Failure, Success, Try} + +sealed abstract class LnInvoice { + + require( + timestamp < LnInvoice.MAX_TIMESTAMP_U64, + s"timestamp ${timestamp.toBigInt} < ${LnInvoice.MAX_TIMESTAMP}") + + require( + isValidSignature(), + s"Did not receive a valid digital signature for the invoice ${toString}") + + private val bech32Separator: Char = Bech32.separator + + def hrp: LnHumanReadablePart + + def timestamp: UInt64 + + def lnTags: LnTaggedFields + + def signature: LnInvoiceSignature + + private def data: Vector[UInt5] = { + val ts = LnInvoice.uInt64ToBase32(timestamp) + val u5s: Vector[UInt5] = ts ++ lnTags.data ++ signature.data + u5s + } + + def network: LnParams = hrp.network + + def amount: Option[LnCurrencyUnit] = hrp.amount + + def amountPicoBitcoins: Option[PicoBitcoins] = { + amount.map(_.toPicoBitcoins) + } + + /** + * The [[NodeId]] that we are paying this invoice too + * We can either recover this with public key recovery from + * the [[LnInvoiceSignature]] or if [[LnTag.NodeIdTag]] is + * defined we MUST use that NodeId. + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#requirements-3]] + */ + def nodeId: NodeId = { + + if (lnTags.nodeId.isDefined) { + lnTags.nodeId.get.nodeId + } else { + val recoverId = signature.bytes.last + val sigData = signatureData + val hashMsg = CryptoUtil.sha256(sigData) + val (pubKey1, pubKey2) = CryptoUtil.recoverPublicKey(signature.signature, hashMsg.bytes) + if (recoverId % 2 == 0) { + NodeId(pubKey1) + } else { + NodeId(pubKey2) + } + } + + } + + /** + * The data that is hashed and then signed in the [[org.bitcoins.core.protocol.ln.LnInvoiceSignature]] + * @return + */ + def signatureData: ByteVector = { + val sig = LnInvoice.buildSignatureData(hrp, timestamp, lnTags) + sig + } + + private def sigHash: Sha256Digest = { + val hash = CryptoUtil.sha256(signatureData) + hash + } + + def bech32Checksum: String = { + val bytes: Vector[UInt5] = LnInvoice.createChecksum(hrp, data) + val bech32 = Bech32.encode5bitToString(bytes) + bech32 + } + + def isValidSignature(): Boolean = { + Try(nodeId.pubKey.verify(sigHash, signature.signature)).getOrElse(false) + } + + override def toString: String = { + val b = new StringBuilder + b.append(hrp.toString) + b.append(bech32Separator) + + val dataToString = Bech32.encode5bitToString(data) + b.append(dataToString) + b.append(bech32Checksum) + + b.toString() + } +} + +object LnInvoice { + private case class LnInvoiceImpl( + hrp: LnHumanReadablePart, + timestamp: UInt64, + lnTags: LnTaggedFields, + signature: LnInvoiceSignature) extends LnInvoice + + + val MAX_TIMESTAMP: BigInt = NumberUtil.pow2(35) + + val MAX_TIMESTAMP_U64: UInt64 = UInt64(MAX_TIMESTAMP) + + def decodeTimestamp(u5s: Vector[UInt5]): UInt64 = { + val decoded = LnUtil.decodeNumber(u5s) + UInt64(decoded) + } + + def hrpExpand(lnHumanReadablePart: LnHumanReadablePart): Vector[UInt5] = { + val bytes = lnHumanReadablePart.bytes + val u5s = Bech32.hrpExpand(bytes) + u5s + } + + def createChecksum(hrp: LnHumanReadablePart, data: Vector[UInt5]): Vector[UInt5] = { + val hrpBytes = hrpExpand(hrp) + val u5s = Bech32.createChecksum(hrpBytes ++ data) + u5s + } + + def verifyChecksum(hrp: LnHumanReadablePart, u5s: Seq[UInt5]): Boolean = { + val data = hrpExpand(hrp) ++ u5s + val checksum = Bech32.polyMod(data) + checksum == 1 + } + + def apply(hrp: LnHumanReadablePart, data: Vector[UInt5]): LnInvoice = { + + //https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#data-part + val TIMESTAMP_LEN = 7 + val SIGNATURE_LEN = 104 + val MIN_LENGTH = TIMESTAMP_LEN + SIGNATURE_LEN + if (data.length < MIN_LENGTH) { + throw new IllegalArgumentException( + s"Cannot create invoice with data length less then ${MIN_LENGTH}, got ${data.length}") + } else { + //first 35 bits is time stamp + val timestampU5s = data.take(TIMESTAMP_LEN) + + val timestamp = decodeTimestamp(timestampU5s) + + //last bits should be a 520 bit signature + //should be 104 5 bit increments (104 * 5 = 520) + val signatureU5s = data.takeRight(SIGNATURE_LEN) + val signature = LnInvoiceSignature.fromU5s(signatureU5s) + + val tags = data.slice(TIMESTAMP_LEN, data.length - SIGNATURE_LEN) + + val taggedFields = LnTaggedFields.fromUInt5s(tags) + + LnInvoice( + hrp = hrp, + timestamp = timestamp, + lnTags = taggedFields, + signature = signature) + } + + } + + def fromString(bech32String: String): Try[LnInvoice] = { + val sepIndexes = { + bech32String.zipWithIndex.filter(_._1 == Bech32.separator) + } + if (sepIndexes.isEmpty) { + Failure(new IllegalArgumentException("LnInvoice did not have the correct separator")) + } else { + val sepIndex = sepIndexes.last._2 + + val hrp = bech32String.take(sepIndex) + + val data = bech32String.splitAt(sepIndex + 1)._2 + + if (hrp.length < 1) { + Failure(new IllegalArgumentException("HumanReadablePart is too short")) + } else if (data.length < 6) { + Failure(new IllegalArgumentException("Data part is too short")) + } else { + + val hrpValid = LnHumanReadablePart.fromString(hrp) + + val dataValid = Bech32.checkDataValidity(data) + + val isChecksumValid: Try[Vector[UInt5]] = hrpValid.flatMap { h: LnHumanReadablePart => + dataValid.flatMap { d: Vector[UInt5] => + stripChecksumIfValid(h,d) + } + } + + isChecksumValid.flatMap { d: Vector[UInt5] => + hrpValid.map(h => LnInvoice(h, d)) + } + } + } + } + + def apply(hrp: LnHumanReadablePart, timestamp: UInt64, lnTags: LnTaggedFields, + signature: LnInvoiceSignature): LnInvoice = { + LnInvoiceImpl( + hrp = hrp, + timestamp = timestamp, + lnTags = lnTags, + signature = signature) + } + + def buildSignatureData( + hrp: LnHumanReadablePart, + timestamp: UInt64, + lnTags: LnTaggedFields): ByteVector = { + val tsu5 = uInt64ToBase32(timestamp) + val payloadU5 = tsu5 ++ lnTags.data + val payloadU8 = Bech32.from5bitTo8bit(payloadU5) + val payload = UInt8.toBytes(payloadU8) + hrp.bytes ++ payload + } + + def buildSigHashData( + hrp: LnHumanReadablePart, + timestamp: UInt64, + lnTags: LnTaggedFields): Sha256Digest = { + val sigdata = buildSignatureData(hrp, timestamp, lnTags) + CryptoUtil.sha256(sigdata) + } + + def buildLnInvoiceSignature( + hrp: LnHumanReadablePart, + timestamp: UInt64, + lnTags: LnTaggedFields, + privateKey: ECPrivateKey): LnInvoiceSignature = { + val sigHash = buildSigHashData(hrp, timestamp, lnTags) + val sig = privateKey.sign(sigHash) + + LnInvoiceSignature( + version = UInt8.zero, + signature = sig) + } + + /** + * The easiest way to create a [[LnInvoice]] + * is by just passing the given pareameters and + * and then build will create a [[LnInvoice]] + * with a valid [[LnInvoiceSignature]] + * @param hrp the [[LnHumanReadablePart]] + * @param timestamp the timestamp on the invoice + * @param lnTags the various tags in the invoice + * @param privateKey - the key used to sign the invoice + * @return + */ + def build( + hrp: LnHumanReadablePart, + timestamp: UInt64, + lnTags: LnTaggedFields, + privateKey: ECPrivateKey): LnInvoice = { + val lnInvoiceSignature = buildLnInvoiceSignature(hrp, timestamp, lnTags, privateKey) + + LnInvoice( + hrp = hrp, + timestamp = timestamp, + lnTags = lnTags, + signature = lnInvoiceSignature) + } + + /** + * The easiest way to create a [[LnInvoice]] + * is by just passing the given parameters and + * and then build will create a [[LnInvoice]] + * with a valid [[LnInvoiceSignature]] + * @param hrp the [[LnHumanReadablePart]] + * @param lnTags the various tags in the invoice + * @param privateKey - the key used to sign the invoice + * @return + */ + def build( + hrp: LnHumanReadablePart, + lnTags: LnTaggedFields, + privateKey: ECPrivateKey): LnInvoice = { + val timestamp = UInt64(System.currentTimeMillis() / 1000L) + val lnInvoiceSignature = buildLnInvoiceSignature(hrp, timestamp, lnTags, privateKey) + + LnInvoice( + hrp = hrp, + timestamp = timestamp, + lnTags = lnTags, + signature = lnInvoiceSignature) + } + + private def uInt64ToBase32(input: UInt64): Vector[UInt5] = { + var numNoPadding = LnUtil.encodeNumber(input.toBigInt) + + while (numNoPadding.length < 7) { + numNoPadding = UInt5.zero +: numNoPadding + } + + require(numNoPadding.length == 7) + numNoPadding + } + + + /** Checks the checksum on a [[org.bitcoins.core.protocol.Bech32Address]] + * and if it is valid, strips the checksum from @d and returns strictly + * the payload + * @param h - the [[LnHumanReadablePart]] of the invoice + * @param d - the payload WITH the checksum included + * @return + */ + private def stripChecksumIfValid(h : LnHumanReadablePart, d: Vector[UInt5]): Try[Vector[UInt5]] = { + if (verifyChecksum(h, d)) { + if (d.size < 6) Success(Vector.empty) + else Success(d.take(d.size - 6)) + } else Failure(new IllegalArgumentException("Checksum was invalid on the LnInvoice")) + } +} + diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoiceSignature.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoiceSignature.scala new file mode 100644 index 0000000000..6437ab91f6 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoiceSignature.scala @@ -0,0 +1,57 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.crypto.ECDigitalSignature +import org.bitcoins.core.number.{ UInt5, UInt8 } +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.{ Bech32, Factory } +import scodec.bits.ByteVector + +/** + * 520 bit digital signature that signs the [[org.bitcoins.core.protocol.ln.LnInvoice LnInvoice]]; + * See + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#data-part BOLT11]] + * for more info. + */ +sealed abstract class LnInvoiceSignature extends NetworkElement { + require(version.toInt >= 0 && version.toInt <= 3, s"signature recovery byte must be 0,1,2,3, got ${version.toInt}") + + def signature: ECDigitalSignature + + def version: UInt8 + + def data: Vector[UInt5] = { + val bytes = signature.toRawRS ++ version.bytes + Bech32.from8bitTo5bit(bytes) + } + + override def bytes: ByteVector = { + signature.toRawRS ++ version.bytes + } +} + +object LnInvoiceSignature extends Factory[LnInvoiceSignature] { + private case class LnInvoiceSignatureImpl( + version: UInt8, + signature: ECDigitalSignature) extends LnInvoiceSignature + + def apply(version: UInt8, signature: ECDigitalSignature): LnInvoiceSignature = { + LnInvoiceSignatureImpl(version, signature) + } + + def fromBytes(bytes: ByteVector): LnInvoiceSignature = { + val sigBytes = bytes.take(64) + val version = UInt8(bytes(64)) + + val signature = ECDigitalSignature.fromRS(sigBytes) + + LnInvoiceSignature.apply( + version = version, + signature = signature) + } + + def fromU5s(u5s: Vector[UInt5]): LnInvoiceSignature = { + val u8s = Bech32.from5bitTo8bit(u5s) + val bytes = UInt8.toBytes(u8s) + fromBytes(bytes) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala new file mode 100644 index 0000000000..6339536776 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala @@ -0,0 +1,87 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.config.{ MainNet, NetworkParameters, RegTest, TestNet3 } +import org.bitcoins.core.protocol.blockchain.ChainParams +import scodec.bits.ByteVector + +sealed abstract class LnParams { + def chain: ChainParams = network.chainParams + + def network: NetworkParameters + + def lnRpcPort: Int + + def lnPort: Int + + /** + * The prefix for generating invoices for a Lightning Invoice. See + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md BOLT11]] + * for more details + */ + def invoicePrefix: ByteVector +} + +object LnParams { + + case object LnBitcoinMainNet extends LnParams { + override def network: MainNet.type = MainNet + + override def lnRpcPort = 8080 + + override def lnPort = 9735 + + override val invoicePrefix: ByteVector = { + ByteVector('l', 'n', 'b', 'c') + } + } + + case object LnBitcoinTestNet extends LnParams { + override def network: TestNet3.type = TestNet3 + + override def lnRpcPort = 8080 + + override def lnPort = 9735 + + override val invoicePrefix: ByteVector = { + ByteVector('l', 'n', 't', 'b') + } + } + + case object LnBitcoinRegTest extends LnParams { + override def network: RegTest.type = RegTest + + override def lnRpcPort = 8080 + + override def lnPort = 9735 + + override val invoicePrefix: ByteVector = { + ByteVector('l', 'n', 'b', 'c', 'r', 't') + } + } + + def fromNetworkParameters(np: NetworkParameters): LnParams = np match { + case MainNet => LnBitcoinMainNet + case TestNet3 => LnBitcoinTestNet + case RegTest => LnBitcoinRegTest + } + + private val allNetworks: Vector[LnParams] = Vector(LnBitcoinMainNet, LnBitcoinTestNet, LnBitcoinRegTest) + + private val prefixes: Map[String, LnParams] = { + val vec: Vector[(String, LnParams)] = { + allNetworks.map { network => + (network.invoicePrefix.decodeAscii.right.get, network) + } + } + vec.toMap + } + + /** + * Returns a [[org.bitcoins.core.protocol.ln.LnParams LnParams]] whose + * network prefix matches the given string. See [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#human-readable-part BOLT11 ]] + * for more details on prefixes. + */ + def fromPrefixString(str: String): Option[LnParams] = { + prefixes.get(str) + } +} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnPolicy.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnPolicy.scala new file mode 100644 index 0000000000..71c40db68f --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnPolicy.scala @@ -0,0 +1,52 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.number.Int64 +import org.bitcoins.core.protocol.ln.currency.{LnMultiplier, MilliSatoshis} + +sealed abstract class LnPolicy { + + /** + * The "amount_msat" field has been artificially limited to a UInt32. This means that the current maximum transaction that can be completed + * over the lightning network is 4294967295 MilliSatoshi. + * This is a self imposed limit, and is subject to change. + * Please see [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc BOLT2]] + * for more info. + */ + val maxAmountMSat: MilliSatoshis = MilliSatoshis(4294967295L) + + + + val maxPicoBitcoins: BigInt = Int64.max.toBigInt + val minPicoBitcoins: BigInt = Int64.min.toBigInt + + val maxMilliBitcoins: BigInt = { + calc(LnMultiplier.Milli) + } + + val minMilliBitcoins: BigInt = -maxMilliBitcoins + + val maxMicroBitcoins: BigInt = { + calc(LnMultiplier.Micro) + } + val minMicroBitcoins: BigInt = -maxMicroBitcoins + + val maxNanoBitcoins: BigInt = { + calc(LnMultiplier.Nano) + } + + val minNanoBitcoins: BigInt = -maxNanoBitcoins + + private def calc(mul: LnMultiplier): BigInt = { + maxPicoBitcoins / + (mul.multiplier / LnMultiplier.Pico.multiplier) + .toBigIntExact().get + } + + + val DEFAULT_LN_P2P_PORT = 9735 + + val DEFAULT_ECLAIR_API_PORT = 8080 + +} + +object LnPolicy extends LnPolicy \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala new file mode 100644 index 0000000000..e3394f99fc --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala @@ -0,0 +1,86 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.number.UInt5 +import org.bitcoins.core.util.Bech32 + +sealed abstract class LnTagPrefix { + val value: Char + + override def toString: String = value.toString +} + +/** + * This defines the necessary Lightning Network Tag Prefix's, as specified in BOLT-11 + * Please see: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields + */ +object LnTagPrefix { + + case object PaymentHash extends LnTagPrefix { + override val value: Char = 'p' + } + case object Description extends LnTagPrefix { + override val value: Char = 'd' + } + + /** The nodeId of the node paying the invoice */ + case object NodeId extends LnTagPrefix { + override val value: Char = 'n' + } + + case object DescriptionHash extends LnTagPrefix { + override val value: Char = 'h' + } + + case object ExpiryTime extends LnTagPrefix { + override val value: Char = 'x' + } + + case object CltvExpiry extends LnTagPrefix { + override val value: Char = 'c' + } + + case object FallbackAddress extends LnTagPrefix { + override val value: Char = 'f' + } + + case object RoutingInfo extends LnTagPrefix { + override val value: Char = 'r' + } + + private lazy val all: Map[Char, LnTagPrefix] = + List(PaymentHash, + Description, + NodeId, + DescriptionHash, + ExpiryTime, + CltvExpiry, + FallbackAddress, + RoutingInfo) + .map(prefix => prefix.value -> prefix) + .toMap + + def fromString(str: String): Option[LnTagPrefix] = { + if (str.length == 1) { + fromChar(str.head) + } else { + None + } + } + + def fromChar(char: Char): Option[LnTagPrefix] = { + all.get(char) + } + + private lazy val prefixUInt5: Map[UInt5, LnTagPrefix] = { + all.map { + case (_, prefix) => + val index = Bech32.charset.indexOf(prefix.value) + (UInt5(index), prefix) + } + } + + def fromUInt5(u5: UInt5): Option[LnTagPrefix] = { + val p = prefixUInt5.get(u5) + p + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala new file mode 100644 index 0000000000..f3d69937b2 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala @@ -0,0 +1,206 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.number.{UInt5, UInt8} +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.protocol.ln.LnTag.PaymentHashTag +import org.bitcoins.core.protocol.ln.util.LnUtil +import org.bitcoins.core.util.Bech32 +import scodec.bits.ByteVector + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.reflect.ClassTag + +/** + * An aggregation of all the individual tagged fields in a [[org.bitcoins.core.protocol.ln.LnInvoice]] + */ +sealed abstract class LnTaggedFields extends NetworkElement { + + require( + (description.nonEmpty && description.get.string.length < 640) || + descriptionHash.nonEmpty, + "You must supply either a description hash, or a literal description that is 640 characters or less to create an invoice.") + + def paymentHash: LnTag.PaymentHashTag + + def description: Option[LnTag.DescriptionTag] + + def nodeId: Option[LnTag.NodeIdTag] + + def descriptionHash: Option[LnTag.DescriptionHashTag] + + def expiryTime: Option[LnTag.ExpiryTimeTag] + + def cltvExpiry: Option[LnTag.MinFinalCltvExpiry] + + def fallbackAddress: Option[LnTag.FallbackAddressTag] + + def routingInfo: Option[LnTag.RoutingInfo] + + lazy val data: Vector[UInt5] = Vector( + Some(paymentHash), + description, + nodeId, + descriptionHash, + expiryTime, + cltvExpiry, + fallbackAddress, + routingInfo) + .filter(_.isDefined) + .flatMap(_.get.data) + + override def bytes: ByteVector = { + val u8s = Bech32.from5bitTo8bit(data) + UInt8.toBytes(u8s) + } + + override def toString: String = { + val b = new mutable.StringBuilder() + + val string = Bech32.encode5bitToString(data) + b.append(string) + + b.toString() + } +} + +object LnTaggedFields { + private case class InvoiceTagImpl( + paymentHash: LnTag.PaymentHashTag, + description: Option[LnTag.DescriptionTag], + nodeId: Option[LnTag.NodeIdTag], + descriptionHash: Option[LnTag.DescriptionHashTag], + expiryTime: Option[LnTag.ExpiryTimeTag], + cltvExpiry: Option[LnTag.MinFinalCltvExpiry], + fallbackAddress: Option[LnTag.FallbackAddressTag], + routingInfo: Option[LnTag.RoutingInfo]) extends LnTaggedFields + + /** + * According to BOLT11 these are the required fields in a LnInvoice + * You need to provide a payment hash and either a description, + * or the hash of the description + */ + def apply( + paymentHashTag: PaymentHashTag, + descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag]): LnTaggedFields = { + + LnTaggedFields.apply(paymentHashTag, descriptionOrHash) + } + + def apply( + paymentHash: LnTag.PaymentHashTag, + descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag], + nodeId: Option[LnTag.NodeIdTag] = None, + expiryTime: Option[LnTag.ExpiryTimeTag] = None, + cltvExpiry: Option[LnTag.MinFinalCltvExpiry] = None, + fallbackAddress: Option[LnTag.FallbackAddressTag] = None, + routingInfo: Option[LnTag.RoutingInfo] = None): LnTaggedFields = { + + val (description, descriptionHash): (Option[LnTag.DescriptionTag], + Option[LnTag.DescriptionHashTag]) = { + if (descriptionOrHash.isLeft) { + (descriptionOrHash.left.toOption, None) + } else { + (None, descriptionOrHash.right.toOption) + } + } + + InvoiceTagImpl( + paymentHash = paymentHash, + description = description, + nodeId = nodeId, + descriptionHash = descriptionHash, + expiryTime = expiryTime, + cltvExpiry = cltvExpiry, + fallbackAddress = fallbackAddress, + routingInfo = routingInfo) + } + + + /** This is intended to parse all of the [[org.bitcoins.core.protocol.ln.LnTaggedFields LnTaggedFields]] + * from the tagged part of the ln invoice. This should only be called + * if other information has already been remove from the invoice + * like the [[LnHumanReadablePart]] + * @param u5s payload of the tagged fields in the invoice + * @return + */ + def fromUInt5s(u5s: Vector[UInt5]): LnTaggedFields = { + @tailrec + def loop(remaining: List[UInt5], fields: Vector[LnTag]): Vector[LnTag] = { + remaining match { + case h :: h1 :: h2 :: t => + + val prefix = LnTagPrefix.fromUInt5(h) + + //next two 5 bit increments are data_length + val dataLengthU5s = Vector(h1, h2) + + val dataLength = LnUtil.decodeDataLength(dataLengthU5s) + + //t is the actual possible payload + val payload: Vector[UInt5] = t.take(dataLength.toInt).toVector + + val tag = LnTag.fromLnTagPrefix(prefix.get, payload) + + val newRemaining = t.slice(payload.size, t.size) + + loop(newRemaining, fields.:+(tag)) + case Nil => + fields + case _ :: _ | _ :: _ :: _ => + throw new IllegalArgumentException( + "Failed to parse LnTaggedFields, needs 15bits of meta data to be able to parse") + } + } + + + val tags = loop(u5s.toList, Vector.empty) + + def getTag[T <: LnTag : ClassTag]: Option[T] = { + tags.map { + case t: T => Some(t) + case _ => None + }.find(_.isDefined).flatten + } + + val paymentHashTag = getTag[LnTag.PaymentHashTag].getOrElse( + throw new IllegalArgumentException(s"Payment hash must be defined in a LnInvoice") + ) + + val description = getTag[LnTag.DescriptionTag] + + val descriptionHash = getTag[LnTag.DescriptionHashTag] + + val nodeId = getTag[LnTag.NodeIdTag] + + val expiryTime = getTag[LnTag.ExpiryTimeTag] + + val cltvExpiry = getTag[LnTag.MinFinalCltvExpiry] + + val fallbackAddress = getTag[LnTag.FallbackAddressTag] + + val routingInfo = getTag[LnTag.RoutingInfo] + + val d: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag] = { + if (description.isDefined && descriptionHash.isDefined) { + throw new IllegalArgumentException(s"Cannot have both description and description hash") + } else if (description.isEmpty && descriptionHash.isEmpty) { + throw new IllegalArgumentException(s"One of description / description hash fields must be defind") + } else if (description.isDefined) { + Left(description.get) + } else { + Right(descriptionHash.get) + } + } + + LnTaggedFields( + paymentHash = paymentHashTag, + descriptionOrHash = d, + nodeId = nodeId, + expiryTime = expiryTime, + cltvExpiry = cltvExpiry, + fallbackAddress = fallbackAddress, + routingInfo = routingInfo) + + } +} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala new file mode 100644 index 0000000000..8e98fb0bbf --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala @@ -0,0 +1,295 @@ +package org.bitcoins.core.protocol.ln + +import java.nio.charset.Charset + +import org.bitcoins.core.config.{MainNet, NetworkParameters} +import org.bitcoins.core.crypto.{Sha256Digest, Sha256Hash160Digest} +import org.bitcoins.core.number.{UInt32, UInt5, UInt8} +import org.bitcoins.core.protocol._ +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.core.protocol.ln.routing.LnRoute +import org.bitcoins.core.protocol.ln.util.LnUtil +import org.bitcoins.core.protocol.script.{P2WPKHWitnessSPKV0, P2WSHWitnessSPKV0} +import org.bitcoins.core.util.{Bech32, CryptoUtil} +import org.slf4j.LoggerFactory +import scodec.bits.ByteVector + +import scala.annotation.tailrec + +/** + * One of the tagged fields on a Lightning Network invoice + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields]] + */ +sealed abstract class LnTag { + + def prefix: LnTagPrefix + + def prefixUInt5: UInt5 = { + val char = Bech32.charset.indexOf(prefix.value) + UInt5(char.toByte) + } + + /** The payload for the tag without any meta information encoded with it */ + def encoded: Vector[UInt5] + + def data: Vector[UInt5] = { + val len = LnUtil.createDataLength(encoded) + prefixUInt5 +: (len ++ encoded) + } + + override def toString: String = { + + val dataBech32 = Bech32.encode5bitToString(data) + + dataBech32 + + } +} + +/** + * All of the different invoice tags that are currently defined + * Refer to BOLT11 for a full list + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields]] + */ +object LnTag { + + + sealed abstract class FallbackAddressV { + val u8: UInt8 + } + + + + /** Fallback address versions */ + object FallbackAddressV { + + case object P2PKH extends FallbackAddressV { + val u8: UInt8 = UInt8(17) + } + + case object P2SH extends FallbackAddressV { + val u8: UInt8 = UInt8(18) + } + + case object WitSPK extends FallbackAddressV { + val u8 = UInt8.zero + } + + private def witnessFromU8(bytes: ByteVector, np: NetworkParameters) = { + val witSPK = bytes.size match { + case 32 => + val hash = Sha256Digest.fromBytes(bytes) + P2WSHWitnessSPKV0.fromHash(hash) + case 20 => + val hash = Sha256Hash160Digest.fromBytes(bytes) + P2WPKHWitnessSPKV0.fromHash(hash) + case _: Long => + throw new IllegalArgumentException(s"Can only create witness spk out of a 32 byte or 20 byte hash, got ${bytes.length}") + } + Bech32Address(witSPK, np) + } + + def fromU8(version: UInt8, bytes: ByteVector, np: NetworkParameters): FallbackAddressTag = { + val address: Address = version match { + case P2PKH.u8 => + val hash = Sha256Hash160Digest(bytes) + P2PKHAddress(hash, np) + case P2SH.u8 => + val hash = Sha256Hash160Digest(bytes) + P2SHAddress(hash, np) + case WitSPK.u8 => witnessFromU8(bytes, np) + case _: UInt8 => + throw new IllegalArgumentException(s"Illegal version to create a fallback address from, got $version") + } + LnTag.FallbackAddressTag(address) + } + } + + private def u32ToU5(u32: UInt32): Vector[UInt5] = { + val encoded = LnUtil.encodeNumber(u32.toLong) + encoded + } + + case class PaymentHashTag(hash: Sha256Digest) extends LnTag { + + override val prefix: LnTagPrefix = LnTagPrefix.PaymentHash + + override val encoded: Vector[UInt5] = { + Bech32.from8bitTo5bit(hash.bytes) + } + } + + case class DescriptionTag(string: String) extends LnTag { + override val prefix: LnTagPrefix = LnTagPrefix.Description + + def descBytes: ByteVector = { + ByteVector(string.getBytes("UTF-8")) + } + + def descriptionHashTag: LnTag.DescriptionHashTag = { + val hash = CryptoUtil.sha256(descBytes) + LnTag.DescriptionHashTag(hash) + } + + override val encoded: Vector[UInt5] = { + val bytes = ByteVector(string.getBytes("UTF-8")) + Bech32.from8bitTo5bit(bytes) + } + + } + + case class NodeIdTag(nodeId: NodeId) extends LnTag { + + override val prefix: LnTagPrefix = LnTagPrefix.NodeId + + override val encoded: Vector[UInt5] = { + Bech32.from8bitTo5bit(nodeId.bytes) + } + } + + case class DescriptionHashTag(hash: Sha256Digest) extends LnTag { + override val prefix: LnTagPrefix = LnTagPrefix.DescriptionHash + + override val encoded: Vector[UInt5] = { + Bech32.from8bitTo5bit(hash.bytes) + } + } + + /** The amount in seconds until this payment request expires */ + case class ExpiryTimeTag(u32: UInt32) extends LnTag { + override val prefix: LnTagPrefix = LnTagPrefix.ExpiryTime + + override val encoded: Vector[UInt5] = { + LnUtil.encodeNumber(u32.toLong) + } + } + + /** + * `min_final_ctlv_expiry` is the minimum difference between + * HTLC CLTV timeout and the current block height, for the + * terminal case (C). This is denominated in blocks. + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection]] + */ + case class MinFinalCltvExpiry(u32: UInt32) extends LnTag { + override val prefix: LnTagPrefix = LnTagPrefix.CltvExpiry + + override val encoded: Vector[UInt5] = { + u32ToU5(u32) + } + + } + + case class FallbackAddressTag(address: Address) extends LnTag { + + /** The version of the fallback address is indicated here in BOLT11 */ + def version: UInt8 = { + address match { + case _: P2PKHAddress => FallbackAddressV.P2PKH.u8 + case _: P2SHAddress => FallbackAddressV.P2SH.u8 + case bech32: Bech32Address => + UInt8(bech32.scriptPubKey.witnessVersion.version.toInt) + } + } + + override val prefix: LnTagPrefix = LnTagPrefix.FallbackAddress + + override val encoded: Vector[UInt5] = { + val b = address.hash.bytes + val u5s = version.toUInt5 +: Bech32.from8bitTo5bit(b) + u5s + } + } + + case class RoutingInfo(routes: Vector[LnRoute]) extends LnTag { + + override val prefix: LnTagPrefix = LnTagPrefix.RoutingInfo + + override val encoded: Vector[UInt5] = { + if (routes.isEmpty) { + Vector.empty + } else { + val serializedRoutes: ByteVector = { + routes.foldLeft(ByteVector.empty)(_ ++ _.bytes) + } + + val u5s = Bech32.from8bitTo5bit(serializedRoutes) + u5s + } + } + } + + object RoutingInfo { + def fromU5s(u5s: Vector[UInt5]): RoutingInfo = { + + @tailrec + def loop(remaining: ByteVector, accum: Vector[LnRoute]): Vector[LnRoute] = { + if (remaining.isEmpty) { + accum + } else { + val route = LnRoute.fromBytes(remaining) + val newRemaining = remaining.slice(route.size, remaining.size) + loop(newRemaining, accum.:+(route)) + } + } + + val bytes = UInt8.toBytes(Bech32.from5bitTo8bit(u5s)) + val vecRoutes: Vector[LnRoute] = loop(bytes, Vector.empty) + + LnTag.RoutingInfo(vecRoutes) + + } + } + + def fromLnTagPrefix(prefix: LnTagPrefix, payload: Vector[UInt5]): LnTag = { + + val u8s = Bech32.from5bitTo8bit(payload) + val bytes = UInt8.toBytes(u8s) + + val tag: LnTag = prefix match { + case LnTagPrefix.PaymentHash => + + val hash = Sha256Digest.fromBytes(bytes) + LnTag.PaymentHashTag(hash) + + case LnTagPrefix.Description => + + val description = new String(bytes.toArray, Charset.forName("UTF-8")) + LnTag.DescriptionTag(description) + + case LnTagPrefix.DescriptionHash => + + val hash = Sha256Digest.fromBytes(bytes) + LnTag.DescriptionHashTag(hash) + + case LnTagPrefix.NodeId => + + val nodeId = NodeId.fromBytes(bytes) + + LnTag.NodeIdTag(nodeId) + + case LnTagPrefix.ExpiryTime => + + val decoded = LnUtil.decodeNumber(payload) + val u32 = UInt32(decoded) + LnTag.ExpiryTimeTag(u32) + + case LnTagPrefix.CltvExpiry => + + val decoded = LnUtil.decodeNumber(payload) + val u32 = UInt32(decoded) + LnTag.MinFinalCltvExpiry(u32) + + case LnTagPrefix.FallbackAddress => + + val version = payload.head.toUInt8 + val noVersion = payload.tail + val noVersionBytes = UInt8.toBytes(Bech32.from5bitTo8bit(noVersion)) + FallbackAddressV.fromU8(version, noVersionBytes, MainNet) + + case LnTagPrefix.RoutingInfo => + RoutingInfo.fromU5s(payload) + } + + tag + } +} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/ShortChannelId.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/ShortChannelId.scala new file mode 100644 index 0000000000..56727b89f8 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/ShortChannelId.scala @@ -0,0 +1,17 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.number.UInt64 +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.Factory +import scodec.bits.ByteVector + +case class ShortChannelId(u64: UInt64) extends NetworkElement { + override def bytes: ByteVector = u64.bytes +} + +object ShortChannelId extends Factory[ShortChannelId] { + + override def fromBytes(byteVector: ByteVector): ShortChannelId = { + new ShortChannelId(UInt64.fromBytes(byteVector)) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelId.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelId.scala new file mode 100644 index 0000000000..434b4fdfbd --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelId.scala @@ -0,0 +1,42 @@ +package org.bitcoins.core.protocol.ln.channel + +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.Factory +import scodec.bits.ByteVector + +/** + * There are two types of ChannelIds in the lightning protocol + * There is a 'temporary' channel id used for the hand shake when initially establishing + * a channel and then a FundedChannelId indicating a channel that has a validly signed tx + * For more information on the distinction between these two types please read + * about + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#channel-establishment establishing a channel]] + */ +sealed abstract class ChannelId extends NetworkElement + +/** + * Represents the the temporary channelId created in the + * `open_channel` msg of the + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#the-open_channel-message LN p2p protocol]] + */ +case class TempChannelId(bytes: ByteVector) extends ChannelId { + require(bytes.length == 32, s"ChannelId must be 32 bytes in size, got ${bytes.length}") +} + +/** + * Represents the stable ChannelId that represents a channel that has been signed by both parties + * This is created in the `funding_signed` msg on the + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#the-funding_signed-message LN p2p protocol]]. + * + * This channelId is derived It's derived from the funding transaction by combining the `funding_txid` and + * the `funding_output_index` using big-endian exclusive-OR (i.e. `funding_output_index` alters the last 2 bytes). + */ +case class FundedChannelId(bytes: ByteVector) extends ChannelId { + require(bytes.length == 32, s"ChannelId must be 32 bytes in size, got ${bytes.length}") +} + +object FundedChannelId extends Factory[FundedChannelId] { + override def fromBytes(bytes: ByteVector): FundedChannelId = { + new FundedChannelId(bytes) + } +} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelState.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelState.scala new file mode 100644 index 0000000000..c4246504a0 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/channel/ChannelState.scala @@ -0,0 +1,54 @@ +package org.bitcoins.core.protocol.ln.channel + +/** + * Copied from [[https://github.com/ACINQ/eclair/blob/master/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala Eclair]] + */ +sealed trait ChannelState + +object ChannelState { + case object WAIT_FOR_INIT_INTERNAL extends ChannelState + case object WAIT_FOR_OPEN_CHANNEL extends ChannelState + case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState + case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState + case object WAIT_FOR_FUNDING_CREATED extends ChannelState + case object WAIT_FOR_FUNDING_SIGNED extends ChannelState + case object WAIT_FOR_FUNDING_CONFIRMED extends ChannelState + case object WAIT_FOR_FUNDING_LOCKED extends ChannelState + case object NORMAL extends ChannelState + case object SHUTDOWN extends ChannelState + case object NEGOTIATING extends ChannelState + case object CLOSING extends ChannelState + case object CLOSED extends ChannelState + case object OFFLINE extends ChannelState + case object SYNCING extends ChannelState + case object WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT extends ChannelState + case object ERR_FUNDING_LOST extends ChannelState + case object ERR_FUNDING_TIMEOUT extends ChannelState + case object ERR_INFORMATION_LEAK extends ChannelState + + private lazy val all: Map[String, ChannelState] = List( + WAIT_FOR_INIT_INTERNAL, + WAIT_FOR_OPEN_CHANNEL, + WAIT_FOR_ACCEPT_CHANNEL, + WAIT_FOR_FUNDING_INTERNAL, + WAIT_FOR_FUNDING_CREATED, + WAIT_FOR_FUNDING_SIGNED, + WAIT_FOR_FUNDING_CONFIRMED, + WAIT_FOR_FUNDING_LOCKED, + NORMAL, + SHUTDOWN, + NEGOTIATING, + CLOSING, + CLOSED, + OFFLINE, + SYNCING, + WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, + ERR_FUNDING_LOST, + ERR_FUNDING_TIMEOUT, + ERR_INFORMATION_LEAK + ).map(state => state.toString -> state).toMap + + def fromString(str: String): Option[ChannelState] = { + all.get(str) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnit.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnit.scala new file mode 100644 index 0000000000..1dcf2d98e1 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnCurrencyUnit.scala @@ -0,0 +1,234 @@ +package org.bitcoins.core.protocol.ln.currency + +import org.bitcoins.core.currency.Satoshis +import org.bitcoins.core.number.{ BaseNumbers, Int64, UInt5 } +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.protocol.ln._ +import org.bitcoins.core.util.Bech32 +import scodec.bits.ByteVector + +import scala.util.{ Failure, Try } + +sealed abstract class LnCurrencyUnit extends NetworkElement { + def character: Char + + def >=(ln: LnCurrencyUnit): Boolean = { + toPicoBitcoinValue >= ln.toPicoBitcoinValue + } + + def >(ln: LnCurrencyUnit): Boolean = { + toPicoBitcoinValue > ln.toPicoBitcoinValue + } + + def <(ln: LnCurrencyUnit): Boolean = { + toPicoBitcoinValue < ln.toPicoBitcoinValue + } + + def <=(ln: LnCurrencyUnit): Boolean = { + toPicoBitcoinValue <= ln.toPicoBitcoinValue + } + + def !=(ln: LnCurrencyUnit): Boolean = !(this == ln) + + def ==(ln: LnCurrencyUnit): Boolean = toPicoBitcoinValue == ln.toPicoBitcoinValue + + def +(ln: LnCurrencyUnit): LnCurrencyUnit = { + PicoBitcoins(toPicoBitcoinValue + ln.toPicoBitcoinValue) + } + + def -(ln: LnCurrencyUnit): LnCurrencyUnit = { + PicoBitcoins(toPicoBitcoinValue - ln.toPicoBitcoinValue) + } + + def *(ln: LnCurrencyUnit): LnCurrencyUnit = { + PicoBitcoins(toPicoBitcoinValue * ln.toPicoBitcoinValue) + } + + def unary_- : LnCurrencyUnit = { + PicoBitcoins(-toPicoBitcoinValue) + } + + override def bytes: ByteVector = Int64(toPicoBitcoinValue).bytes.reverse + + def toUInt5s: Vector[UInt5] = { + val u5s = Bech32.from8bitTo5bit(bytes) + u5s + } + + def toBigInt: BigInt + + def toLong: Long = toBigInt.bigInteger.longValueExact() + + def toInt: Int = toBigInt.bigInteger.intValueExact() + + protected def underlying: BigInt + + def toSatoshis: Satoshis = { + LnCurrencyUnits.toSatoshi(this) + } + + def toPicoBitcoinValue: BigInt = { + toBigInt * toPicoBitcoinMultiplier + } + + def toPicoBitcoinDecimal: BigDecimal = { + BigDecimal(toPicoBitcoinValue.bigInteger) + } + + def toPicoBitcoinMultiplier: Int + + def toPicoBitcoins: PicoBitcoins = PicoBitcoins(toPicoBitcoinValue) + + def encodedBytes: ByteVector = { + ByteVector(toEncodedString.map(_.toByte)) + } + + def toMSat: MilliSatoshis = MilliSatoshis.fromPico(toPicoBitcoins) + + /** This returns the string encoding defined in BOLT11 + * For instance, 100 + * [[org.bitcoins.core.protocol.ln.currency.PicoBitcoins PicoBitcoins]] + * would appear as "100p" + */ + def toEncodedString: String = { + toBigInt + character.toString + } +} + +sealed abstract class MilliBitcoins extends LnCurrencyUnit { + override def character: Char = 'm' + + override def toPicoBitcoinMultiplier: Int = 1000000000 + + override def toBigInt: BigInt = underlying + +} + +object MilliBitcoins extends BaseNumbers[MilliBitcoins] { + val min = MilliBitcoins(LnPolicy.minMilliBitcoins) + val max = MilliBitcoins(LnPolicy.maxMilliBitcoins) + val zero = MilliBitcoins(0) + val one = MilliBitcoins(1) + + def apply(milliBitcoins: Int64): MilliBitcoins = MilliBitcoins(milliBitcoins.toBigInt) + + def apply(underlying: BigInt): MilliBitcoins = MilliBitcoinsImpl(underlying) + + private case class MilliBitcoinsImpl(underlying: BigInt) extends MilliBitcoins { + require(underlying >= LnPolicy.minMilliBitcoins, "Number was too small for MilliBitcoins, got: " + underlying) + require(underlying <= LnPolicy.maxMilliBitcoins, "Number was too big for MilliBitcoins, got: " + underlying) + } +} + +sealed abstract class MicroBitcoins extends LnCurrencyUnit { + override def character: Char = 'u' + + override def toPicoBitcoinMultiplier: Int = 1000000 + + override def toBigInt: BigInt = underlying + +} + +object MicroBitcoins extends BaseNumbers[MicroBitcoins] { + val min = MicroBitcoins(LnPolicy.minMicroBitcoins) + val max = MicroBitcoins(LnPolicy.maxMicroBitcoins) + val zero = MicroBitcoins(0) + val one = MicroBitcoins(1) + + def apply(microBitcoins: Int64): MicroBitcoins = MicroBitcoins(microBitcoins.toBigInt) + + def apply(underlying: BigInt): MicroBitcoins = MicroBitcoinsImpl(underlying) + + private case class MicroBitcoinsImpl(underlying: BigInt) extends MicroBitcoins { + require(underlying >= LnPolicy.minMicroBitcoins, "Number was too small for MicroBitcoins, got: " + underlying) + require(underlying <= LnPolicy.maxMicroBitcoins, "Number was too big for MicroBitcoins, got: " + underlying) + } +} + +sealed abstract class NanoBitcoins extends LnCurrencyUnit { + override def character: Char = 'n' + + override def toPicoBitcoinMultiplier: Int = 1000 + + override def toBigInt: BigInt = underlying + +} + +object NanoBitcoins extends BaseNumbers[NanoBitcoins] { + val min = NanoBitcoins(LnPolicy.minNanoBitcoins) + val max = NanoBitcoins(LnPolicy.maxNanoBitcoins) + val zero = NanoBitcoins(0) + val one = NanoBitcoins(1) + + def apply(nanoBitcoins: Int64): NanoBitcoins = NanoBitcoins(nanoBitcoins.toBigInt) + + def apply(underlying: BigInt): NanoBitcoins = NanoBitcoinsImpl(underlying) + + private case class NanoBitcoinsImpl(underlying: BigInt) extends NanoBitcoins { + require(underlying >= LnPolicy.minNanoBitcoins, "Number was too small for NanoBitcoins, got: " + underlying) + require(underlying <= LnPolicy.maxNanoBitcoins, "Number was too big for NanoBitcoins, got: " + underlying) + } +} + +sealed abstract class PicoBitcoins extends LnCurrencyUnit { + override def character: Char = 'p' + + override def toPicoBitcoinMultiplier: Int = 1 + + override def toBigInt: BigInt = underlying +} + +object PicoBitcoins extends BaseNumbers[PicoBitcoins] { + val min = PicoBitcoins(LnPolicy.minPicoBitcoins) + val max = PicoBitcoins(LnPolicy.maxPicoBitcoins) + val zero = PicoBitcoins(0) + val one = PicoBitcoins(1) + + def apply(i64: Int64): PicoBitcoins = PicoBitcoins(i64.toBigInt) + + def apply(underlying: BigInt): PicoBitcoins = PicoBitcoinsImpl(underlying) + + private case class PicoBitcoinsImpl(underlying: BigInt) extends PicoBitcoins { + require(underlying >= LnPolicy.minPicoBitcoins, "Number was too small for PicoBitcoins, got: " + underlying) + require(underlying <= LnPolicy.maxPicoBitcoins, "Number was too big for PicoBitcoins, got: " + underlying) + } +} + +object LnCurrencyUnits { + private[currency] val PICO_TO_SATOSHIS = 10000 + private[currency] val MSAT_TO_PICO = 10 + val zero: LnCurrencyUnit = PicoBitcoins.zero + + /** + * For information regarding the rounding of sub-Satoshi values, see + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#commitment-transaction-outputs BOLT3]] + */ + def toSatoshi(lnCurrencyUnits: LnCurrencyUnit): Satoshis = { + val pico = lnCurrencyUnits.toPicoBitcoins + val sat = pico.toBigInt / PICO_TO_SATOSHIS + Satoshis(Int64(sat)) + } + + def fromMSat(msat: MilliSatoshis): PicoBitcoins = { + //msat are technically 10^-11 + //while pico are 10^-12, so we need to convert + val underlying = msat.toBigInt * MSAT_TO_PICO + PicoBitcoins(underlying) + } + + def fromEncodedString(input: String): Try[LnCurrencyUnit] = { + val (amountStr, unit) = input.splitAt(input.length - 1) + val amount = Try(BigInt(amountStr)) + if (amount.isSuccess) { + unit match { + case "m" => Try(MilliBitcoins(amount.get)) + case "u" => Try(MicroBitcoins(amount.get)) + case "n" => Try(NanoBitcoins(amount.get)) + case "p" => Try(PicoBitcoins(amount.get)) + case _: String => Failure(new IllegalArgumentException(s"LnCurrencyUnit not found. Expected MilliBitcoins (m), MicroBitcoins (u), NanoBitcoins (n), or PicoBitcoins (p), got: $unit")) + } + } else { + Failure(new IllegalArgumentException(s"Could not convert amount to valid number, got: $amount")) + } + } +} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnMultiplier.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnMultiplier.scala new file mode 100644 index 0000000000..44bc3a0ef9 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/LnMultiplier.scala @@ -0,0 +1,26 @@ +package org.bitcoins.core.protocol.ln.currency + +/** Used by [[org.bitcoins.core.protocol.ln.currency.LnCurrencyUnit LnCurrencyUnit]] + * to scale between values + */ +sealed abstract class LnMultiplier { + val multiplier: BigDecimal +} + +object LnMultiplier { + case object Milli extends LnMultiplier { + val multiplier: BigDecimal = BigDecimal(0.001) + } + + case object Micro extends LnMultiplier { + val multiplier: BigDecimal = BigDecimal(0.000001) + } + + case object Nano extends LnMultiplier { + override val multiplier: BigDecimal = BigDecimal(0.000000001) + } + + case object Pico extends LnMultiplier { + override val multiplier: BigDecimal = BigDecimal(0.000000000001) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshis.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshis.scala new file mode 100644 index 0000000000..878e4662e2 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/currency/MilliSatoshis.scala @@ -0,0 +1,126 @@ +package org.bitcoins.core.protocol.ln.currency + +import org.bitcoins.core.currency.{CurrencyUnit, Satoshis} +import org.bitcoins.core.number.UInt64 +import org.bitcoins.core.protocol.NetworkElement +import scodec.bits.ByteVector + +import scala.math.BigDecimal.RoundingMode + +/** + * The common currency unit used in the + * LN protocol for updating HTLCs. See + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc BOLT2]] + */ +sealed abstract class MilliSatoshis extends NetworkElement { + require(toBigInt >= 0, s"Millisatoshis cannot be negative, got $toBigInt") + + protected def underlying: BigInt + + def toBigInt: BigInt = underlying + + def toLong: Long = toBigInt.bigInteger.longValueExact + + def toBigDecimal: BigDecimal = BigDecimal(toBigInt) + + def toLnCurrencyUnit: LnCurrencyUnit = { + LnCurrencyUnits.fromMSat(this) + } + + def ==(lnCurrencyUnit: LnCurrencyUnit): Boolean = { + toLnCurrencyUnit == lnCurrencyUnit + } + + def !=(lnCurrencyUnit: LnCurrencyUnit): Boolean = { + toLnCurrencyUnit != lnCurrencyUnit + } + + def >=(ln: LnCurrencyUnit): Boolean = { + toLnCurrencyUnit >= ln + } + + def >(ln: LnCurrencyUnit): Boolean = { + toLnCurrencyUnit > ln + } + + def <(ln: LnCurrencyUnit): Boolean = { + toLnCurrencyUnit < ln + } + + def <=(ln: LnCurrencyUnit): Boolean = { + toLnCurrencyUnit <= ln + } + + def ==(ms: MilliSatoshis): Boolean = { + toLnCurrencyUnit == ms.toLnCurrencyUnit + } + + def !=(ms: MilliSatoshis): Boolean = { + toLnCurrencyUnit != ms.toLnCurrencyUnit + } + + def >=(ms: MilliSatoshis): Boolean = { + toLnCurrencyUnit >= ms.toLnCurrencyUnit + } + + def >(ms: MilliSatoshis): Boolean = { + toLnCurrencyUnit > ms.toLnCurrencyUnit + } + + def <(ms: MilliSatoshis): Boolean = { + toLnCurrencyUnit < ms.toLnCurrencyUnit + } + + def <=(ms: MilliSatoshis): Boolean = { + toLnCurrencyUnit <= ms.toLnCurrencyUnit + } + + def toUInt64: UInt64 = { + UInt64(underlying) + } + + def toSatoshis: Satoshis = { + toLnCurrencyUnit.toSatoshis + } + + override def bytes: ByteVector = toUInt64.bytes.reverse + +} + +object MilliSatoshis { + + private case class MilliSatoshisImpl(underlying: BigInt) extends MilliSatoshis + + val zero: MilliSatoshis = MilliSatoshis(0) + val one: MilliSatoshis = MilliSatoshis(1) + + def apply(underlying: BigInt): MilliSatoshis = { + MilliSatoshisImpl(underlying) + } + + def fromPico(picoBitcoins: PicoBitcoins): MilliSatoshis = { + val pico = picoBitcoins.toPicoBitcoinDecimal + // we need to divide by 10 to get to msat + val msatDec = pico / LnCurrencyUnits.MSAT_TO_PICO + + //now we need to round, we are going to round the same way round + //outputs when publishing txs to the blockchain + //https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#commitment-transaction-outputs + + val rounded = msatDec.setScale(0, RoundingMode.DOWN) + + MilliSatoshis(rounded.toBigIntExact.get) + } + + def apply(lnCurrencyUnit: LnCurrencyUnit): MilliSatoshis = { + fromPico(picoBitcoins = lnCurrencyUnit.toPicoBitcoins) + } + + def apply(currencyUnit: CurrencyUnit): MilliSatoshis = { + fromSatoshis(currencyUnit.satoshis) + } + + def fromSatoshis(sat: Satoshis): MilliSatoshis = { + MilliSatoshis(sat.toBigInt * 1000) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/fee/FeeBaseMSat.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/fee/FeeBaseMSat.scala new file mode 100644 index 0000000000..e887fd25c6 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/fee/FeeBaseMSat.scala @@ -0,0 +1,33 @@ +package org.bitcoins.core.protocol.ln.fee + +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.protocol.ln.currency.MilliSatoshis +import scodec.bits.ByteVector + +/** + * Represents the fee we charge for forwarding an HTLC on the Lightning network + * This is used in the ChannelUpdate and Routing information of a [[org.bitcoins.core.protocol.ln.LnInvoice LnInvoice]] + * See + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_update-message BOLT7]] + * and + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#on-mainnet-with-fallback-address-1rustyrx2oai4eyydpqgwvel62bbgqn9t-with-extra-routing-info-to-go-via-nodes-029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255-then-039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 BOLT11]] + */ +case class FeeBaseMSat(msat: MilliSatoshis) extends NetworkElement { + require(msat.toLong <= UInt32.max.toLong, s"Value too large for FeeBaseMSat $msat") + + override def bytes: ByteVector = { + //note that the feebase msat is only 4 bytes + UInt32(msat.toLong).bytes + } +} + +/** + * Represents the proportion of a satoshi we charge for forwarding an HTLC + * through our channel. I.e. if the forwarded payment is larger, this fee will be larger. + * See + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_update-message BOLT7]] + */ +case class FeeProportionalMillionths(u32: UInt32) extends NetworkElement { + override def bytes: ByteVector = u32.bytes +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/node/NodeId.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/node/NodeId.scala new file mode 100644 index 0000000000..dd2c71760e --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/node/NodeId.scala @@ -0,0 +1,32 @@ +package org.bitcoins.core.protocol.ln.node + +import org.bitcoins.core.crypto.ECPublicKey +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.Factory +import scodec.bits.ByteVector + +/** + * `NodeId` is simply a wrapper for + * [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]]. + * This public key needs to be a + * 33 byte compressed secp256k1 public key. + */ +case class NodeId(pubKey: ECPublicKey) extends NetworkElement { + require(pubKey.isCompressed, s"Cannot create a nodeId from a public key that was not compressed ${pubKey.hex}") + + override def toString: String = pubKey.hex + + override def bytes: ByteVector = pubKey.bytes +} + +object NodeId extends Factory[NodeId] { + + def fromPubKey(pubKey: ECPublicKey): NodeId = { + NodeId(pubKey) + } + + override def fromBytes(bytes: ByteVector): NodeId = { + val pubKey = ECPublicKey.fromBytes(bytes) + fromPubKey(pubKey) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/routing/LnRoute.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/routing/LnRoute.scala new file mode 100644 index 0000000000..45ae9b0740 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/routing/LnRoute.scala @@ -0,0 +1,81 @@ +package org.bitcoins.core.protocol.ln.routing + +import java.math.BigInteger + +import org.bitcoins.core.crypto.ECPublicKey +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.protocol.ln.ShortChannelId +import org.bitcoins.core.protocol.ln.currency.MilliSatoshis +import org.bitcoins.core.protocol.ln.fee.{ FeeBaseMSat, FeeProportionalMillionths } +import org.bitcoins.core.util.BitcoinSUtil +import scodec.bits.ByteVector + +/** + * Indicates a node to route through with specific options on the Lightning Network + * For more details on these settings please see + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection BOLT2]] + */ +case class LnRoute( + pubkey: ECPublicKey, + shortChannelID: ShortChannelId, + feeBaseMsat: FeeBaseMSat, + feePropMilli: FeeProportionalMillionths, + cltvExpiryDelta: Short) extends NetworkElement { + + require(pubkey.isCompressed, s"Can only use a compressed public key in routing") + + override def bytes: ByteVector = { + + val cltvExpiryDeltaHex = BitcoinSUtil.encodeHex(cltvExpiryDelta) + + pubkey.bytes ++ + shortChannelID.bytes ++ + feeBaseMsat.bytes ++ + feePropMilli.bytes ++ + ByteVector.fromValidHex(cltvExpiryDeltaHex) + } +} + +object LnRoute { + + def fromBytes(bytes: ByteVector): LnRoute = { + val PUBKEY_LEN = 33 + val SHORT_CHANNEL_ID_LEN = 8 + val FEE_BASE_U32_LEN = 4 + val FEE_PROPORTIONAL_LEN = 4 + val CLTV_EXPIRTY_DELTA_LEN = 2 + + val TOTAL_LEN = PUBKEY_LEN + + SHORT_CHANNEL_ID_LEN + + FEE_BASE_U32_LEN + + FEE_PROPORTIONAL_LEN + + CLTV_EXPIRTY_DELTA_LEN + + require(bytes.length >= TOTAL_LEN, s"ByteVector must at least of length $TOTAL_LEN, got ${bytes.length}") + + val (pubKeyBytes, rest0) = bytes.splitAt(PUBKEY_LEN) + val pubKey = ECPublicKey.fromBytes(pubKeyBytes) + + val (shortChannelIdBytes, rest1) = rest0.splitAt(SHORT_CHANNEL_ID_LEN) + + val shortChannelId = ShortChannelId.fromBytes(shortChannelIdBytes) + + val (feeBaseU32Bytes, rest2) = rest1.splitAt(FEE_BASE_U32_LEN) + + val feeBaseU32 = UInt32.fromBytes(feeBaseU32Bytes) + val feeBase = feeBaseU32.toLong + val feeBaseMSat = FeeBaseMSat(MilliSatoshis(feeBase)) + + val (u32Bytes, rest3) = rest2.splitAt(FEE_PROPORTIONAL_LEN) + + val u32 = UInt32.fromBytes(u32Bytes) + val feeProportionalMillionths = FeeProportionalMillionths(u32) + + val (cltvExpiryDeltaBytes, _) = rest3.splitAt(CLTV_EXPIRTY_DELTA_LEN) + + val cltvExpiryDelta = new BigInteger(cltvExpiryDeltaBytes.toArray).shortValueExact + + LnRoute(pubKey, shortChannelId, feeBaseMSat, feeProportionalMillionths, cltvExpiryDelta) + } +} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/util/LnUtil.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/util/LnUtil.scala new file mode 100644 index 0000000000..33347919bf --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/util/LnUtil.scala @@ -0,0 +1,79 @@ +package org.bitcoins.core.protocol.ln.util + +import org.bitcoins.core.number.UInt5 +import org.bitcoins.core.util.Bech32 + +import scala.annotation.tailrec + +trait LnUtil { + + /** + * The formula for this calculation is as follows: + * Take the length of the Bech32 encoded input and divide it by 32. + * Take the quotient, and encode this value as Bech32. Take the remainder and encode this value as Bech32. + * Append these values to produce a valid Lighting Network `data_length` field. + * Please see + * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#examples Bolt11]] + * for examples. + */ + def createDataLength(bech32String: String): Vector[UInt5] = { + val u5s = Bech32.decodeStringToU5s(bech32String) + createDataLength(u5s) + } + + /** Creates the appropriate 10 bit vector for the data length of an element */ + def createDataLength(u5s: Vector[UInt5]): Vector[UInt5] = { + val len = u5s.size + val encodedNoPadding = encodeNumber(len) + + val encoded = { + if (encodedNoPadding.size == 1) { + UInt5.zero +: encodedNoPadding + } else { + encodedNoPadding + } + } + + val size = encoded.size + require(size == 2, s"data_length must be 2 uint5s, got $size") + + encoded + } + + def decodeDataLength(u5s: Vector[UInt5]): Long = { + require(u5s.length == 2, s"Data Length is required to be 10 bits, got ${u5s.length}") + decodeNumber(u5s) + } + + /** Returns a 5 bit bytevector with the encoded number for a LN invoice */ + @tailrec + final def encodeNumber(len: BigInt, accum: Vector[UInt5] = Vector.empty): Vector[UInt5] = { + val quotient = len / 32 + val remainder = UInt5(len % 32) + if (quotient >= 32) { + encodeNumber(quotient, remainder +: accum) + } else if (quotient == 0) { + remainder +: accum + } else { + val quo = UInt5.fromByte(quotient.toByte) + val v = Vector(quo, remainder) + v ++ accum + } + + } + + @tailrec + final def decodeNumber(vector: Vector[UInt5], accum: Long = 0): Long = { + + if (vector.isEmpty) accum + else if (vector.size == 1) { + decodeNumber(vector.tail, vector.head.toInt + accum) + } else { + val n = BigInt(32).pow(vector.size - 1) + val newAccum = vector.head.toBigInt * n + accum + decodeNumber(vector.tail, newAccum.toLong) + } + } +} + +object LnUtil extends LnUtil diff --git a/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala b/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala index 4a11537ddb..32c324cf2a 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala @@ -1,8 +1,8 @@ package org.bitcoins.core.protocol.script +import org.bitcoins.core.consensus.Consensus import org.bitcoins.core.crypto._ import org.bitcoins.core.protocol._ -import org.bitcoins.core.consensus.Consensus import org.bitcoins.core.script.bitwise.{ OP_EQUAL, OP_EQUALVERIFY } import org.bitcoins.core.script.constant.{ BytesToPushOntoStack, _ } import org.bitcoins.core.script.control.{ OP_ELSE, OP_ENDIF, OP_IF, OP_RETURN } @@ -10,7 +10,7 @@ import org.bitcoins.core.script.crypto.{ OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIF import org.bitcoins.core.script.locktime.{ OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY } import org.bitcoins.core.script.reserved.UndefinedOP_NOP import org.bitcoins.core.script.stack.{ OP_DROP, OP_DUP } -import org.bitcoins.core.serializers.script.{ ScriptParser } +import org.bitcoins.core.serializers.script.ScriptParser import org.bitcoins.core.util._ import scodec.bits.ByteVector @@ -587,13 +587,17 @@ object P2WPKHWitnessSPKV0 extends ScriptFactory[P2WPKHWitnessSPKV0] { asmBytes.size == 22 } + def fromHash(hash: Sha256Hash160Digest): P2WPKHWitnessSPKV0 = { + val pushop = BitcoinScriptUtil.calculatePushOp(hash.bytes) + fromAsm(Seq(OP_0) ++ pushop ++ Seq(ScriptConstant(hash.bytes))) + } + /** Creates a P2WPKH witness script pubkey */ def apply(pubKey: ECPublicKey): P2WPKHWitnessSPKV0 = { //https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#restrictions-on-public-key-type require(pubKey.isCompressed, s"Public key must be compressed to be used in a segwit script, see BIP143") val hash = CryptoUtil.sha256Hash160(pubKey.bytes) - val pushop = BitcoinScriptUtil.calculatePushOp(hash.bytes) - fromAsm(Seq(OP_0) ++ pushop ++ Seq(ScriptConstant(hash.bytes))) + fromHash(hash) } } @@ -619,11 +623,15 @@ object P2WSHWitnessSPKV0 extends ScriptFactory[P2WSHWitnessSPKV0] { asmBytes.size == 34 } + def fromHash(hash: Sha256Digest): P2WSHWitnessSPKV0 = { + val pushop = BitcoinScriptUtil.calculatePushOp(hash.bytes) + fromAsm(Seq(OP_0) ++ pushop ++ Seq(ScriptConstant(hash.bytes))) + } + def apply(spk: ScriptPubKey): P2WSHWitnessSPKV0 = { require(BitcoinScriptUtil.isOnlyCompressedPubKey(spk), s"Public key must be compressed to be used in a segwit script, see BIP143") val hash = CryptoUtil.sha256(spk.asmBytes) - val pushop = BitcoinScriptUtil.calculatePushOp(hash.bytes) - fromAsm(Seq(OP_0) ++ pushop ++ Seq(ScriptConstant(hash.bytes))) + fromHash(hash) } } diff --git a/core/src/main/scala/org/bitcoins/core/script/crypto/CryptoInterpreter.scala b/core/src/main/scala/org/bitcoins/core/script/crypto/CryptoInterpreter.scala index a840758f23..e823e3f8e1 100644 --- a/core/src/main/scala/org/bitcoins/core/script/crypto/CryptoInterpreter.scala +++ b/core/src/main/scala/org/bitcoins/core/script/crypto/CryptoInterpreter.scala @@ -1,12 +1,12 @@ package org.bitcoins.core.script.crypto +import org.bitcoins.core.consensus.Consensus import org.bitcoins.core.crypto._ import org.bitcoins.core.script.constant._ import org.bitcoins.core.script.control.{ ControlOperationsInterpreter, OP_VERIFY } import org.bitcoins.core.script.flag.ScriptFlagUtil import org.bitcoins.core.script.result._ import org.bitcoins.core.script.{ ScriptProgram, _ } -import org.bitcoins.core.consensus.Consensus import org.bitcoins.core.util.{ BitcoinSLogger, BitcoinScriptUtil, CryptoUtil } import scodec.bits.ByteVector diff --git a/core/src/main/scala/org/bitcoins/core/util/Bech32.scala b/core/src/main/scala/org/bitcoins/core/util/Bech32.scala new file mode 100644 index 0000000000..63fafc5ba0 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/util/Bech32.scala @@ -0,0 +1,239 @@ +package org.bitcoins.core.util + +import org.bitcoins.core.number.{UInt32, UInt5, UInt8} +import scodec.bits.{BitVector, ByteVector} + +import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} + +/** + * A abstract class representing basic utility functions of Bech32 + * For more information on Bech32 please seee BIP173 + * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki]] + */ +sealed abstract class Bech32 { + + private val generators: Vector[Long] = Vector( + UInt32("3b6a57b2").toLong, + UInt32("26508e6d").toLong, UInt32("1ea119fa").toLong, + UInt32("3d4233dd").toLong, UInt32("2a1462b3").toLong) + + /** + * Creates a checksum for the given byte vector according to + * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]] + */ + def createChecksum(u5s: Vector[UInt5]): Vector[UInt5] = { + val z = UInt5.zero + val polymod: Long = polyMod(u5s ++ Array(z, z, z, z, z, z)) ^ 1 + //[(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + val result: Vector[UInt5] = 0.until(6).map { i => + //((polymod >> five * (five - u)) & UInt8(31.toShort)) + UInt5((polymod >> 5 * (5 - i)) & 31) + }.toVector + result + } + + /** + * Expands the human readable part of a bech32 address as per + * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]] + */ + def hrpExpand(bytes: ByteVector): Vector[UInt5] = { + val x: ByteVector = bytes.map { b: Byte => + (b >> 5).toByte + } + val withZero: ByteVector = x ++ ByteVector.low(1) + + val y: ByteVector = bytes.map { char => + (char & 0x1f).toByte + } + val result = withZero ++ y + + UInt5.toUInt5s(result) + } + + def polyMod(bytes: Vector[UInt5]): Long = { + var chk: Long = 1 + bytes.foreach { v => + val b = chk >> 25 + //chk = (chk & 0x1ffffff) << 5 ^ v + chk = (chk & 0x1ffffff) << 5 ^ v.toLong + 0.until(5).foreach { i: Int => + //chk ^= GEN[i] if ((b >> i) & 1) else 0 + if (((b >> i) & 1) == 1) { + chk = chk ^ generators(i) + } + } + } + chk + } + + /** Checks if the possible human readable part follows + * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]] + * rules */ + def checkHrpValidity[T](hrp: String, f: String => Try[T]): Try[T] = { + @tailrec + def loop(remaining: List[Char], accum: Seq[UInt8], isLower: Boolean, isUpper: Boolean): Try[Seq[UInt8]] = remaining match { + case h :: t => + if (h < 33 || h > 126) { + Failure(new IllegalArgumentException("Invalid character range for hrp, got: " + hrp)) + } else if (isLower && isUpper) { + Failure(new IllegalArgumentException("HRP had mixed case, got: " + hrp)) + } else { + loop( + remaining = t, + accum = UInt8(h.toByte) +: accum, + isLower = h.isLower || isLower, + isUpper = h.isUpper || isUpper) + } + case Nil => + if (isLower && isUpper) { + Failure( + new IllegalArgumentException("HRP had mixed case, got: " + hrp)) + } else { + Success(accum.reverse) + } + } + + val isValid = + loop(hrp.toCharArray.toList, Nil, isLower = false, isUpper = false) + + isValid.flatMap { _ => + f(hrp.toLowerCase) + } + } + + /** + * Takes in the data portion of a bech32 address and decodes it to a byte array + * It also checks the validity of the data portion according to BIP173 + */ + def checkDataValidity(data: String): Try[Vector[UInt5]] = { + @tailrec + def loop(remaining: List[Char], + accum: Vector[UInt5], + hasUpper: Boolean, + hasLower: Boolean): Try[Vector[UInt5]] = remaining match { + case Nil => Success(accum.reverse) + case h :: t => + if (!Bech32.charset.contains(h.toLower)) { + Failure( + new IllegalArgumentException( + "Invalid character in data of bech32 address, got: " + h)) + } else { + if ((h.isUpper && hasLower) || (h.isLower && hasUpper)) { + Failure( + new IllegalArgumentException( + "Cannot have mixed case for bech32 address")) + } else { + val byte = Bech32.charset.indexOf(h.toLower).toByte + + if (byte >= 0 && byte < 32) { + loop(remaining = t, + accum = UInt5.fromByte(byte) +: accum, + hasUpper = h.isUpper || hasUpper, + hasLower = h.isLower || hasLower) + } else { + Failure( + new IllegalArgumentException( + s"Byte was not in a valid range, got $byte")) + } + + } + } + } + val payload: Try[Vector[UInt5]] = loop(data.toCharArray.toList, Vector.empty, + hasUpper = false, hasLower = false) + + payload + } + + /** Encodes a bitvector to a bech32 string */ + def encodeBitVec(bitVec: BitVector): String = { + @tailrec + def loop(remaining: BitVector, accum: Vector[UInt5]): Vector[UInt5] = { + if (remaining.length > 5) { + + val u5 = UInt5(remaining.take(5).toByte()) + + val newRemaining = remaining.slice(5, remaining.size) + + loop(newRemaining, accum.:+(u5)) + + } else { + + val u5 = UInt5(remaining.toByte()) + + accum.:+(u5) + } + } + + val u5s = loop(bitVec, Vector.empty) + + encode5bitToString(u5s) + } + + /** + * Converts a byte vector to 5bit vector + * and then serializes to bech32 + */ + def encode8bitToString(bytes: ByteVector): String = { + val vec = UInt8.toUInt8s(bytes) + encode8bitToString(vec) + } + + /** + * Converts a byte vector to 5bit vector + * and then serializes to bech32 + */ + def encode8bitToString(bytes: Vector[UInt8]): String = { + val b = from8bitTo5bit(bytes) + encode5bitToString(b) + } + + /** Takes a bech32 5bit array and encodes it to a string */ + def encode5bitToString(b: Vector[UInt5]): String = { + b.map(b => Bech32.charset(b.toInt)).mkString + } + + /** Converts a byte vector from 8bits to 5bits */ + def from8bitTo5bit(bytes: ByteVector): Vector[UInt5] = { + val u8s = UInt8.toUInt8s(bytes) + val u5s = NumberUtil.convertUInt8sToUInt5s(u8s) + u5s + } + + /** Converts a byte array from 8bits to base 5 bits */ + def from8bitTo5bit(u8s: Vector[UInt8]): Vector[UInt5] = { + val bytes = UInt8.toBytes(u8s) + from8bitTo5bit(bytes) + } + + /** Decodes a byte array from 5bits to base 8bits */ + def from5bitTo8bit(b: Vector[UInt5]): Vector[UInt8] = { + NumberUtil.convertUInt5sToUInt8(b) + } + + /** Assumes we are given a valid bech32 string */ + def decodeStringToU5s(str: String): Vector[UInt5] = { + str.map { char => + UInt5(Bech32.charset.indexOf(char)) + }.toVector + } + +} + +object Bech32 extends Bech32 { + + /** Separator used to separate the hrp & data parts of a bech32 addr */ + val separator = '1' + + /* + * See [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]] + * for more + */ + val charset: Vector[Char] = Vector( + 'q', 'p', 'z', 'r', 'y', '9', 'x', '8', + 'g', 'f', '2', 't', 'v', 'd', 'w', '0', + 's', '3', 'j', 'n', '5', '4', 'k', 'h', + 'c', 'e', '6', 'm', 'u', 'a', '7', 'l') +} diff --git a/core/src/main/scala/org/bitcoins/core/util/BitcoinSUtil.scala b/core/src/main/scala/org/bitcoins/core/util/BitcoinSUtil.scala index cde11112bd..7c6f9c6c21 100644 --- a/core/src/main/scala/org/bitcoins/core/util/BitcoinSUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/BitcoinSUtil.scala @@ -39,11 +39,8 @@ trait BitcoinSUtil { } def encodeHex(short: Short): String = { - val hex = short.toHexString.length % 2 match { - case 1 => "0" + short.toHexString - case _: Int => short.toHexString - } - addPadding(4, hex) + val bytes = ByteVector.fromShort(short) + encodeHex(bytes) } def encodeHex(bigInt: BigInt): String = BitcoinSUtil.encodeHex(ByteVector(bigInt.toByteArray)) diff --git a/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala b/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala index be350aa062..2a443a1d22 100644 --- a/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala @@ -1,11 +1,13 @@ package org.bitcoins.core.util +import java.math.BigInteger import java.security.MessageDigest import org.bitcoins.core.crypto._ import org.bouncycastle.crypto.digests.{ RIPEMD160Digest, SHA512Digest } import org.bouncycastle.crypto.macs.HMac import org.bouncycastle.crypto.params.KeyParameter +import org.bouncycastle.math.ec.ECPoint import scodec.bits.ByteVector /** @@ -73,6 +75,43 @@ trait CryptoUtil { hmac512.doFinal(output, 0) ByteVector(output) } + + /** + * + * @param x x coordinate + * @return a tuple (p1, p2) where p1 and p2 are points on the curve and p1.x = p2.x = x + * p1.y is even, p2.y is odd + */ + def recoverPoint(x: BigInteger): (ECPoint, ECPoint) = { + val curve = CryptoParams.curve.getCurve() + val x1 = curve.fromBigInteger(x) + val square = x1.square().add(curve.getA).multiply(x1).add(curve.getB) + val y1 = square.sqrt() + val y2 = y1.negate() + val R1 = curve.createPoint(x1.toBigInteger, y1.toBigInteger).normalize() + val R2 = curve.createPoint(x1.toBigInteger, y2.toBigInteger).normalize() + if (y1.testBitZero()) (R2, R1) else (R1, R2) + } + + /** + * Recover public keys from a signature and the message that was signed. This method will return 2 public keys, and the signature + * can be verified with both, but only one of them matches that private key that was used to generate the signature. + * + * @param signature signature + * @param message message that was signed + * @return a (pub1, pub2) tuple where pub1 and pub2 are candidates public keys. If you have the recovery id then use + * pub1 if the recovery id is even and pub2 if it is odd + */ + def recoverPublicKey(signature: ECDigitalSignature, message: ByteVector): (ECPublicKey, ECPublicKey) = { + val curve = CryptoParams.curve + val (r, s) = (signature.r.bigInteger, signature.s.bigInteger) + val m = new BigInteger(1, message.toArray) + + val (p1, p2) = recoverPoint(r) + val Q1 = (p1.multiply(s).subtract(curve.getG.multiply(m))).multiply(r.modInverse(curve.getN)) + val Q2 = (p2.multiply(s).subtract(curve.getG.multiply(m))).multiply(r.modInverse(curve.getN)) + (ECPublicKey.fromPoint(Q1), ECPublicKey.fromPoint(Q2)) + } } object CryptoUtil extends CryptoUtil diff --git a/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala b/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala index da61ffb689..a8c90f5ffb 100644 --- a/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala @@ -2,7 +2,7 @@ package org.bitcoins.core.util import java.math.BigInteger -import org.bitcoins.core.number.{ UInt32, UInt8 } +import org.bitcoins.core.number._ import scodec.bits.ByteVector import scala.math.BigInt @@ -59,26 +59,33 @@ trait NumberUtil extends BitcoinSLogger { /** Converts a hex string to a [[Long]]. */ def toLong(hex: String): Long = toLong(BitcoinSUtil.decodeHex(hex)) - /** Converts a sequence uint8 'from' base to 'to' base */ - def convertUInt8s(data: Seq[UInt8], from: UInt32, to: UInt32, pad: Boolean): Try[Seq[UInt8]] = { + /** + * + * Converts a sequence uint8 `from` base to `to` base + * @param pad + * @param f + */ + def convert[To <: Number[To]](data: Vector[UInt8], from: UInt32, to: UInt32, pad: Boolean, f: UInt8 => To): Try[Vector[To]] = { var acc: UInt32 = UInt32.zero var bits: UInt32 = UInt32.zero - var ret: Seq[UInt8] = Nil + val ret = Vector.newBuilder[To] val maxv: UInt32 = (UInt32.one << to) - UInt32.one val eight = UInt32(8) + val fromU8 = UInt8(from.toLong.toShort) if (from > eight || to > eight) { Failure(new IllegalArgumentException("Can't have convert bits 'from' or 'to' parameter greater than 8")) } else { - data.map { h => - if ((h >> UInt8(from.toLong.toShort)) != UInt8.zero) { + data.map { h: UInt8 => + if ((h >> fromU8.toInt) != UInt8.zero) { Failure(new IllegalArgumentException("Invalid input for bech32: " + h)) } else { acc = (acc << from) | UInt32(h.toLong) bits = bits + from while (bits >= to) { bits = bits - to - val r: Seq[UInt8] = Seq(UInt8((((acc >> bits) & maxv).toInt.toShort))) - ret = ret ++ r + val newBase = UInt8(((acc >> bits) & maxv).toInt.toShort) + val r: To = f(newBase) + ret.+=(r) } } } @@ -86,17 +93,34 @@ trait NumberUtil extends BitcoinSLogger { if (pad) { if (bits > UInt32.zero) { val r: Long = ((acc << (to - bits) & maxv)).toLong - ret = ret ++ Seq(UInt8(r.toShort)) + ret.+=(f(UInt8(r.toShort))) } } else if (bits >= from || ((acc << (to - bits)) & maxv) != UInt8.zero) { Failure(new IllegalArgumentException("Invalid padding in encoding")) } - Success(ret) + Success(ret.result()) } } - def convertBytes(data: ByteVector, from: UInt32, to: UInt32, pad: Boolean): Try[Seq[UInt8]] = { - convertUInt8s(UInt8.toUInt8s(data), from, to, pad) + def convertBytes[T <: Number[T]](data: ByteVector, from: UInt32, to: UInt32, pad: Boolean, f: Byte => T): Try[Vector[T]] = { + val wrapperF: UInt8 => T = { u8: UInt8 => + f(UInt8.toByte(u8)) + } + convert[T](UInt8.toUInt8s(data), from, to, pad, wrapperF) + } + + def convertUInt8sToUInt5s(u8s: Vector[UInt8]): Vector[UInt5] = { + val f = { u8: UInt8 => UInt5.fromByte(UInt8.toByte(u8)) } + val u8sTry = NumberUtil.convert[UInt5](u8s, UInt32(8), UInt32(5), pad = true, f) + u8sTry.get + + } + + def convertUInt5sToUInt8(u5s: Vector[UInt5]): Vector[UInt8] = { + val u8s = u5s.map(_.toUInt8) + val u8sTry = NumberUtil.convert[UInt8](u8s, UInt32(5), UInt32(8), pad = false, { u8: UInt8 => u8 }) + //should always be able to convert from uint5 => uint8 + u8sTry.get } } diff --git a/eclair-rpc/README.md b/eclair-rpc/README.md new file mode 100644 index 0000000000..289b8e3033 --- /dev/null +++ b/eclair-rpc/README.md @@ -0,0 +1,27 @@ +# Bitcoin-s Eclair RPC client + +This is a RPC client for [Eclair](https://github.com/acinq/eclair). It assumes that a bitcoind instance is running. + +Currently this RPC client is written for the latest official version of eclair which is [v0.2-beta8](https://github.com/ACINQ/eclair/releases/tag/v0.2-beta8) + +## Configuration eclair + +Please see the configuration secion of the +[Eclair README](https://github.com/acinq/eclair#configuring-eclair). + + + +## Starting the jar + +You need to download the jar from the [Eclair GitHub](https://github.com/ACINQ/eclair/releases/tag/v0.2-beta8). + + +To run Eclair you can use this command: + +```bash +$ java -jar eclair-node-0.2-beta8-52821b8.jar & +``` + +Alternatively you can set the `ECLAIR_PATH` env variable and then you can start Eclair with the `start` method on `EclairRpcClient`. + +**YOU NEED TO SET `ECLAIR_PATH` CORRECTLY TO BE ABLE TO RUN THE UNIT TESTS** diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala new file mode 100644 index 0000000000..7a43a51215 --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala @@ -0,0 +1,73 @@ +package org.bitcoins.eclair.rpc.api + +import org.bitcoins.core.crypto.Sha256Digest +import org.bitcoins.core.currency.CurrencyUnit +import org.bitcoins.core.protocol.ln.LnInvoice +import org.bitcoins.core.protocol.ln.channel.{ ChannelId, FundedChannelId } +import org.bitcoins.core.protocol.ln.currency.{ LnCurrencyUnit, MilliSatoshis } +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.core.protocol.script.ScriptPubKey +import org.bitcoins.core.wallet.fee.SatoshisPerByte +import org.bitcoins.eclair.rpc.json._ +import org.bitcoins.eclair.rpc.network.{ NodeUri } + +import scala.concurrent.{ ExecutionContext, Future } + +trait EclairApi { + + def allChannels(): Future[Vector[ChannelDesc]] + + def allNodes(): Future[Vector[NodeInfo]] + + def allUpdates(nodeIdOpt: Option[NodeId]): Future[Vector[ChannelUpdate]] + + def channels(nodeId: NodeId): Future[Vector[ChannelInfo]] + + def channel(id: ChannelId): Future[ChannelResult] + + def checkInvoice(invoice: LnInvoice): Future[PaymentRequest] + + def checkPayment(invoiceOrHash: Either[LnInvoice, Sha256Digest]): Future[Boolean] + + def connect(nodeURI: NodeUri): Future[String] + + def close(id: ChannelId, spk: ScriptPubKey): Future[String] + + def findRoute(nodeId: NodeId ): Future[Vector[String]] + + def findRoute(invoice: LnInvoice): Future[Vector[String]] = { + findRoute(nodeId = invoice.nodeId) + } + + def forceClose(id: ChannelId): Future[String] + + def getInfo: Future[GetInfoResult] + + def getNodeURI: Future[NodeUri] + + def getPeers: Future[Vector[PeerInfo]] + + def isConnected(nodeId: NodeId): Future[Boolean] + + def open( + nodeId: NodeId, + fundingSatoshis: CurrencyUnit, + pushMsat: Option[MilliSatoshis], + feerateSatPerByte: Option[SatoshisPerByte], + channelFlags: Option[Byte]): Future[FundedChannelId] + + def nodeId()(implicit ec: ExecutionContext): Future[NodeId] = { + getNodeURI.map(_.nodeId) + } + def receive(amountMsat: LnCurrencyUnit, description: String): Future[LnInvoice] + + def receive( + amountMsat: Option[LnCurrencyUnit], + description: Option[String], + expirySeconds: Option[Long]): Future[LnInvoice] + + def send(paymentRequest: LnInvoice): Future[PaymentResult] + + def send(invoice: LnInvoice, amount: LnCurrencyUnit): Future[PaymentResult] + +} \ No newline at end of file diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala new file mode 100644 index 0000000000..309528299e --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala @@ -0,0 +1,535 @@ +package org.bitcoins.eclair.rpc.client + +import java.io.File +import java.util.UUID + +import akka.actor.ActorSystem +import akka.http.javadsl.model.headers.HttpCredentials +import akka.http.scaladsl.Http +import akka.http.scaladsl.model._ +import akka.stream.ActorMaterializer +import akka.util.ByteString +import org.bitcoins.core.crypto.Sha256Digest +import org.bitcoins.core.currency.CurrencyUnit +import org.bitcoins.core.protocol.ln.LnInvoice +import org.bitcoins.core.protocol.ln.channel.{ChannelId, FundedChannelId} +import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, MilliSatoshis} +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.core.protocol.script.ScriptPubKey +import org.bitcoins.core.util.BitcoinSUtil +import org.bitcoins.core.wallet.fee.SatoshisPerByte +import org.bitcoins.eclair.rpc.api.EclairApi +import org.bitcoins.eclair.rpc.config.EclairInstance +import org.bitcoins.eclair.rpc.json._ +import org.bitcoins.eclair.rpc.network.{NodeUri, PeerState} +import org.slf4j.LoggerFactory +import play.api.libs.json._ + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, ExecutionContext, Future, Promise} +import scala.sys.process._ +import scala.util.{Failure, Properties, Success, Try} + +class EclairRpcClient(val instance: EclairInstance)(implicit system: ActorSystem) extends EclairApi { + import JsonReaders._ + + private val resultKey = "result" + private val errorKey = "error" + implicit val m = ActorMaterializer.create(system) + implicit val ec: ExecutionContext = m.executionContext + private val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) + + def getDaemon: EclairInstance = instance + + override def allChannels: Future[Vector[ChannelDesc]] = { + eclairCall[Vector[ChannelDesc]]("allchannels") + } + + override def allNodes: Future[Vector[NodeInfo]] = { + eclairCall[Vector[NodeInfo]]("allnodes") + } + + override def allUpdates( + nodeIdOpt: Option[NodeId]): Future[Vector[ChannelUpdate]] = { + val params = if (nodeIdOpt.isEmpty) List.empty else List(JsString(nodeIdOpt.get.toString)) + eclairCall[Vector[ChannelUpdate]]("allupdates", params) + } + + def allUpdates: Future[Vector[ChannelUpdate]] = allUpdates(nodeIdOpt = None) + + def allUpdates(nodeId: NodeId): Future[Vector[ChannelUpdate]] = + allUpdates(Some(nodeId)) + + override def channel(channelId: ChannelId): Future[ChannelResult] = { + eclairCall[ChannelResult]("channel", List(JsString(channelId.hex))) + } + + private def channels(nodeId: Option[NodeId]): Future[Vector[ChannelInfo]] = { + val params = if (nodeId.isEmpty) List.empty else List(JsString(nodeId.get.toString)) + + eclairCall[Vector[ChannelInfo]]("channels", params) + } + + def channels(): Future[Vector[ChannelInfo]] = channels(nodeId = None) + + override def channels(nodeId: NodeId): Future[Vector[ChannelInfo]] = + channels(Some(nodeId)) + + override def checkInvoice(invoice: LnInvoice): Future[PaymentRequest] = { + eclairCall[PaymentRequest]("checkinvoice", List(JsString(invoice.toString))) + } + + override def checkPayment(invoiceOrHash: Either[LnInvoice, Sha256Digest]): Future[Boolean] = { + + val string = { + if (invoiceOrHash.isLeft) { + invoiceOrHash.left.get.toString + } else { + invoiceOrHash.right.get.hex + } + } + + eclairCall[Boolean]("checkpayment", List(JsString(string))) + } + + private def close( + channelId: ChannelId, + scriptPubKey: Option[ScriptPubKey]): Future[String] = { + val params = + if (scriptPubKey.isEmpty) { + List(JsString(channelId.hex)) + } else { + + val asmHex = BitcoinSUtil.encodeHex(scriptPubKey.get.asmBytes) + + List(JsString(channelId.hex), JsString(asmHex)) + } + + eclairCall[String]("close", params) + } + + def close(channelId: ChannelId): Future[String] = close(channelId, scriptPubKey = None) + + override def close(channelId: ChannelId, scriptPubKey: ScriptPubKey): Future[String] = { + close(channelId, Some(scriptPubKey)) + } + + def connect(nodeId: NodeId, host: String, port: Int): Future[String] = { + val uri = NodeUri(nodeId, host, port) + connect(uri) + } + + override def connect(uri: NodeUri): Future[String] = { + logger.info(s"Connecting to $uri") + eclairCall[String]("connect", List(JsString(uri.toString))) + } + + override def findRoute(nodeId: NodeId): Future[Vector[String]] = { + eclairCall[Vector[String]]("findroute", List(JsString(nodeId.hex))) + } + + override def forceClose(channelId: ChannelId): Future[String] = { + eclairCall[String]("forceclose", List(JsString(channelId.hex))) + } + + override def getInfo: Future[GetInfoResult] = { + val result = eclairCall[GetInfoResult]("getinfo") + result + } + + override def getNodeURI: Future[NodeUri] = { + getInfo.map { info => + val id = info.nodeId + val host = instance.uri.getHost + val port = instance.uri.getPort + NodeUri(nodeId = id, host = host, port = port) + } + } + + def help: Future[Vector[String]] = { + eclairCall[Vector[String]]("help") + } + + override def isConnected(nodeId: NodeId): Future[Boolean] = { + getPeers.map(_.exists(p => p.nodeId == nodeId && p.state == PeerState.CONNECTED)) + } + + override def open( + nodeId: NodeId, + fundingSatoshis: CurrencyUnit, + pushMsat: Option[MilliSatoshis], + feerateSatPerByte: Option[SatoshisPerByte], + channelFlags: Option[Byte]): Future[FundedChannelId] = { + val num = pushMsat.getOrElse(MilliSatoshis.zero).toBigDecimal + val pushMsatJson = JsNumber(num) + val sat = fundingSatoshis.satoshis.toBigDecimal + + val params = { + if (feerateSatPerByte.isEmpty) { + List(JsString(nodeId.toString), JsNumber(sat), pushMsatJson) + } else if (channelFlags.isEmpty) { + List( + JsString(nodeId.toString), + JsNumber(sat), + pushMsatJson, + JsNumber(feerateSatPerByte.get.toLong)) + } else { + List( + JsString(nodeId.toString), + JsNumber(sat), + pushMsatJson, + JsNumber(feerateSatPerByte.get.toLong), + JsString(channelFlags.toString)) + } + } + + //this is unfortunately returned in this format + //created channel 30bdf849eb9f72c9b41a09e38a6d83138c2edf332cb116dd7cf0f0dfb66be395 + val call = eclairCall[String]("open", params) + + //let's just return the chanId + val chanIdF = call.map(_.split(" ").last) + + chanIdF.map(FundedChannelId.fromHex) + } + + def open(nodeId: NodeId, fundingSatoshis: CurrencyUnit): Future[FundedChannelId] = { + open(nodeId, fundingSatoshis, pushMsat = None, feerateSatPerByte = None, channelFlags = None) + } + + def open( + nodeId: NodeId, + fundingSatoshis: CurrencyUnit, + pushMsat: MilliSatoshis): Future[FundedChannelId] = { + open(nodeId, fundingSatoshis, Some(pushMsat), feerateSatPerByte = None, channelFlags = None) + } + + def open( + nodeId: NodeId, + fundingSatoshis: CurrencyUnit, + pushMsat: MilliSatoshis, + feerateSatPerByte: SatoshisPerByte): Future[FundedChannelId] = { + open(nodeId, fundingSatoshis, Some(pushMsat), Some(feerateSatPerByte), channelFlags = None) + } + + def open( + nodeId: NodeId, + fundingSatoshis: CurrencyUnit, + pushMsat: MilliSatoshis = MilliSatoshis.zero, + feerateSatPerByte: SatoshisPerByte, + channelFlags: Byte): Future[FundedChannelId] = { + open( + nodeId, + fundingSatoshis, + Some(pushMsat), + Some(feerateSatPerByte), + Some(channelFlags)) + } + + def open( + nodeId: NodeId, + fundingSatoshis: CurrencyUnit, + feerateSatPerByte: SatoshisPerByte, + channelFlags: Byte): Future[FundedChannelId] = { + open( + nodeId, + fundingSatoshis, + pushMsat = None, + Some(feerateSatPerByte), + Some(channelFlags)) + } + + override def getPeers: Future[Vector[PeerInfo]] = { + eclairCall[Vector[PeerInfo]]("peers") + } + + + /** A way to generate a [[org.bitcoins.core.protocol.ln.LnInvoice LnInvoice]] + * with eclair. + * @param amountMsat the amount to be encoded in the invoice + * @param description meta information about the invoice + * @param expirySeconds when the invoice expires + * @return + */ + override def receive( + amountMsat: Option[LnCurrencyUnit], + description: Option[String], + expirySeconds: Option[Long]): Future[LnInvoice] = { + val msat = amountMsat.map(_.toMSat) + + val params = { + if (amountMsat.isEmpty) { + List(JsString(description.getOrElse(""))) + } else { + val amt = JsNumber(msat.get.toBigDecimal) + if (expirySeconds.isEmpty) { + List(amt, JsString(description.getOrElse(""))) + } else { + List( + amt, + JsString(description.getOrElse("")), + JsNumber(expirySeconds.get)) + } + } + } + + val serializedF = eclairCall[String]("receive", params) + + serializedF.flatMap { str => + val invoiceTry = LnInvoice.fromString(str) + invoiceTry match { + case Success(i) => + + //register a monitor for when the payment is received + registerPaymentMonitor(i) + + Future.successful(i) + case Failure(err) => + Future.failed(err) + } + } + } + + /** + * Pings eclair every second to see if a invoice has been paid + * If the invoice has bene paid, we publish a + * [[org.bitcoins.eclair.rpc.json.PaymentSucceeded PaymentSucceeded]] + * event to the [[akka.actor.ActorSystem ActorSystem]]'s + * [[akka.event.EventStream ActorSystem.eventStream]] + * + * If your application is interested in listening for payments, + * you need to subscribe to the even stream and listen for a + * [[org.bitcoins.eclair.rpc.json.PaymentSucceeded PaymentSucceeded]] + * case class. You also need to check the + * payment hash is the hash you expected + */ + private def registerPaymentMonitor(invoice: LnInvoice)(implicit system: ActorSystem): Unit = { + + val p: Promise[Unit] = Promise[Unit]() + + val runnable = new Runnable() { + + override def run(): Unit = { + val isPaidF = checkPayment(Left(invoice)) + + //register callback that publishes a payment to our actor system's + //event stream, + isPaidF.map { isPaid: Boolean => + + if (!isPaid) { + //do nothing since the invoice has not been paid yet + () + } else { + //invoice has been paid, let's publish to event stream + //so subscribers so the even stream can see that a payment + //was received + //we need to create a `PaymentSucceeded` + val ps = PaymentSucceeded( + amountMsat = invoice.amount.get.toMSat, + paymentHash = invoice.lnTags.paymentHash.hash, + paymentPreimage = "", + route = JsArray.empty) + system.eventStream.publish(ps) + + //complete the promise so the runnable will be canceled + p.success(()) + + () + } + } + + () + } + } + + val cancellable = system.scheduler.schedule(1.seconds, 1.seconds, runnable) + + p.future.map(_ => cancellable.cancel()) + + () + } + + def receive(): Future[LnInvoice] = + receive(amountMsat = None, description = None, expirySeconds = None) + + def receive(description: String): Future[LnInvoice] = + receive(amountMsat = None, Some(description), expirySeconds = None) + + override def receive(amountMsat: LnCurrencyUnit, description: String): Future[LnInvoice] = + receive(Some(amountMsat), Some(description), expirySeconds = None) + + def receive( + amountMsat: LnCurrencyUnit, + description: String, + expirySeconds: Long): Future[LnInvoice] = + receive(Some(amountMsat), Some(description), Some(expirySeconds)) + + def receive(amountMsat: LnCurrencyUnit): Future[LnInvoice] = + receive(Some(amountMsat), description = None, expirySeconds = None) + + def receive(amountMsat: LnCurrencyUnit, expirySeconds: Long): Future[LnInvoice] = + receive(Some(amountMsat), description = None, Some(expirySeconds)) + + def send( + amountMsat: LnCurrencyUnit, + paymentHash: Sha256Digest, + nodeId: NodeId): Future[PaymentResult] = { + eclairCall[PaymentResult]( + "send", + List(JsNumber(amountMsat.toPicoBitcoinDecimal), JsString(paymentHash.hex), JsString(nodeId.toString))) + } + + private def send( + invoice: LnInvoice, + amountMsat: Option[LnCurrencyUnit]): Future[PaymentResult] = { + + val params = { + if (amountMsat.isEmpty) { + List(JsString(invoice.toString)) + } else { + List(JsString(invoice.toString), JsNumber(amountMsat.get.toPicoBitcoinDecimal)) + } + } + + eclairCall[PaymentResult]("send", params) + } + + def send(invoice: LnInvoice): Future[PaymentResult] = send(invoice, None) + + def send(invoice: LnInvoice, amountMsat: LnCurrencyUnit): Future[PaymentResult] = + send(invoice, Some(amountMsat)) + + def updateRelayFee( + channelId: ChannelId, + feeBaseMsat: LnCurrencyUnit, + feeProportionalMillionths: Long): Future[String] = { + eclairCall[String]( + "updaterelayfee", + List( + JsString(channelId.hex), + JsNumber(feeBaseMsat.toPicoBitcoinDecimal), + JsNumber(feeProportionalMillionths))) + } + + // TODO: channelstats, audit, networkfees? + // TODO: Add types + + private def eclairCall[T]( + command: String, + parameters: List[JsValue] = List.empty)( + implicit + reader: Reads[T]): Future[T] = { + val request = buildRequest(getDaemon, command, JsArray(parameters)) + val responseF = sendRequest(request) + + val payloadF: Future[JsValue] = responseF.flatMap(getPayload) + payloadF.map { payload => + val validated: JsResult[T] = (payload \ resultKey).validate[T] + val parsed: T = parseResult(validated, payload) + parsed + } + } + + case class RpcError(code: Int, message: String) + implicit val rpcErrorReads: Reads[RpcError] = Json.reads[RpcError] + + private def parseResult[T](result: JsResult[T], json: JsValue): T = { + result match { + case res: JsSuccess[T] => + res.value + case res: JsError => + (json \ errorKey).validate[RpcError] match { + case err: JsSuccess[RpcError] => + logger.error(s"Error ${err.value.code}: ${err.value.message}") + throw new RuntimeException( + s"Error ${err.value.code}: ${err.value.message}") + case _: JsError => + logger.error(JsError.toJson(res).toString()) + throw new IllegalArgumentException( + s"Could not parse JsResult: ${(json \ resultKey).get}") + } + } + } + + private def getPayload(response: HttpResponse): Future[JsValue] = { + val payloadF = response.entity.dataBytes.runFold(ByteString.empty)(_ ++ _) + + payloadF.map { payload => + val parsed: JsValue = Json.parse(payload.decodeString(ByteString.UTF_8)) + parsed + } + } + + private def sendRequest(req: HttpRequest): Future[HttpResponse] = { + val respF = Http(m.system).singleRequest(req) + respF + } + + private def buildRequest(instance: EclairInstance, methodName: String, params: JsArray): HttpRequest = { + val uuid = UUID.randomUUID().toString + + val obj: JsObject = JsObject( + Map( + "method" -> JsString(methodName), + "params" -> params, + "id" -> JsString(uuid))) + + val uri = instance.rpcUri.toString + // Eclair doesn't use a username + val username = "" + val password = instance.authCredentials.password + HttpRequest( + method = HttpMethods.POST, + uri, + entity = + HttpEntity(ContentTypes.`application/json`, obj.toString)) + .addCredentials( + HttpCredentials.createBasicHttpCredentials(username, password)) + } + + private def pathToEclairJar: String = { + val path = Properties + .envOrNone("ECLAIR_PATH") + .getOrElse(throw new RuntimeException( + List("Environment variable ECLAIR_PATH is not set!", + "This needs to be set to the directory containing the Eclair Jar" + ).mkString(" "))) + + val eclairV = "/eclair-node-0.2-beta8-52821b8.jar" + val fullPath = path + eclairV + + val jar = new File(fullPath) + if (jar.exists) { + fullPath + } else { + throw new RuntimeException(s"Could not Eclair Jar at location $fullPath") + } + } + + private var process: Option[Process] = None + + def start(): Unit = { + + if (process.isEmpty) { + val p = Process(s"java -jar -Declair.datadir=${instance.authCredentials.datadir.get} $pathToEclairJar &") + val result = p.run() + logger.info(s"Starting eclair with datadir ${instance.authCredentials.datadir.get}") + + process = Some(result) + () + } else { + logger.info(s"Eclair was already started!") + () + } + + } + + def isStarted(): Boolean = { + val t = Try(Await.result(getInfo, 1.second)) + t.isSuccess + } + + def stop(): Option[Unit] = { + process.map(_.destroy()) + } +} diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/ConfigUtil.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/ConfigUtil.scala new file mode 100644 index 0000000000..1f85d9fb67 --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/ConfigUtil.scala @@ -0,0 +1,14 @@ +package org.bitcoins.eclair.rpc.config + +import com.typesafe.config.Config + +import scala.util.{ Failure, Success, Try } + +object ConfigUtil { + def getIntOrElse(config: Config, path: String, default: Int): Int = { + Try(config.getInt(path)) match { + case Success(num) => num + case Failure(_) => default + } + } +} diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairAuthCredentials.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairAuthCredentials.scala new file mode 100644 index 0000000000..73db0fbeae --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairAuthCredentials.scala @@ -0,0 +1,103 @@ +package org.bitcoins.eclair.rpc.config + +import java.io.File + +import com.typesafe.config.{ Config, ConfigFactory } +import org.bitcoins.rpc.config.BitcoindAuthCredentials + +sealed trait EclairAuthCredentials { + + /** The directory where our `eclair.conf` file is located */ + def datadir: Option[File] + + def bitcoinAuthOpt: Option[BitcoindAuthCredentials] + + /** `rpcusername` field in our `bitcoin.conf` file */ + def bitcoinUsername: Option[String] = { + bitcoinAuthOpt.map(_.username) + } + + /** `rpcpassword` field in our `bitcoin.conf` file */ + def bitcoinPassword: Option[String] = { + bitcoinAuthOpt.map(_.password) + } + + /** `rpcport` field in our `bitcoin.conf` file */ + def bitcoinRpcPort: Option[Int] = { + bitcoinAuthOpt.map(_.rpcPort) + } + + /** `eclair.api.password` field in our `eclair.conf` file */ + def password: String + + /** The port for eclair's rpc client */ + def port: Int + + def copyWithDatadir(datadir: File): EclairAuthCredentials = { + EclairAuthCredentials( + password = password, + bitcoinAuthOpt = bitcoinAuthOpt, + port = port, + datadir = Some(datadir)) + } +} + +object EclairAuthCredentials { + private case class AuthCredentialsImpl( + password: String, + bitcoinAuthOpt: Option[BitcoindAuthCredentials], + port: Int, + datadir: Option[File]) extends EclairAuthCredentials + + def apply( + password: String, + bitcoinAuthOpt: Option[BitcoindAuthCredentials], + port: Int): EclairAuthCredentials = { + EclairAuthCredentials(password, bitcoinAuthOpt, port, None) + } + + def apply( + password: String, + bitcoinAuthOpt: Option[BitcoindAuthCredentials], + port: Int, + datadir: Option[File]): EclairAuthCredentials = { + AuthCredentialsImpl(password, bitcoinAuthOpt, port, datadir) + } + + def fromDatadir(datadir: File): EclairAuthCredentials = { + val confFile = new File(datadir.getAbsolutePath + "/eclair.conf") + val config = ConfigFactory.parseFile(confFile) + val auth = fromConfig(config) + auth.copyWithDatadir(datadir = datadir) + } + + /** + * Parses a [[com.typesafe.config.Config Config]] in the format of this + * [[https://github.com/ACINQ/eclair/blob/master/eclair-core/src/main/resources/reference.conf sample reference.conf]] + * file to a + * [[org.bitcoins.eclair.rpc.config.EclairAuthCredentials EclairAuthCredentials]] + */ + def fromConfig(config: Config): EclairAuthCredentials = { + + val bitcoindUsername = config.getString("eclair.bitcoind.rpcuser") + val bitcoindPassword = config.getString("eclair.bitcoind.rpcpassword") + val bitcoindRpcPort = config.getInt("eclair.bitcoind.rpcport") + + //does eclair not have a username field?? + val password = config.getString("eclair.api.password") + val eclairRpcPort = ConfigUtil.getIntOrElse(config, "eclair.api.port", 8080) + + val bitcoindAuth = { + BitcoindAuthCredentials( + username = bitcoindUsername, + password = bitcoindPassword, + rpcPort = bitcoindRpcPort) + } + + EclairAuthCredentials( + password = password, + bitcoinAuthOpt = Some(bitcoindAuth), + port = eclairRpcPort) + } + +} diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairInstance.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairInstance.scala new file mode 100644 index 0000000000..1a107a1cff --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/config/EclairInstance.scala @@ -0,0 +1,85 @@ +package org.bitcoins.eclair.rpc.config + +import java.io.File +import java.net.URI + +import com.typesafe.config.{Config, ConfigFactory} +import org.bitcoins.core.config.{MainNet, NetworkParameters, RegTest, TestNet3} +import org.bitcoins.core.protocol.ln.LnPolicy + +sealed trait EclairInstance { + def network: NetworkParameters + def uri: URI + def rpcUri: URI + def authCredentials: EclairAuthCredentials + + def copyWithDatadir(datadir: File): EclairInstance = { + EclairInstance( + network = network, + uri = uri, + rpcUri = rpcUri, + authCredentials = authCredentials.copyWithDatadir(datadir)) + } +} + +object EclairInstance { + private case class EclairInstanceImpl( + network: NetworkParameters, + uri: URI, + rpcUri: URI, + authCredentials: EclairAuthCredentials) extends EclairInstance + + def apply( + network: NetworkParameters, + uri: URI, + rpcUri: URI, + authCredentials: EclairAuthCredentials): EclairInstance = { + EclairInstanceImpl(network, uri, rpcUri, authCredentials) + } + + def fromDatadir(datadir: File): EclairInstance = { + val eclairConf = new File(datadir.getAbsolutePath + "/eclair.conf") + val config = ConfigFactory.parseFile(eclairConf) + val instance = fromConfig(config) + instance.copyWithDatadir(datadir) + + } + /** + * Parses a [[com.typesafe.config.Config Config]] in the format of this + * [[https://github.com/ACINQ/eclair/blob/master/eclair-core/src/main/resources/reference.conf sample reference.conf]] + * file to a + * [[org.bitcoins.eclair.rpc.config.EclairInstance EclairInstance]] + */ + def fromConfig(config: Config): EclairInstance = { + val chain = config.getString("eclair.chain") + + + + val serverBindingIp = config.getString("eclair.server.binding-ip") + val serverPort = ConfigUtil.getIntOrElse(config, "eclair.server.port", LnPolicy.DEFAULT_LN_P2P_PORT) + + val rpcHost = config.getString("eclair.api.binding-ip") + val rpcPort = ConfigUtil.getIntOrElse(config, "eclair.api.port", LnPolicy.DEFAULT_ECLAIR_API_PORT) + + val np: NetworkParameters = chain match { + case "regtest" => RegTest + case "testnet" => TestNet3 + case "mainnet" => MainNet + case network: String => throw new IllegalArgumentException(s"Unknown network $network in eclair.conf") + } + + val uri: URI = new URI(s"http://$serverBindingIp:$serverPort") + + val rpcUri: URI = new URI(s"http://$rpcHost:$rpcPort") + + val eclairAuth = EclairAuthCredentials.fromConfig(config) + + val instance = EclairInstance( + network = np, + uri = uri, + rpcUri = rpcUri, + authCredentials = eclairAuth) + + instance + } +} diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/EclairModels.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/EclairModels.scala new file mode 100644 index 0000000000..494d69bff2 --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/EclairModels.scala @@ -0,0 +1,197 @@ +package org.bitcoins.eclair.rpc.json + +import org.bitcoins.core.crypto.{ DoubleSha256Digest, ECDigitalSignature, Sha256Digest } +import org.bitcoins.core.protocol.ln.{ LnHumanReadablePart, LnInvoiceSignature } +import org.bitcoins.core.protocol.ln.channel.{ ChannelState, FundedChannelId } +import org.bitcoins.core.protocol.ln.currency.{ MilliSatoshis } +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.eclair.rpc.network.{ PeerState } +import play.api.libs.json.{ JsArray, JsObject } + +sealed abstract class EclairModels + +case class GetInfoResult( + nodeId: NodeId, + alias: String, + port: Int, + chainHash: DoubleSha256Digest, + blockHeight: Long) + +case class PeerInfo( + nodeId: NodeId, + state: PeerState, + //address: String, + channels: Int) + +case class ChannelInfo(nodeId: NodeId, channelId: FundedChannelId, state: ChannelState) + +case class NodeInfo( + signature: ECDigitalSignature, + features: String, + timestamp: Long, + nodeId: NodeId, + rgbColor: String, + alias: String, + addresses: Vector[String]) + +case class ChannelDesc(shortChannelId: String, a: String, b: String) + +case class ChannelUpdate( + signature: ECDigitalSignature, + chainHash: DoubleSha256Digest, + shortChannelId: String, + timestamp: Long, + flags: String, + cltvExpiryDelta: Int, + htlcMinimumMsat: Long, + feeBaseMsat: MilliSatoshis, + feeProportionalMillionths: Long) + +/* ChannelResult starts here, some of this may be useful but it seems that data is different at different times + +case class CommitInput( + outPoint: String, + amountSatoshis: Long) +implicit val commitInputReads: Reads[CommitInput] = + Json.reads[CommitInput] + +case class CommitChanges( + proposed: Vector[String], // IDK WHAT TYPE THIS SHOULD BE + signed: Vector[String], // IDK WHAT TYPE THIS SHOULD BE + acked: Vector[String] // IDK WHAT TYPE THIS SHOULD BE +) +implicit val commitChangesReads: Reads[CommitChanges] = + Json.reads[CommitChanges] + +case class CommitSpec( + htlcs: Vector[String], + feeratePerKw: Long, + toLocalMsat: Long, + toRemoteMsat: Long) +implicit val commitSpecReads: Reads[CommitSpec] = + Json.reads[CommitSpec] + +case class RemoteCommit( + index: Int, + spec: CommitSpec, + txid: String, + remotePerCommitmentPoint: String) +implicit val remoteCommitReads: Reads[RemoteCommit] = + Json.reads[RemoteCommit] + +case class PublishableTxs( + commitTx: String, + htlcTxsAndSigs: Vector[String]) +implicit val publishableTxsReads: Reads[PublishableTxs] = + Json.reads[PublishableTxs] + +case class LocalCommit( + index: Int, + spec: CommitSpec, + publishableTxs: PublishableTxs) +implicit val localCommitReads: Reads[LocalCommit] = + Json.reads[LocalCommit] + +case class RemoteParams( + nodeId: String, + dustLimitSatoshis: Long, + maxHtlcValueInFlightMsat: Long, + channelReserveSatoshis: Long, + htlcMinimumMsat: Long, + toSelfDelay: Long, + maxAcceptedHtlcs: Long, + fundingPubKey: String, + revocationBasepoint: String, + paymentBasepoint: String, + delayedPaymentBasepoint: String, + htlcBasepoint: String, + globalFeatures: String, + localFeatures: String) +implicit val remoteParamsReads: Reads[RemoteParams] = + Json.reads[RemoteParams] + +case class ChannelKeyPath( + path: Vector[Long]) +implicit val channelKeyPathReads: Reads[ChannelKeyPath] = + Json.reads[ChannelKeyPath] + +case class LocalParams( + nodeId: String, + channelKeyPath: ChannelKeyPath, + dustLimitSatoshis: Long, + maxHtlcValueInFlightMsat: Long, + channelReserveSatoshis: Long, + htlcMinimumMsat: Long, + toSelfDelay: Long, + maxAcceptedHtlcs: Long, + isFunder: Boolean, + defaultFinalScriptPubKey: String, + globalFeatures: String, + localFeatures: String) +implicit val localParamsReads: Reads[LocalParams] = + Json.reads[LocalParams] + +case class ChannelCommitments( + localParams: LocalParams, + remoteParams: RemoteParams, + channelFlags: Int, + localCommit: LocalCommit, + remoteCommit: RemoteCommit, + localChanges: CommitChanges, + remoteChanges: CommitChanges, + localNextHtlcId: Long, + remoteNextHtlcId: Long, + originChannels: String, // IDK WHAT TYPE THIS SHOULD BE + remoteNextCommitInfo: String, + commitInput: CommitInput, + remotePerCommitmentSecrets: Option[String], // IDK WHAT TYPE THIS SHOULD BE + channelId: String) +implicit val channelCommitmentsReads: Reads[ChannelCommitments] = + Json.reads[ChannelCommitments] + +case class ChannelData( + commitments: ChannelCommitments, + shortChannelId: String, + buried: Boolean, + channelUpdate: ChannelUpdate) +implicit val channelDataReads: Reads[ChannelData] = + Json.reads[ChannelData] +*/ +case class ChannelResult( + nodeId: NodeId, + channelId: FundedChannelId, + state: ChannelState, + data: JsObject) + +// ChannelResult ends here + +case class PaymentRequest( + prefix: LnHumanReadablePart, + amount: Option[MilliSatoshis], + timestamp: Long, + nodeId: NodeId, + tags: Vector[JsObject], + signature: LnInvoiceSignature) + +sealed abstract class PaymentResult +case class PaymentSucceeded( + amountMsat: MilliSatoshis, + paymentHash: Sha256Digest, + paymentPreimage: String, + route: JsArray) extends PaymentResult + +case class PaymentFailed( + paymentHash: Sha256Digest, + failures: Vector[JsObject]) extends PaymentResult +/* +case class PaymentFailure(???) extends SendResult +implicit val paymentFailureReads: Reads[PaymentFailure] = Json.reads[PaymentFailure] +implicit val sendResultReads: Reads[SendResult] = Reads[SendResult] { json => + json.validate[PaymentSucceeded] match { + case success: JsSuccess[PaymentSucceeded] => success + case err1: JsError => json.validate[PaymentFailure] match { + case failure: JsSuccess[PaymentFailure] => failure + case err2: JsError => JsError.merge(err1, err2) + } + } +}*/ diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/JsonReaders.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/JsonReaders.scala new file mode 100644 index 0000000000..b2c79b0267 --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/json/JsonReaders.scala @@ -0,0 +1,132 @@ +package org.bitcoins.eclair.rpc.json + +import org.bitcoins.core.protocol.ln.{ LnHumanReadablePart, LnInvoice, LnInvoiceSignature } +import org.bitcoins.core.protocol.ln.channel.{ ChannelState, FundedChannelId } +import org.bitcoins.core.protocol.ln.currency.{ MilliSatoshis, PicoBitcoins } +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.eclair.rpc.network.PeerState +import org.bitcoins.rpc.serializers.SerializerUtil +import play.api.libs.json._ + +import scala.util.{ Failure, Success } + +object JsonReaders { + import org.bitcoins.rpc.serializers.JsonReaders._ + + implicit val channelStateReads: Reads[ChannelState] = { + Reads { jsValue: JsValue => + SerializerUtil.processJsStringOpt(ChannelState.fromString)(jsValue) + } + } + + implicit val peerStateReads: Reads[PeerState] = { + Reads { jsValue: JsValue => + SerializerUtil.processJsStringOpt(PeerState.fromString)(jsValue) + } + } + + implicit val picoBitcoinsReads: Reads[PicoBitcoins] = { + Reads { jsValue: JsValue => + SerializerUtil.processJsNumberBigInt(PicoBitcoins.apply)(jsValue) + } + } + + implicit val msatReads: Reads[MilliSatoshis] = { + Reads { jsValue: JsValue => + SerializerUtil.processJsNumberBigInt(MilliSatoshis.apply)(jsValue) + + } + } + + implicit val nodeIdReads: Reads[NodeId] = { + Reads { jsValue: JsValue => + SerializerUtil.processJsString(NodeId.fromHex)(jsValue) + } + } + + implicit val lnHrpReads: Reads[LnHumanReadablePart] = { + Reads { jsValue => + SerializerUtil.processJsStringOpt(LnHumanReadablePart.fromString(_).toOption)(jsValue) + } + } + + implicit val lnInvoiceSignatureReads: Reads[LnInvoiceSignature] = { + Reads { jsValue => + SerializerUtil.processJsString(LnInvoiceSignature.fromHex)(jsValue) + } + } + + implicit val getInfoResultReads: Reads[GetInfoResult] = { + Json.reads[GetInfoResult] + } + + implicit val peerInfoReads: Reads[PeerInfo] = { + Json.reads[PeerInfo] + } + + implicit val nodeInfoReads: Reads[NodeInfo] = { + Json.reads[NodeInfo] + } + + implicit val fundedChannelIdReads: Reads[FundedChannelId] = { + Reads { jsValue: JsValue => + SerializerUtil.processJsString(FundedChannelId.fromHex)(jsValue) + } + } + + implicit val channelDescReads: Reads[ChannelDesc] = { + Json.reads[ChannelDesc] + } + + implicit val channelInfoReads: Reads[ChannelInfo] = { + Json.reads[ChannelInfo] + } + + implicit val channelUpdateReads: Reads[ChannelUpdate] = { + Json.reads[ChannelUpdate] + } + + implicit val paymentRequestReads: Reads[PaymentRequest] = { + Json.reads[PaymentRequest] + } + + implicit val paymentSucceededReads: Reads[PaymentSucceeded] = { + Json.reads[PaymentSucceeded] + } + + implicit val paymentFailedReads: Reads[PaymentFailed] = { + Json.reads[PaymentFailed] + } + + implicit val paymentResultReads: Reads[PaymentResult] = { + Reads[PaymentResult] { jsValue => + val sendResult = jsValue.validate[PaymentSucceeded] + sendResult match { + case p: JsSuccess[PaymentSucceeded] => p + case err1: JsError => + val pFailedResult = jsValue.validate[PaymentFailed] + pFailedResult match { + case s: JsSuccess[PaymentFailed] => s + case err2: JsError => + JsError.merge(err1, err2) + } + } + } + } + + implicit val channelResultReads: Reads[ChannelResult] = + Json.reads[ChannelResult] + + implicit val lnInvoiceReads: Reads[LnInvoice] = + Reads[LnInvoice] { + case JsString(invoice) => + LnInvoice.fromString(invoice) match { + case Success(paymentRequest) => JsSuccess(paymentRequest) + case Failure(err) => + JsError(s"Invalid refund invoice: ${err.toString}") + } + case bad @ (_: JsNumber | _: JsObject | _: JsArray | _: JsBoolean | + JsNull) => + JsError(s"Invalid type on refund invoice: $bad, expected JsString") + } +} diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/NodeUri.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/NodeUri.scala new file mode 100644 index 0000000000..61d89b4088 --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/NodeUri.scala @@ -0,0 +1,46 @@ +package org.bitcoins.eclair.rpc.network + +import org.bitcoins.core.protocol.ln.node.NodeId + +import scala.util.{ Failure, Success, Try } + +case class NodeUri(nodeId: NodeId, host: String, port: Int) { + override def toString = s"$nodeId@$host:$port" +} + +object NodeUri { + + private val defaultPort = ":9735" + + def fromString(uri: String): Try[NodeUri] = { + val patternWithPort = """(\w+)@([\w.]+(\w+)):(\d{2,5})""".r + + val isUriWithPort = patternWithPort.findFirstIn(uri) + + val nodeUriT = isUriWithPort match { + case Some(withPort) => + Success(parse(withPort)) + case None => + Failure(new IllegalArgumentException(s"Failed to parse $uri to a NodeUri")) + } + nodeUriT + } + + def fromStringNoPort(uri: String): Try[NodeUri] = { + fromString(uri + defaultPort) + } + + /** + * Assumes format is [nodeId]@[host]:[port] + */ + private def parse(validUri: String): NodeUri = { + //key is 33 bytes in size + val (key: String, rest: String) = validUri.splitAt(66) + + val (host, port) = rest.splitAt(rest.size - 5) + + val nodeId = NodeId.fromHex(key) + + NodeUri(nodeId, host.tail, port.tail.toInt) + } +} \ No newline at end of file diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/PeerState.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/PeerState.scala new file mode 100644 index 0000000000..ba7fe999c9 --- /dev/null +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/network/PeerState.scala @@ -0,0 +1,15 @@ +package org.bitcoins.eclair.rpc.network + +sealed abstract class PeerState + +object PeerState { + case object CONNECTED extends PeerState + + case object DISCONNECTED extends PeerState + + private val all = List(CONNECTED, DISCONNECTED) + + def fromString(str: String): Option[PeerState] = { + all.find(_.toString == str) + } +} \ No newline at end of file diff --git a/eclair-rpc/src/test/scala/org/bitcoins/eclair/network/NodeUriTest.scala b/eclair-rpc/src/test/scala/org/bitcoins/eclair/network/NodeUriTest.scala new file mode 100644 index 0000000000..62c7068bbd --- /dev/null +++ b/eclair-rpc/src/test/scala/org/bitcoins/eclair/network/NodeUriTest.scala @@ -0,0 +1,47 @@ +package org.bitcoins.eclair.network + +import org.bitcoins.eclair.rpc.network.NodeUri +import org.scalatest.FlatSpec + +class NodeUriTest extends FlatSpec { + + behavior of "NodeUri" + + it must "read the suredbits node uri" in { + val sb = "0338f57e4e20abf4d5c86b71b59e995ce4378e373b021a7b6f41dabb42d3aad069@ln.test.suredbits.com:9735" + val uriT = NodeUri.fromString(sb) + assert(uriT.isSuccess == true) + + assert(uriT.get.toString == sb) + } + + it must "read the suredbits node uri without the port" in { + val sb = "0338f57e4e20abf4d5c86b71b59e995ce4378e373b021a7b6f41dabb42d3aad069@ln.test.suredbits.com" + val uriT = NodeUri.fromStringNoPort(sb) + assert(uriT.isSuccess == true) + + assert(uriT.get.toString == (sb + ":9735")) + } + + it must "read a node uri with a ip address" in { + val sb = "0338f57e4e20abf4d5c86b71b59e995ce4378e373b021a7b6f41dabb42d3aad069@127.0.0.1:9735" + + val uriT = NodeUri.fromString(sb) + + assert(uriT.isSuccess) + + assert(uriT.get.toString == sb) + } + + it must "fail to read a node uri without a nodeId" in { + val sb = "@ln.test.suredbits.com" + val uriT = NodeUri.fromString(sb) + assert(uriT.isFailure == true) + } + + it must "fail to read a node uri with a invalid port" in { + val sb = "0338f57e4e20abf4d5c86b71b59e995ce4378e373b021a7b6f41dabb42d3aad069@ln.test.suredbits.com:abc2" + val uriT = NodeUri.fromString(sb) + assert(uriT.isFailure == true) + } +} diff --git a/eclair-rpc/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala b/eclair-rpc/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala new file mode 100644 index 0000000000..f6a2542c29 --- /dev/null +++ b/eclair-rpc/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala @@ -0,0 +1,362 @@ +package org.bitcoins.eclair.rpc + +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis} +import org.bitcoins.core.number.Int64 +import org.bitcoins.core.protocol.ln.channel.{ChannelId, ChannelState} +import org.bitcoins.core.protocol.ln.currency.{MicroBitcoins, MilliBitcoins, NanoBitcoins, PicoBitcoins} +import org.bitcoins.core.protocol.ln.node.NodeId +import org.bitcoins.core.util.BitcoinSLogger +import org.bitcoins.eclair.rpc.client.EclairRpcClient +import org.bitcoins.eclair.rpc.config.{EclairAuthCredentials, EclairInstance} +import org.bitcoins.eclair.rpc.json._ +import org.bitcoins.rpc.BitcoindRpcTestUtil +import org.scalatest.{Assertion, AsyncFlatSpec, BeforeAndAfterAll} + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, Future} + +class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll { + + implicit val system = ActorSystem("EclairRpcClient") + implicit val m = ActorMaterializer.create(system) + implicit val ec = m.executionContext + implicit val bitcoinNp = EclairTestUtil.network + + val logger = BitcoinSLogger.logger + + val bitcoindRpcClient = BitcoindRpcTestUtil.startedBitcoindRpcClient() + val (client, otherClient) = EclairTestUtil.createNodePair(Some(bitcoindRpcClient)) + + behavior of "RpcClient" + + it should "be able to open and close a channel" in { + + val changeAddrF = bitcoindRpcClient.getNewAddress() + val result: Future[Assertion] = { + val isOpenedF: Future[(ChannelId, Assertion)] = { + otherClient.getInfo.flatMap { info => + val amt = Satoshis(Int64(100000)) + val openedChanF = client.open(info.nodeId, amt) + + openedChanF.flatMap { channelId => + val exists = hasChannel(client, channelId) + exists.map(e => (channelId, e)) + } + } + } + + val isConfirmedF: Future[(ChannelId,Assertion)] = { + isOpenedF.map { case (chanId, assertion) => + val _ = bitcoindRpcClient.generate(6) + EclairTestUtil.awaitChannelNormal( + client1 = client, + chanId = chanId + ) + + (chanId, assertion) + } + } + + val isClosedF = { + isConfirmedF.flatMap { case (chanId, assertion) => + + val closedF = changeAddrF.flatMap { addr => + client.close(chanId,addr.scriptPubKey) + } + + closedF.flatMap { _ => + + EclairTestUtil.awaitUntilChannelClosing(client,chanId) + val chanF = client.channel(chanId) + chanF.map { chan => + assert(chan.state == ChannelState.CLOSING) + } + } + } + } + + val closedOnChainF = { + isClosedF.flatMap { _ => + changeAddrF.flatMap { addr => + + val amountF = bitcoindRpcClient.getReceivedByAddress( + address = addr, + minConfirmations = 0) + + amountF.map(amt => assert(amt > CurrencyUnits.zero)) + + } + } + } + + closedOnChainF + } + + result + } + + it should "fail to authenticate on bad password" in { + val goodCredentials = client.instance.authCredentials + val badCredentials = EclairAuthCredentials("bad_password", goodCredentials.bitcoinAuthOpt, goodCredentials.port) + val badInstance = EclairInstance(client.instance.network, client.instance.uri, client.instance.rpcUri, badCredentials) + val badClient = new EclairRpcClient(badInstance) + + recoverToSucceededIf[RuntimeException](badClient.getInfo) + } + + it should "be able to list an existing peer and isConnected must be true" in { + //test assumes that a connection to a peer was made in `beforeAll` + val otherClientNodeIdF = otherClient.getInfo.map(_.nodeId) + + otherClientNodeIdF.flatMap(nid => hasConnection(client, nid)) + } + + it should "be able to generate an invoice and get the same amount back" in { + val amt = PicoBitcoins(10) //this is the smallest unit we can use, 1 msat + val description = "bitcoin-s test case" + val expiry = (System.currentTimeMillis() / 1000) + + val invoiceF = client.receive( + description = description, + amountMsat = amt, + expirySeconds = expiry) + + val assert0: Future[Assertion] = { + invoiceF.map { i => + assert(i.amount.get == amt) + assert(i.lnTags.description.get.string == description) + assert(i.lnTags.expiryTime.get.u32.toLong == expiry) + } + } + + val amt1 = NanoBitcoins.one + val invoice1F = client.receive( + description = description, + amountMsat = amt1, + expirySeconds = expiry) + + val assert1 = { + invoice1F.map { i => + assert(i.amount.get == amt1) + assert(i.lnTags.description.get.string == description) + assert(i.lnTags.expiryTime.get.u32.toLong == expiry) + } + } + + val amt2 = MicroBitcoins.one + val invoice2F = client.receive( + description = description, + amountMsat = amt2, + expirySeconds = expiry) + + val assert2 = { + invoice2F.map { i => + assert(i.amount.get == amt2) + assert(i.lnTags.description.get.string == description) + assert(i.lnTags.expiryTime.get.u32.toLong == expiry) + + } + } + + val amt3 = MilliBitcoins.one + + val invoice3F = client.receive( + description = description, + amountMsat = amt3, + expirySeconds = expiry) + + val assert3 = { + invoice3F.map { i => + assert(i.amount.get == amt3) + assert(i.lnTags.description.get.string == description) + assert(i.lnTags.expiryTime.get.u32.toLong == expiry) + } + } + + assert0.flatMap { _ => + assert1.flatMap { _ => + assert2.flatMap(_ => assert3) + } + } + } + it should "be able to generate a payment invoice and then check that invoice" in { + val amt = PicoBitcoins(1000) //1 satoshi + val description = "bitcoin-s test case" + val expiry = (System.currentTimeMillis() / 1000) + + val invoiceF = client.receive( + description = description, + amountMsat = amt, + expirySeconds = expiry) + + val paymentRequestF: Future[PaymentRequest] = invoiceF.flatMap { i => + client.checkInvoice(i) + } + + paymentRequestF.map { paymentRequest => + assert(paymentRequest.amount.get == amt.toMSat) + assert(paymentRequest.timestamp == expiry) + } + } + + it should "open a channel, send a payment, and close the channel" in { + val openChannelIdF = openAndConfirmChannel(client, otherClient) + + val paymentAmount = NanoBitcoins(100000) + val invoiceF = openChannelIdF.flatMap(_ => otherClient.receive(paymentAmount)) + + val paymentF = invoiceF.flatMap(i => client.send(i)) + + val isCorrectAmountF = paymentF.map { p => + assert(p.isInstanceOf[PaymentSucceeded]) + + val pSucceed = p.asInstanceOf[PaymentSucceeded] + + assert(pSucceed.amountMsat == paymentAmount) + + } + + val closedChannelF: Future[Assertion] = isCorrectAmountF.flatMap { _ => + openChannelIdF.flatMap { cid => + + val closedF = client.close(cid) + + EclairTestUtil.awaitUntilChannelClosing(otherClient, cid) + + val otherStateF = closedF.flatMap(_ => otherClient.channel(cid)) + + val isClosed = otherStateF.map { channel => + assert(channel.state == ChannelState.CLOSING) + } + isClosed + } + } + + isCorrectAmountF.flatMap { _ => + closedChannelF.map { _ => + succeed + } + } + + } + + it should "check a payment" in { + val openChannelIdF = openAndConfirmChannel(client, otherClient) + + val paymentAmount = NanoBitcoins(100000) + val invoiceF = openChannelIdF.flatMap(_ => otherClient.receive(paymentAmount)) + + val isPaid1F = invoiceF.flatMap(i => otherClient.checkPayment(Left(i))) + + val isNotPaidAssertF = isPaid1F.map(isPaid => assert(!isPaid)) + + //send the payment now + val paidF: Future[PaymentResult] = invoiceF.flatMap(i => client.send(i)) + + val isPaid2F: Future[Boolean] = paidF.flatMap { p => + val succeed = p.asInstanceOf[PaymentSucceeded] + + otherClient.checkPayment(Right(succeed.paymentHash)) + } + + val isPaidAssertF = isPaid2F.map(isPaid => assert(isPaid)) + + isNotPaidAssertF.flatMap { isNotPaid => + isPaidAssertF.map { isPaid => + succeed + + } + } + } + + it should "be able to send payments in both directions" in { + val openChannelIdF = openAndConfirmChannel(client, otherClient) + + val paymentAmount = NanoBitcoins(100000) + val invoiceF = openChannelIdF.flatMap(_ => otherClient.receive(paymentAmount)) + //send the payment now + val paidF: Future[PaymentResult] = invoiceF.flatMap(i => client.send(i)) + + val isPaidF: Future[Boolean] = paidF.flatMap { p => + val succeed = p.asInstanceOf[PaymentSucceeded] + otherClient.checkPayment(Right(succeed.paymentHash)) + } + + val isPaidAssertF = isPaidF.map(isPaid => assert(isPaid)) + + isPaidAssertF.flatMap { isPaid => + val invoice2F = openChannelIdF.flatMap(_ => client.receive(paymentAmount)) + //send the payment now + val paid2F: Future[PaymentResult] = invoice2F.flatMap((i => otherClient.send(i))) + + val isPaid2F: Future[Boolean] = paid2F.flatMap { p => + assert(p.isInstanceOf[PaymentSucceeded]) + val succeed = p.asInstanceOf[PaymentSucceeded] + client.checkPayment(Right(succeed.paymentHash)) + } + + isPaid2F.map(isPaid => assert(isPaid)) + } + } + + private def hasConnection(client: EclairRpcClient, nodeId: NodeId): Future[Assertion] = { + + val hasPeersF = client.getPeers.map(_.nonEmpty) + + val hasPeersAssertF = hasPeersF.map(h => assert(h)) + + val isConnectedF = client.isConnected(nodeId) + + val isConnectedAssertF = isConnectedF.map(isConnected => assert(isConnected)) + + hasPeersAssertF.flatMap(hasPeers => isConnectedAssertF.map(isConn => isConn)) + } + + /** Checks that the given [[org.bitcoins.eclair.rpc.client.EclairRpcClient]] has the given chanId */ + private def hasChannel(client: EclairRpcClient, chanId: ChannelId): Future[Assertion] = { + val recognizedOpenChannel: Future[Assertion] = { + + val chanResultF: Future[ChannelResult] = client.channel(chanId) + + chanResultF.map(c => assert(c.channelId == chanId)) + + } + + recognizedOpenChannel + } + + private def openAndConfirmChannel( + client1: EclairRpcClient, + client2: EclairRpcClient, + amount: CurrencyUnit = Satoshis(Int64(1000000))): Future[ChannelId] = { + + val bitcoindRpc = EclairTestUtil.getBitcoindRpc(client1) + + val nodeId2F: Future[NodeId] = client2.getInfo.map(_.nodeId) + + val channelIdF: Future[ChannelId] = nodeId2F.flatMap(nid2 => client1.open(nid2, amount)) + + //confirm the funding tx + val genF = channelIdF.flatMap(_ => bitcoindRpc.generate(6)) + + channelIdF.flatMap { cid => + genF.map { _ => + + //wait until our peer has put the channel in the + //NORMAL state so we can route payments to them + EclairTestUtil.awaitUntilChannelNormal(client2, cid) + + cid + + } + } + } + + override def afterAll(): Unit = { + val s1 = EclairTestUtil.shutdown(client) + val s2 = otherClient.stop() + Await.result(system.terminate(), 10.seconds) + } +} diff --git a/project/Deps.scala b/project/Deps.scala index 6dda122627..8e7c804d11 100644 --- a/project/Deps.scala +++ b/project/Deps.scala @@ -79,7 +79,23 @@ object Deps { ) val bench = List( - "org.slf4j" % "slf4j-api" % V.slf4j withSources() withJavadoc(), - Compile.logback + "org.slf4j" % "slf4j-api" % V.slf4j withSources() withJavadoc(), + Compile.logback + ) + + val eclairRpc = List( + Compile.akkaHttp, + Compile.akkaStream, + Compile.playJson, + Compile.slf4j, + Test.akkaHttp, + Test.logback, + Test.scalaTest, + Test.scalacheck + ) + + val testkit = List( + Compile.slf4j, + "org.scalacheck" %% "scalacheck" % V.scalacheck withSources() withJavadoc() ) } diff --git a/rpc/src/main/scala/org/bitcoins/rpc/RpcUtil.scala b/rpc/src/main/scala/org/bitcoins/rpc/RpcUtil.scala index 94f4aa411e..7c1ecc6c34 100644 --- a/rpc/src/main/scala/org/bitcoins/rpc/RpcUtil.scala +++ b/rpc/src/main/scala/org/bitcoins/rpc/RpcUtil.scala @@ -16,44 +16,59 @@ trait RpcUtil extends BitcoinSLogger { } } + def retryUntilSatisfied( + condition: => Boolean, + duration: FiniteDuration, + maxTries: Int = 50)(implicit system: ActorSystem): Future[Unit] = { + val f = () => Future.successful(condition) + retryUntilSatisfiedF(f, duration, maxTries) + } + /** * The returned Future completes when condition becomes true - * @param condition The condition being waited on + * @param conditionF The condition being waited on * @param duration The interval between calls to check condition * @param maxTries If condition is tried this many times, the Future fails * @param system An ActorSystem to schedule calls to condition * @return A Future[Unit] that succeeds if condition becomes true and fails otherwise */ - def retryUntilSatisfied( - condition: => Boolean, - duration: FiniteDuration = 100.milliseconds, + def retryUntilSatisfiedF( + conditionF: () => Future[Boolean], + duration: FiniteDuration = 100.millis, maxTries: Int = 50)(implicit system: ActorSystem): Future[Unit] = { - retryUntilSatisfiedWithCounter(condition, duration, maxTries = maxTries) + + retryUntilSatisfiedWithCounter( + conditionF = conditionF, + duration = duration, + maxTries = maxTries) } // Has a different name so that default values are permitted private def retryUntilSatisfiedWithCounter( - condition: => Boolean, + conditionF: () => Future[Boolean], duration: FiniteDuration, counter: Int = 0, maxTries: Int)(implicit system: ActorSystem): Future[Unit] = { + implicit val ec = system.dispatcher - if (counter == maxTries) { - Future.failed(new RuntimeException("Condition timed out")) - } else if (condition) { - Future.successful(()) - } else { - val p = Promise[Boolean]() - val runnable = retryRunnable(condition, p) + conditionF().flatMap { condition => - system.scheduler.scheduleOnce(duration, runnable) + if (condition) { + Future.successful(()) + } else if (counter == maxTries) { + Future.failed(new RuntimeException("Condition timed out")) + } else { + val p = Promise[Boolean]() + val runnable = retryRunnable(condition, p) - p.future.flatMap { - case true => Future.successful(()) - case false => retryUntilSatisfiedWithCounter(condition, duration, counter + 1, maxTries) + system.scheduler.scheduleOnce(duration, runnable) + + p.future.flatMap { + case true => Future.successful(()) + case false => retryUntilSatisfiedWithCounter(conditionF, duration, counter + 1, maxTries) + } } - } } @@ -69,25 +84,47 @@ trait RpcUtil extends BitcoinSLogger { * @param system An ActorSystem to schedule calls to condition */ def awaitCondition( - condition: => Boolean, + condition: () => Boolean, duration: FiniteDuration = 100.milliseconds, maxTries: Int = 50, overallTimeout: FiniteDuration = 1.hour)(implicit system: ActorSystem): Unit = { - Await.result(retryUntilSatisfied(condition, duration, maxTries), overallTimeout) + + //type hackery here to go from () => Boolean to () => Future[Boolean] + //to make sure we re-evaluate every time retryUntilSatisfied is called + def conditionDef: Boolean = condition() + val conditionF: () => Future[Boolean] = () => Future.successful(conditionDef) + + awaitConditionF(conditionF, duration, maxTries, overallTimeout) + } + + def awaitConditionF( + conditionF: () => Future[Boolean], + duration: FiniteDuration = 100.milliseconds, + maxTries: Int = 50, + overallTimeout: FiniteDuration = 1.hour)(implicit system: ActorSystem): Unit = { + + val f: Future[Unit] = retryUntilSatisfiedF( + conditionF = conditionF, + duration = duration, + maxTries = maxTries) + + Await.result(f, overallTimeout) } def awaitServer( server: BitcoindRpcClient, - duration: FiniteDuration = 100.milliseconds, + duration: FiniteDuration = 1.seconds, maxTries: Int = 50)(implicit system: ActorSystem): Unit = { - awaitCondition(server.isStarted, duration, maxTries) + val f = () => server.isStarted + awaitCondition(f, duration, maxTries) } def awaitServerShutdown( server: BitcoindRpcClient, duration: FiniteDuration = 300.milliseconds, maxTries: Int = 50)(implicit system: ActorSystem): Unit = { - awaitCondition(!server.isStarted, duration, maxTries) + val f = () => !server.isStarted + awaitCondition(f, duration, maxTries) } } diff --git a/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindAuthCredentials.scala b/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindAuthCredentials.scala index 54b0edeec9..1b1c76530c 100644 --- a/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindAuthCredentials.scala +++ b/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindAuthCredentials.scala @@ -15,24 +15,28 @@ sealed trait BitcoindAuthCredentials { /** rpcpassword field in our bitcoin.conf file */ def password: String + + def rpcPort: Int } object BitcoindAuthCredentials { private case class BitcoindAuthCredentialsImpl( username: String, password: String, + rpcPort: Int, datadir: File) extends BitcoindAuthCredentials - def apply(username: String, password: String): BitcoindAuthCredentials = { + def apply(username: String, password: String, rpcPort: Int): BitcoindAuthCredentials = { val defaultDataDir = new File(System.getProperty("user.home") + "/.bitcoin") - BitcoindAuthCredentials(username, password, defaultDataDir) + BitcoindAuthCredentials(username, password, rpcPort, defaultDataDir) } def apply( username: String, password: String, + rpcPort: Int, datadir: File): BitcoindAuthCredentials = { - BitcoindAuthCredentialsImpl(username, password, datadir) + BitcoindAuthCredentialsImpl(username, password, rpcPort, datadir) } } diff --git a/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindInstance.scala b/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindInstance.scala index 09c5981887..56c92fe791 100644 --- a/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindInstance.scala +++ b/rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindInstance.scala @@ -8,11 +8,16 @@ import org.bitcoins.core.config.NetworkParameters * Created by chris on 4/29/17. */ sealed trait BitcoindInstance { - + require( + rpcUri.getPort == rpcPort, + s"RpcUri and the rpcPort in authCredentials are different ${rpcUri} authcred: ${rpcPort}") def network: NetworkParameters def uri: URI def rpcUri: URI def authCredentials: BitcoindAuthCredentials + + def rpcPort: Int = authCredentials.rpcPort + def zmqPortOpt: Option[Int] } object BitcoindInstance { @@ -20,14 +25,15 @@ object BitcoindInstance { network: NetworkParameters, uri: URI, rpcUri: URI, - authCredentials: BitcoindAuthCredentials) - extends BitcoindInstance + authCredentials: BitcoindAuthCredentials, + zmqPortOpt: Option[Int]) extends BitcoindInstance def apply( network: NetworkParameters, uri: URI, rpcUri: URI, - authCredentials: BitcoindAuthCredentials): BitcoindInstance = { - BitcoindInstanceImpl(network, uri, rpcUri, authCredentials) + authCredentials: BitcoindAuthCredentials, + zmqPortOpt: Option[Int] = None): BitcoindInstance = { + BitcoindInstanceImpl(network, uri, rpcUri, authCredentials, zmqPortOpt) } } diff --git a/rpc/src/main/scala/org/bitcoins/rpc/serializers/JsonWriters.scala b/rpc/src/main/scala/org/bitcoins/rpc/serializers/JsonWriters.scala index d841956ea9..4ca7957e23 100644 --- a/rpc/src/main/scala/org/bitcoins/rpc/serializers/JsonWriters.scala +++ b/rpc/src/main/scala/org/bitcoins/rpc/serializers/JsonWriters.scala @@ -4,6 +4,7 @@ import org.bitcoins.core.crypto.DoubleSha256Digest import org.bitcoins.core.currency.Bitcoins import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.BitcoinAddress +import org.bitcoins.core.protocol.ln.currency.MilliSatoshis import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.transaction.{ Transaction, TransactionInput } import org.bitcoins.core.util.BitcoinSUtil @@ -51,4 +52,8 @@ object JsonWriters { Json.toJson(o.map { case (k, v) => (keyString(k), v) }) } } + + implicit object MilliSatoshisWrites extends Writes[MilliSatoshis] { + override def writes(o: MilliSatoshis): JsValue = JsNumber(o.toBigDecimal) + } } diff --git a/rpc/src/main/scala/org/bitcoins/rpc/serializers/SerializerUtil.scala b/rpc/src/main/scala/org/bitcoins/rpc/serializers/SerializerUtil.scala new file mode 100644 index 0000000000..c5b39667c7 --- /dev/null +++ b/rpc/src/main/scala/org/bitcoins/rpc/serializers/SerializerUtil.scala @@ -0,0 +1,68 @@ +package org.bitcoins.rpc.serializers + +import play.api.libs.json._ + +sealed abstract class SerializerUtil { + def processJsNumberBigInt[T](numFunc: BigInt => T)( + json: JsValue): JsResult[T] = json match { + case JsNumber(nDecimal) => + val nOpt = nDecimal.toBigIntExact() + nOpt match { + case Some(t) => JsSuccess(numFunc(t)) + case None => JsError(s"Could not parsed expected t from given string $nDecimal") + } + case err @ (JsNull | _: JsBoolean | _: JsString | _: JsArray | + _: JsObject) => + buildJsErrorMsg("jsnumber", err) + } + + def buildJsErrorMsg(expected: String, err: JsValue): JsError = { + JsError(s"error.expected.$expected, got ${Json.toJson(err).toString()}") + } + + def buildErrorMsg(expected: String, err: Any): JsError = { + JsError(s"error.expected.$expected, got ${err.toString}") + } + + // For use in implementing reads method of Reads[T] where T is constructed from a JsNumber via numFunc + def processJsNumber[T](numFunc: BigDecimal => T)( + json: JsValue): JsResult[T] = json match { + case JsNumber(n) => JsSuccess(numFunc(n)) + case err @ (JsNull | _: JsBoolean | _: JsString | _: JsArray | + _: JsObject) => + SerializerUtil.buildJsErrorMsg("jsnumber", err) + } + + def processJsObject[T](f: JsObject => T)(json: JsValue): JsResult[T] = { + json match { + case obj: JsObject => JsSuccess(f(obj)) + case err @ (JsNull | _: JsBoolean | _: JsString | _: JsArray | + _: JsNumber) => + SerializerUtil.buildJsErrorMsg("jsobject", err) + } + } + + // For use in implementing reads method of Reads[T] where T is constructed from a JsString via strFunc + def processJsString[T](strFunc: String => T)( + json: JsValue): JsResult[T] = json match { + case JsString(s) => JsSuccess(strFunc(s)) + case err @ (JsNull | _: JsBoolean | _: JsNumber | _: JsArray | + _: JsObject) => + SerializerUtil.buildJsErrorMsg("jsstring", err) + } + + def processJsStringOpt[T](f: String => Option[T])(jsValue: JsValue): JsResult[T] = { + jsValue match { + case JsString(key) => + val tOpt = f(key) + tOpt match { + case Some(t) => JsSuccess(t) + case None => SerializerUtil.buildErrorMsg("invalid jsstring", jsValue) + } + case err @ (_: JsNumber | _: JsObject | _: JsArray | JsNull | _: JsBoolean) => + SerializerUtil.buildErrorMsg("jsstring", err) + } + } +} + +object SerializerUtil extends SerializerUtil diff --git a/rpc/src/test/scala/org/bitcoins/rpc/RpcUtilTest.scala b/rpc/src/test/scala/org/bitcoins/rpc/RpcUtilTest.scala index 94a2b10a11..99ceb8d468 100644 --- a/rpc/src/test/scala/org/bitcoins/rpc/RpcUtilTest.scala +++ b/rpc/src/test/scala/org/bitcoins/rpc/RpcUtilTest.scala @@ -21,29 +21,35 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll { true } - private def boolLaterDoneAnd(bool: Boolean, boolFuture: Future[Boolean]): Boolean = - boolFuture.value.contains(Success(bool)) + private def boolLaterDoneAnd(bool: Boolean, boolFuture: Future[Boolean]): Future[Boolean] = { + Future.successful(boolFuture.value.contains(Success(bool))) + } - private def boolLaterDoneAndTrue(trueLater: Future[Boolean]): Boolean = - boolLaterDoneAnd(true, trueLater) + private def boolLaterDoneAndTrue(trueLater: Future[Boolean]): () => Future[Boolean] = { + () => boolLaterDoneAnd(true, trueLater) + } behavior of "RpcUtil" it should "complete immediately if condition is true" in { - RpcUtil.retryUntilSatisfied(condition = true, duration = 0.millis).map { _ => + RpcUtil.retryUntilSatisfiedF( + conditionF = () => Future.successful(true), + duration = 0.millis).map { _ => succeed } } it should "fail if condition is false" in { recoverToSucceededIf[RuntimeException] { - RpcUtil.retryUntilSatisfied(condition = false, duration = 0.millis) + RpcUtil.retryUntilSatisfiedF( + conditionF = () => Future.successful(false), + duration = 0.millis) } } it should "succeed after a delay" in { val boolLater = trueLater(delay = 250) - RpcUtil.retryUntilSatisfied(boolLaterDoneAndTrue(boolLater)).map { _ => + RpcUtil.retryUntilSatisfiedF(boolLaterDoneAndTrue(boolLater)).map { _ => succeed } } @@ -51,25 +57,25 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll { it should "fail if there is a delay and duration is zero" in { val boolLater = trueLater(delay = 250) recoverToSucceededIf[RuntimeException] { - RpcUtil.retryUntilSatisfied(boolLaterDoneAndTrue(boolLater), duration = 0.millis) + RpcUtil.retryUntilSatisfiedF(boolLaterDoneAndTrue(boolLater), duration = 0.millis) } } it should "succeed immediately if condition is true" in { - RpcUtil.awaitCondition(condition = true, 0.millis) + RpcUtil.awaitCondition(condition = () => true, 0.millis) succeed } it should "timeout if condition is false" in { assertThrows[RuntimeException] { - RpcUtil.awaitCondition(condition = false, duration = 0.millis) + RpcUtil.awaitCondition(condition = () => false, duration = 0.millis) } } it should "block for a delay and then succeed" in { val boolLater = trueLater(delay = 250) val before: Long = System.currentTimeMillis - RpcUtil.awaitCondition(boolLaterDoneAndTrue(boolLater)) + RpcUtil.awaitConditionF(boolLaterDoneAndTrue(boolLater)) val after: Long = System.currentTimeMillis assert(after - before >= 250) } @@ -77,16 +83,16 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll { it should "timeout if there is a delay and duration is zero" in { val boolLater = trueLater(delay = 250) assertThrows[RuntimeException] { - RpcUtil.awaitCondition(boolLaterDoneAndTrue(boolLater), duration = 0.millis) + RpcUtil.awaitConditionF(boolLaterDoneAndTrue(boolLater), duration = 0.millis) } } "TestUtil" should "create a temp bitcoin directory when creating a DaemonInstance, and then delete it" in { - val instance = TestUtil.instance(TestUtil.randomPort, TestUtil.randomPort) + val instance = BitcoindRpcTestUtil.instance(BitcoindRpcTestUtil.randomPort, BitcoindRpcTestUtil.randomPort) val dir = instance.authCredentials.datadir assert(dir.isDirectory) assert(dir.listFiles.contains(new File(dir.getAbsolutePath + "/bitcoin.conf"))) - TestUtil.deleteTmpDir(dir) + BitcoindRpcTestUtil.deleteTmpDir(dir) assert(!dir.exists) } @@ -94,7 +100,7 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll { implicit val m: ActorMaterializer = ActorMaterializer.create(system) implicit val ec = m.executionContext - val instance = TestUtil.instance() + val instance = BitcoindRpcTestUtil.instance() val client = new BitcoindRpcClient(instance) client.start() RpcUtil.awaitCondition(client.isStarted) @@ -108,7 +114,7 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll { } "TestUtil" should "be able to create a connected node pair with 100 blocks and then delete them" in { - TestUtil.createNodePair().flatMap { + BitcoindRpcTestUtil.createNodePair().flatMap { case (client1, client2) => assert(client1.getDaemon.authCredentials.datadir.isDirectory) assert(client2.getDaemon.authCredentials.datadir.isDirectory) @@ -122,7 +128,7 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll { client2.getBlockCount.map { count2 => assert(count2 == 100) - TestUtil.deleteNodePair(client1, client2) + BitcoindRpcTestUtil.deleteNodePair(client1, client2) assert(!client1.getDaemon.authCredentials.datadir.exists) assert(!client2.getDaemon.authCredentials.datadir.exists) } diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/AddressGenerator.scala b/testkit/src/main/scala/org/bitcoins/core/gen/AddressGenerator.scala new file mode 100644 index 0000000000..cc82e22cbc --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/AddressGenerator.scala @@ -0,0 +1,34 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.protocol._ +import org.scalacheck.Gen + +/** + * Created by chris on 6/12/17. + */ +sealed trait AddressGenerator { + + def p2pkhAddress: Gen[P2PKHAddress] = for { + hash <- CryptoGenerators.sha256Hash160Digest + network <- ChainParamsGenerator.networkParams + addr = P2PKHAddress(hash, network) + } yield addr + + def p2shAddress: Gen[P2SHAddress] = for { + hash <- CryptoGenerators.sha256Hash160Digest + network <- ChainParamsGenerator.networkParams + addr = P2SHAddress(hash, network) + } yield addr + + def bech32Address: Gen[Bech32Address] = for { + (witSPK, _) <- ScriptGenerators.witnessScriptPubKey + network <- ChainParamsGenerator.networkParams + addr = Bech32Address(witSPK, network) + } yield addr + + def bitcoinAddress: Gen[BitcoinAddress] = Gen.oneOf(p2pkhAddress, p2shAddress, bech32Address) + + def address: Gen[Address] = Gen.oneOf(p2pkhAddress, p2shAddress, bech32Address) +} + +object AddressGenerator extends AddressGenerator \ No newline at end of file diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/BlockchainElementsGenerator.scala b/testkit/src/main/scala/org/bitcoins/core/gen/BlockchainElementsGenerator.scala new file mode 100644 index 0000000000..ef507b9bef --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/BlockchainElementsGenerator.scala @@ -0,0 +1,97 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.consensus.Merkle +import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.number.{ Int32, UInt32 } +import org.bitcoins.core.protocol.blockchain.{ Block, BlockHeader } +import org.bitcoins.core.protocol.transaction.{ EmptyTransaction, Transaction } +import org.scalacheck.Gen + +import scala.annotation.tailrec + +/** + * Created by tom on 7/6/16. + */ +sealed abstract class BlockchainElementsGenerator { + /** Generates a block that contains the given txs, plus some more randomly generated ones */ + def block(txs: Seq[Transaction]): Gen[Block] = for { + randomNum <- Gen.choose(1, 10) + neededTxs = if ((randomNum - txs.size) >= 0) randomNum else 0 + genTxs <- Gen.listOfN(neededTxs, TransactionGenerators.transaction) + allTxs = genTxs ++ txs + header <- blockHeader(allTxs) + } yield Block(header, allTxs) + /** + * Generates a random [[Block]], note that we limit this + * to 10 transactions currently + */ + def block: Gen[Block] = for { + header <- blockHeader + txs <- TransactionGenerators.smallTransactions + } yield Block(header, txs) + + /** Generates a random [[BlockHeader]] */ + def blockHeader: Gen[BlockHeader] = for { + previousBlockHash <- CryptoGenerators.doubleSha256Digest + b <- blockHeader(previousBlockHash) + } yield b + + /** Generates a random [[BlockHeader]] with the specified previousBlockHash */ + def blockHeader(previousBlockHash: DoubleSha256Digest): Gen[BlockHeader] = for { + nBits <- NumberGenerator.uInt32s + b <- blockHeader(previousBlockHash, nBits) + } yield b + + /** Generates a random [[BlockHeader]] where you can specify the previousBlockHash and nBits */ + def blockHeader(previousBlockHash: DoubleSha256Digest, nBits: UInt32): Gen[BlockHeader] = for { + numTxs <- Gen.choose(1, 5) + txs <- Gen.listOfN(numTxs, TransactionGenerators.transaction) + header <- blockHeader(previousBlockHash, nBits, txs) + } yield header + + /** Generates a [[BlockHeader]]] that has the fields set to the given values */ + def blockHeader(previousBlockHash: DoubleSha256Digest, nBits: UInt32, txs: Seq[Transaction]): Gen[BlockHeader] = for { + version <- NumberGenerator.int32s + merkleRootHash = Merkle.computeMerkleRoot(txs) + time <- NumberGenerator.uInt32s + nonce <- NumberGenerator.uInt32s + } yield BlockHeader(version, previousBlockHash, merkleRootHash, time, nBits, nonce) + + /** Generates a [[BlockHeader]] that has a merkle root hash corresponding to the given txs */ + def blockHeader(txs: Seq[Transaction]): Gen[BlockHeader] = for { + previousBlockHash <- CryptoGenerators.doubleSha256Digest + nBits <- NumberGenerator.uInt32s + header <- blockHeader(previousBlockHash, nBits, txs) + } yield header + + /** + * Generates a chain of valid headers of the size specified by num, + * 'valid' means their nBits are the same and each header properly + * references the previous block header's hash + */ + def validHeaderChain(num: Long): Gen[Seq[BlockHeader]] = { + blockHeader.flatMap { startHeader => + validHeaderChain(num, startHeader) + } + } + + def validHeaderChain(num: Long, startHeader: BlockHeader): Gen[Seq[BlockHeader]] = { + @tailrec + def loop(remainingHeaders: Long, accum: Seq[BlockHeader]): Seq[BlockHeader] = { + if (remainingHeaders == 0) accum.reverse + else { + val nextHeader = buildBlockHeader(accum.head.hash, accum.head.nBits) + loop(remainingHeaders - 1, nextHeader +: accum) + } + } + loop(num - 1, Seq(startHeader)) + } + + private def buildBlockHeader(prevBlockHash: DoubleSha256Digest, nBits: UInt32): BlockHeader = { + //nonce for the unique hash + val nonce = NumberGenerator.uInt32s.sample.get + BlockHeader(Int32.one, prevBlockHash, EmptyTransaction.txId, UInt32.one, nBits, nonce) + } +} + +object BlockchainElementsGenerator extends BlockchainElementsGenerator diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/BloomFilterGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/BloomFilterGenerators.scala new file mode 100644 index 0000000000..0134c6128b --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/BloomFilterGenerators.scala @@ -0,0 +1,38 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.bloom._ +import org.scalacheck.Gen +import scodec.bits.ByteVector + +/** + * Created by chris on 8/7/16. + */ +abstract class BloomFilterGenerator { + + /** Builds a generic bloom filter loaded with no hashes and returns it */ + def bloomFilter: Gen[BloomFilter] = for { + size <- Gen.choose(1, 100) + falsePositiveRate <- Gen.choose(0.00001, 0.99999) + tweak <- NumberGenerator.uInt32s + flags <- bloomFlag + } yield BloomFilter(size, falsePositiveRate, tweak, flags) + + /** Loads a generic bloom filter with the given byte vectors and returns it */ + def bloomFilter(byteVectors: Seq[ByteVector]): Gen[BloomFilter] = for { + filter <- bloomFilter + } yield filter.insertByteVectors(byteVectors) + + /** Returns a bloom filter loaded with randomly generated byte vectors */ + def loadedBloomFilter: Gen[(BloomFilter, Seq[ByteVector])] = for { + filter <- bloomFilter + randomNum <- Gen.choose(0, filter.filterSize.num.toInt) + hashes <- CryptoGenerators.doubleSha256DigestSeq(randomNum) + loaded = filter.insertHashes(hashes) + } yield (loaded, hashes.map(_.bytes)) + + /** Generates a random bloom flag */ + def bloomFlag: Gen[BloomFlag] = Gen.oneOf(BloomUpdateNone, BloomUpdateAll, BloomUpdateP2PKOnly) + +} + +object BloomFilterGenerator extends BloomFilterGenerator diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/ChainParamsGenerator.scala b/testkit/src/main/scala/org/bitcoins/core/gen/ChainParamsGenerator.scala new file mode 100644 index 0000000000..e3683927f2 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/ChainParamsGenerator.scala @@ -0,0 +1,16 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.config._ +import org.scalacheck.Gen + +/** + * Created by chris on 6/6/17. + */ +sealed abstract class ChainParamsGenerator { + + def networkParams: Gen[NetworkParameters] = bitcoinNetworkParams + + def bitcoinNetworkParams: Gen[BitcoinNetwork] = Gen.oneOf(MainNet, TestNet3, RegTest) +} + +object ChainParamsGenerator extends ChainParamsGenerator diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/CreditingTxGen.scala b/testkit/src/main/scala/org/bitcoins/core/gen/CreditingTxGen.scala new file mode 100644 index 0000000000..10fd167448 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/CreditingTxGen.scala @@ -0,0 +1,182 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.crypto.Sign +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.protocol.script._ +import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.core.script.crypto.HashType +import org.bitcoins.core.wallet.utxo.BitcoinUTXOSpendingInfo +import org.scalacheck.Gen + +sealed abstract class CreditingTxGen { + /** Minimum amount of outputs to generate */ + private val min = 1 + /** Maximum amount of outputs to generate */ + private val max = 3 + + /** Note this generator does NOT generate outputs with negative values */ + private def nonEmptyOutputs: Gen[Seq[TransactionOutput]] = Gen.choose(1, 5).flatMap { n => + Gen.listOfN(n, TransactionGenerators.realisticOutput) + } + + def rawOutput: Gen[BitcoinUTXOSpendingInfo] = { + Gen.oneOf(p2pkOutput, p2pkhOutput, multiSigOutput, /*cltvOutput,*/ csvOutput, p2wpkhOutput) + } + + def rawOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, rawOutput)) + + def basicOutput: Gen[BitcoinUTXOSpendingInfo] = { + Gen.oneOf(p2pkOutput, p2pkhOutput, multiSigOutput) + } + + def nonP2WSHOutput: Gen[BitcoinUTXOSpendingInfo] = { + //note, cannot put a p2wpkh here + Gen.oneOf(p2pkOutput, p2pkhOutput, multiSigOutput, /*cltvOutput,*/ csvOutput) + } + + def nonP2SHOutput: Gen[BitcoinUTXOSpendingInfo] = { + Gen.oneOf(p2pkOutput, p2pkhOutput, multiSigOutput, /*cltvOutput,*/ csvOutput, p2wpkhOutput, p2wshOutput) + } + + def output: Gen[BitcoinUTXOSpendingInfo] = Gen.oneOf( + p2pkOutput, + p2pkhOutput, multiSigOutput, p2shOutput, + csvOutput, /*cltvOutput,*/ + p2wpkhOutput, p2wshOutput) + + def outputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = { + Gen.choose(min, 5).flatMap(n => Gen.listOfN(n, output)) + } + + /** Generates a crediting tx with a p2pk spk at the returned index */ + def p2pkOutput: Gen[BitcoinUTXOSpendingInfo] = ScriptGenerators.p2pkScriptPubKey.flatMap { p2pk => + build(p2pk._1, Seq(p2pk._2), None, None) + } + + /** Generates multiple crediting txs with p2pk spks at the returned index */ + def p2pkOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = { + Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2pkOutput)) + } + + /** + * Generates a transaction that has a p2pkh output at the returned index. This + * output can be spent by the returned ECPrivateKey + */ + def p2pkhOutput: Gen[BitcoinUTXOSpendingInfo] = ScriptGenerators.p2pkhScriptPubKey.flatMap { p2pkh => + build(p2pkh._1, Seq(p2pkh._2), None, None) + } + + /** Generates a sequence of p2pkh outputs at the returned index */ + def p2pkhOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = { + Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2pkhOutput)) + } + + def multiSigOutput: Gen[BitcoinUTXOSpendingInfo] = ScriptGenerators.multiSigScriptPubKey.flatMap { multisig => + build(multisig._1, multisig._2, None, None) + } + + def multiSigOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = { + Gen.choose(min, max).flatMap(n => Gen.listOfN(n, multiSigOutput)) + } + + def p2shOutput: Gen[BitcoinUTXOSpendingInfo] = nonP2SHOutput.flatMap { o => + CryptoGenerators.hashType.map { hashType => + val oldOutput = o.output + val redeemScript = o.output.scriptPubKey + val p2sh = P2SHScriptPubKey(redeemScript) + val updatedOutput = TransactionOutput(oldOutput.value, p2sh) + BitcoinUTXOSpendingInfo(TransactionOutPoint(o.outPoint.txId, o.outPoint.vout), updatedOutput, o.signers, Some(redeemScript), o.scriptWitnessOpt, hashType) + } + } + + def p2shOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = { + Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2shOutput)) + } + + def cltvOutput: Gen[BitcoinUTXOSpendingInfo] = TransactionGenerators.spendableCLTVValues.flatMap { + case (scriptNum, _) => + basicOutput.flatMap { o => + CryptoGenerators.hashType.map { hashType => + val oldOutput = o.output + val csvSPK = CLTVScriptPubKey(scriptNum, oldOutput.scriptPubKey) + val updatedOutput = TransactionOutput(oldOutput.value, csvSPK) + BitcoinUTXOSpendingInfo(TransactionOutPoint(o.outPoint.txId, o.outPoint.vout), updatedOutput, o.signers, o.redeemScriptOpt, o.scriptWitnessOpt, hashType) + } + } + } + + def cltvOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, cltvOutput)) + + def csvOutput: Gen[BitcoinUTXOSpendingInfo] = TransactionGenerators.spendableCSVValues.flatMap { + case (scriptNum, _) => + basicOutput.flatMap { o => + CryptoGenerators.hashType.map { hashType => + val oldOutput = o.output + val csvSPK = CSVScriptPubKey(scriptNum, oldOutput.scriptPubKey) + val updatedOutput = TransactionOutput(oldOutput.value, csvSPK) + BitcoinUTXOSpendingInfo(TransactionOutPoint(o.outPoint.txId, o.outPoint.vout), updatedOutput, o.signers, o.redeemScriptOpt, o.scriptWitnessOpt, hashType) + } + } + } + + def csvOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, csvOutput)) + + def p2wpkhOutput: Gen[BitcoinUTXOSpendingInfo] = ScriptGenerators.p2wpkhSPKV0.flatMap { witSPK => + val scriptWit = P2WPKHWitnessV0(witSPK._2.head.publicKey) + build(witSPK._1, witSPK._2, None, Some(scriptWit)) + } + + def p2wpkhOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2wpkhOutput)) + + def p2wshOutput: Gen[BitcoinUTXOSpendingInfo] = nonP2WSHOutput.flatMap { + case BitcoinUTXOSpendingInfo(_, txOutput, signer, _, _, _) => + val spk = txOutput.scriptPubKey + val scriptWit = P2WSHWitnessV0(spk) + val witSPK = P2WSHWitnessSPKV0(spk) + build(witSPK, signer, None, Some(scriptWit)) + } + + def p2wshOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2wshOutput)) + + /** A nested output is a p2sh/p2wsh wrapped output */ + def nestedOutput: Gen[BitcoinUTXOSpendingInfo] = { + Gen.oneOf(p2wshOutput, p2shOutput) + } + + def nestedOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, nestedOutput)) + + def random: Gen[BitcoinUTXOSpendingInfo] = nonEmptyOutputs.flatMap { outputs => + Gen.choose(0, outputs.size - 1).flatMap { outputIndex: Int => + ScriptGenerators.scriptPubKey.flatMap { + case (spk, keys) => + WitnessGenerators.scriptWitness.flatMap { wit: ScriptWitness => + CryptoGenerators.hashType.map { hashType: HashType => + val tc = TransactionConstants + val signers: Seq[Sign] = keys + val creditingTx = BaseTransaction(tc.validLockVersion, Nil, outputs, tc.lockTime) + BitcoinUTXOSpendingInfo(TransactionOutPoint(creditingTx.txId, UInt32.apply(outputIndex)), TransactionOutput(creditingTx.outputs(outputIndex).value, creditingTx.outputs(outputIndex).scriptPubKey), signers, Some(spk), Some(wit), hashType) + } + } + } + } + } + + def randoms: Gen[Seq[BitcoinUTXOSpendingInfo]] = Gen.choose(min, max).flatMap(n => Gen.listOfN(n, random)) + + private def build(spk: ScriptPubKey, signers: Seq[Sign], + redeemScript: Option[ScriptPubKey], + scriptWitness: Option[ScriptWitness]): Gen[BitcoinUTXOSpendingInfo] = nonEmptyOutputs.flatMap { outputs => + CryptoGenerators.hashType.flatMap { hashType => + Gen.choose(0, outputs.size - 1).map { idx => + val old = outputs(idx) + val updated = outputs.updated(idx, TransactionOutput(old.value, spk)) + val tc = TransactionConstants + val btx = BaseTransaction(tc.version, Nil, updated, tc.lockTime) + BitcoinUTXOSpendingInfo(TransactionOutPoint(btx.txId, UInt32.apply(idx)), TransactionOutput(old.value, spk), signers, redeemScript, scriptWitness, hashType) + } + } + } +} + +object CreditingTxGen extends CreditingTxGen { +} diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/CryptoGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/CryptoGenerators.scala new file mode 100644 index 0000000000..0f9e1adfe0 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/CryptoGenerators.scala @@ -0,0 +1,104 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.crypto._ +import org.bitcoins.core.script.crypto.HashType +import org.bitcoins.core.util.CryptoUtil +import org.scalacheck.Gen + +/** + * Created by chris on 6/22/16. + */ +sealed abstract class CryptoGenerators { + + def privateKey: Gen[ECPrivateKey] = Gen.const(ECPrivateKey()) + + /** + * Generate a sequence of private keys + * @param num maximum number of keys to generate + * @return + */ + def privateKeySeq(num: Int): Gen[Seq[ECPrivateKey]] = Gen.listOfN(num, privateKey) + + /** + * Generates a sequence of private keys, and determines an amount of 'required' private keys + * that a transaction needs to be signed with + * @param num the maximum number of keys to generate + * @return + */ + def privateKeySeqWithRequiredSigs(num: Int): Gen[(Seq[ECPrivateKey], Int)] = { + if (num <= 0) { + Gen.const(Nil, 0) + } else { + val privateKeys = privateKeySeq(num) + for { + keys <- privateKeys + requiredSigs <- Gen.choose(0, keys.size - 1) + } yield (keys, requiredSigs) + } + } + + /** + * Generates a random number of private keys less than the max public keys setting in [[ScriptSettings]] + * also generates a random 'requiredSigs' number that a transaction needs to be signed with + */ + def privateKeySeqWithRequiredSigs: Gen[(Seq[ECPrivateKey], Int)] = for { + num <- Gen.choose(0, 15) + keysAndRequiredSigs <- privateKeySeqWithRequiredSigs(num) + } yield keysAndRequiredSigs + + /** A generator with 7 or less private keys -- useful for creating smaller scripts */ + def smallPrivateKeySeqWithRequiredSigs: Gen[(Seq[ECPrivateKey], Int)] = for { + num <- Gen.choose(0, 7) + keysAndRequiredSigs <- privateKeySeqWithRequiredSigs(num) + } yield keysAndRequiredSigs + + /** Generates a random public key */ + def publicKey: Gen[ECPublicKey] = for { + privKey <- privateKey + } yield privKey.publicKey + + /** Generates a random digital signature */ + def digitalSignature: Gen[ECDigitalSignature] = for { + privKey <- privateKey + hash <- CryptoGenerators.doubleSha256Digest + } yield privKey.sign(hash) + + /** Generates a random [[DoubleSha256Digest]] */ + def doubleSha256Digest: Gen[DoubleSha256Digest] = for { + hex <- StringGenerators.hexString + digest = CryptoUtil.doubleSHA256(hex) + } yield digest + + /** + * Generates a sequence of [[DoubleSha256Digest]] + * @param num the number of digets to generate + * @return + */ + def doubleSha256DigestSeq(num: Int): Gen[Seq[DoubleSha256Digest]] = Gen.listOfN(num, doubleSha256Digest) + + /** Generates a random [[org.bitcoins.core.crypto.Sha256Hash160Digest]] */ + def sha256Hash160Digest: Gen[Sha256Hash160Digest] = for { + pubKey <- publicKey + hash = CryptoUtil.sha256Hash160(pubKey.bytes) + } yield hash + + /** Generates a random [[HashType]] */ + def hashType: Gen[HashType] = Gen.oneOf(HashType.sigHashAll, HashType.sigHashNone, HashType.sigHashSingle, + HashType.sigHashAnyoneCanPay, HashType.sigHashSingleAnyoneCanPay, HashType.sigHashNoneAnyoneCanPay, + HashType.sigHashAllAnyoneCanPay) + + def extVersion: Gen[ExtKeyVersion] = Gen.oneOf(MainNetPriv, MainNetPub, TestNet3Priv, TestNet3Pub) + + /** Generates an [[org.bitcoins.core.crypto.ExtPrivateKey]] */ + def extPrivateKey: Gen[ExtPrivateKey] = for { + version <- Gen.oneOf(MainNetPriv, TestNet3Priv) + ext = ExtPrivateKey(version) + } yield ext + + def extPublicKey: Gen[ExtPublicKey] = extPrivateKey.map(_.extPublicKey) + + def extKey: Gen[ExtKey] = Gen.oneOf(extPrivateKey, extPublicKey) + +} + +object CryptoGenerators extends CryptoGenerators \ No newline at end of file diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/CurrencyUnitGenerator.scala b/testkit/src/main/scala/org/bitcoins/core/gen/CurrencyUnitGenerator.scala new file mode 100644 index 0000000000..2fba1a1028 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/CurrencyUnitGenerator.scala @@ -0,0 +1,59 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.currency.{ Bitcoins, CurrencyUnit, CurrencyUnits, Satoshis } +import org.bitcoins.core.number.Int64 +import org.bitcoins.core.protocol.ln.currency._ +import org.scalacheck.Gen + +/** + * Created by chris on 6/23/16. + */ +trait CurrencyUnitGenerator { + + def satoshis: Gen[Satoshis] = for { + int64 <- NumberGenerator.int64s + } yield Satoshis(int64) + + def bitcoins: Gen[Bitcoins] = for { + sat <- satoshis + } yield Bitcoins(sat) + + def currencyUnit: Gen[CurrencyUnit] = Gen.oneOf(satoshis, bitcoins) + + def positiveSatoshis: Gen[Satoshis] = satoshis.suchThat(_ >= CurrencyUnits.zero) + + /** + * Generates a postiive satoshi value that is 'realistic'. This current 'realistic' range + * is from 0 to 1,000,000 bitcoin + */ + def positiveRealistic: Gen[Satoshis] = Gen.choose(0, Bitcoins(1000000).satoshis.toLong).map { n => + Satoshis(Int64(n)) + } +} + +object CurrencyUnitGenerator extends CurrencyUnitGenerator + +trait LnCurrencyUnitGenerator { + + def milliBitcoin: Gen[MilliBitcoins] = for { + amount <- Gen.choose(MilliBitcoins.min.toLong, MilliBitcoins.max.toLong) + } yield MilliBitcoins(amount) + + def microBitcoin: Gen[MicroBitcoins] = for { + amount <- Gen.choose(MicroBitcoins.min.toLong, MicroBitcoins.max.toLong) + } yield MicroBitcoins(amount) + + def nanoBitcoin: Gen[NanoBitcoins] = for { + amount <- Gen.choose(NanoBitcoins.min.toLong, NanoBitcoins.max.toLong) + } yield NanoBitcoins(amount) + + def picoBitcoin: Gen[PicoBitcoins] = for { + amount <- Gen.choose(PicoBitcoins.min.toLong, PicoBitcoins.max.toLong) + } yield PicoBitcoins(amount) + + def lnCurrencyUnit: Gen[LnCurrencyUnit] = Gen.oneOf(milliBitcoin, microBitcoin, nanoBitcoin, picoBitcoin) + + def negativeLnCurrencyUnit: Gen[LnCurrencyUnit] = lnCurrencyUnit.suchThat(_ < LnCurrencyUnits.zero) +} + +object LnCurrencyUnitGenerator extends LnCurrencyUnitGenerator \ No newline at end of file diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/MerkleGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/MerkleGenerators.scala new file mode 100644 index 0000000000..6c279d9b50 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/MerkleGenerators.scala @@ -0,0 +1,67 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.bloom.BloomFilter +import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.protocol.blockchain.{ Block, MerkleBlock, PartialMerkleTree } +import org.bitcoins.core.protocol.transaction.Transaction +import org.scalacheck.Gen + +/** + * Created by chris on 8/12/16. + */ +abstract class MerkleGenerator { + + /** Generates a merkle block with the given txs matched inside the [[PartialMerkleTree]] */ + def merkleBlockWithInsertedTxIds(txs: Seq[Transaction]): Gen[(MerkleBlock, Block, Seq[DoubleSha256Digest])] = for { + block <- BlockchainElementsGenerator.block(txs) + txIds = txs.map(_.txId) + merkleBlock = MerkleBlock(block, txIds) + } yield (merkleBlock, block, txIds) + + /** Returns a [[MerkleBlock]] including the sequence of hashes inserted in to the bloom filter */ + def merkleBlockWithInsertedTxIds: Gen[(MerkleBlock, Block, Seq[DoubleSha256Digest])] = for { + //TODO: Revisit this later, I've limited this to increase test speed. If I increase this from 5 tests take a lot longer + rand <- Gen.choose(1, 5) + txs <- Gen.listOfN(rand, TransactionGenerators.transaction) + result <- merkleBlockWithInsertedTxIds(txs) + } yield result + + /** + * Returns a [[MerkleBlock]] created with a [[org.bitcoins.core.bloom.BloomFilter]], with the block it was created from + * and the transactions that were matched inside of that block + * NOTE: Since bloom filters can produce false positives, it is possible that there will be + * matches in the parital merkle tree that SHOULD NOT be matched. Bloom filters do not guaratnee no + * false negatives. + * @return + */ + def merkleBlockCreatedWithBloomFilter: Gen[(MerkleBlock, Block, Seq[DoubleSha256Digest], BloomFilter)] = for { + block <- BlockchainElementsGenerator.block + //choose some random txs in the block to put in the bloom filter + txIds <- Gen.someOf(block.transactions.map(_.txId)) + initialFilter <- BloomFilterGenerator.bloomFilter(txIds.map(_.bytes)) + } yield { + val (merkleBlock, loadedFilter) = MerkleBlock(block, initialFilter) + (merkleBlock, block, txIds, loadedFilter) + } + + /** Generates a partial merkle tree with a sequence of txids and a flag indicating if the txid was matched */ + def partialMerkleTree: Gen[(PartialMerkleTree, Seq[(Boolean, DoubleSha256Digest)])] = for { + randomNum <- Gen.choose(1, 25) + txMatches <- txIdsWithMatchIndication(randomNum) + } yield (PartialMerkleTree(txMatches), txMatches) + + /** + * Generates a transaction ids with a boolean indicator if they match the bloom filter or not + * this is useful for testing partial merkle trees as this is how they are built. + * @return + */ + private def txIdWithMatchIndication: Gen[(Boolean, DoubleSha256Digest)] = for { + hash <- CryptoGenerators.doubleSha256Digest + bool <- Gen.choose(0, 1) + } yield (bool == 1, hash) + + /** Generates a list of txids with a boolean indicator signifying if it matched the bloom filter or not */ + def txIdsWithMatchIndication(num: Int): Gen[Seq[(Boolean, DoubleSha256Digest)]] = Gen.listOfN(num, txIdWithMatchIndication) +} + +object MerkleGenerator extends MerkleGenerator diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/NumberGenerator.scala b/testkit/src/main/scala/org/bitcoins/core/gen/NumberGenerator.scala new file mode 100644 index 0000000000..785c9645ce --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/NumberGenerator.scala @@ -0,0 +1,93 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.number._ +import org.bitcoins.core.protocol.CompactSizeUInt +import org.bitcoins.core.script.constant.ScriptNumber +import org.bitcoins.core.util.NumberUtil +import org.scalacheck.Gen +import org.scalacheck.Arbitrary.arbitrary +import scodec.bits.BitVector + +/** + * Created by chris on 6/16/16. + */ +trait NumberGenerator { + + /** Creates a generator that generates positive long numbers */ + def positiveLongs: Gen[Long] = Gen.choose(0, Long.MaxValue) + + /** Creates a generator for positive longs without the number zero */ + def positiveLongsNoZero: Gen[Long] = Gen.choose(1, Long.MaxValue) + + /** Creates a number generator that generates negative long numbers */ + def negativeLongs: Gen[Long] = Gen.choose(Long.MinValue, -1) + + def uInt5: Gen[UInt5] = Gen.choose(0, 31).map(n => UInt5(n)) + + def uInt5s: Gen[Seq[UInt5]] = Gen.listOf(uInt5) + + def uInt8: Gen[UInt8] = Gen.choose(0, 255).map(n => UInt8(n.toShort)) + + def uInt8s: Gen[Seq[UInt8]] = Gen.listOf(uInt8) + /** + * Generates a number in the range 0 <= x <= 2 ^^32 - 1 + * then wraps it in a UInt32 + */ + def uInt32s: Gen[UInt32] = Gen.choose(0L, (NumberUtil.pow2(32) - 1).toLong).map(UInt32(_)) + + /** Chooses a BigInt in the ranges of 0 <= bigInt < 2^^64 */ + def bigInts: Gen[BigInt] = Gen.chooseNum(Long.MinValue, Long.MaxValue) + .map(x => BigInt(x) + BigInt(2).pow(63)) + + def positiveBigInts: Gen[BigInt] = bigInts.filter(_ >= 0) + + def bigIntsUInt64Range: Gen[BigInt] = positiveBigInts.filter(_ < (BigInt(1) << 64)) + + /** + * Generates a number in the range 0 <= x < 2^^64 + * then wraps it in a UInt64 + */ + def uInt64s: Gen[UInt64] = for { + bigInt <- bigIntsUInt64Range + } yield UInt64(bigInt) + + def int32s: Gen[Int32] = Gen.choose(Int32.min.toLong, Int32.max.toLong).map(Int32(_)) + + def int64s: Gen[Int64] = Gen.choose(Int64.min.toLong, Int64.max.toLong).map(Int64(_)) + + def scriptNumbers: Gen[ScriptNumber] = Gen.choose(Int64.min.toLong, Int64.max.toLong).map(ScriptNumber(_)) + + def positiveScriptNumbers: Gen[ScriptNumber] = Gen.choose(0L, Int64.max.toLong).map(ScriptNumber(_)) + + def compactSizeUInts: Gen[CompactSizeUInt] = uInt64s.map(CompactSizeUInt(_)) + + /** Generates an arbitrary [[Byte]] in Scala */ + def byte: Gen[Byte] = arbitrary[Byte] + + /** Generates a 100 byte sequence */ + def bytes: Gen[List[Byte]] = for { + num <- Gen.choose(0, 100) + b <- bytes(num) + } yield b + + /** + * Generates the number of bytes specified by num + * @param num + * @return + */ + def bytes(num: Int): Gen[List[Byte]] = Gen.listOfN(num, byte) + + /** Generates a random boolean */ + def bool: Gen[Boolean] = for { + num <- Gen.choose(0, 1) + } yield num == 1 + + /** Generates a bit vector */ + def bitVector: Gen[BitVector] = for { + n <- Gen.choose(0, 100) + vector <- Gen.listOfN(n, bool) + } yield BitVector.bits(vector) + +} + +object NumberGenerator extends NumberGenerator diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/ScriptGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/ScriptGenerators.scala new file mode 100644 index 0000000000..3bdf5181d3 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/ScriptGenerators.scala @@ -0,0 +1,544 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.consensus.Consensus +import org.bitcoins.core.crypto.{ TransactionSignatureCreator, _ } +import org.bitcoins.core.currency.{ CurrencyUnit, CurrencyUnits } +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.policy.Policy +import org.bitcoins.core.protocol.script.{ P2SHScriptPubKey, _ } +import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.core.script.constant.{ ScriptNumber, _ } +import org.bitcoins.core.script.crypto.HashType +import org.bitcoins.core.util.BitcoinSLogger +import org.bitcoins.core.wallet.signer.{ MultiSigSigner, P2PKHSigner, P2PKSigner } +import org.scalacheck.Gen + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.DurationInt + +/** + * Created by chris on 6/22/16. + */ +//TODO: Need to provide generators for [[NonStandardScriptSignature]] and [[NonStandardScriptPubKey]] +sealed abstract class ScriptGenerators extends BitcoinSLogger { + val timeout = 5.seconds + + def p2pkScriptSignature: Gen[P2PKScriptSignature] = for { + digitalSignature <- CryptoGenerators.digitalSignature + } yield P2PKScriptSignature(digitalSignature) + + def p2pkhScriptSignature: Gen[P2PKHScriptSignature] = for { + privKey <- CryptoGenerators.privateKey + hash <- CryptoGenerators.doubleSha256Digest + signature = privKey.sign(hash) + } yield P2PKHScriptSignature(signature, privKey.publicKey) + + def multiSignatureScriptSignature: Gen[MultiSignatureScriptSignature] = { + val signatures: Gen[Seq[ECDigitalSignature]] = for { + numKeys <- Gen.choose(1, Consensus.maxPublicKeysPerMultiSig) + hash <- CryptoGenerators.doubleSha256Digest + } yield for { + _ <- 0 until numKeys + privKey = ECPrivateKey() + } yield privKey.sign(hash) + signatures.map(sigs => MultiSignatureScriptSignature(sigs)) + } + + def emptyScriptSignature = p2pkhScriptSignature.map(_ => EmptyScriptSignature) + + /** + * Generates a [[org.bitcoins.core.protocol.script.P2SHScriptSignature]] + * WARNING: the redeem script and the script signature DO NOT evaluate to true + * if executed by [[org.bitcoins.core.script.interpreter.ScriptInterpreter]] + */ + def p2shScriptSignature: Gen[P2SHScriptSignature] = for { + (scriptPubKey, _) <- randomNonP2SHScriptPubKey + scriptSig <- pickCorrespondingScriptSignature(scriptPubKey) + p2shScriptSig = P2SHScriptSignature(scriptSig, scriptPubKey) + } yield p2shScriptSig + + def cltvScriptSignature: Gen[CLTVScriptSignature] = for { + scriptSig <- randomNonLockTimeScriptSig + } yield CLTVScriptSignature(scriptSig) + + def csvScriptSignature: Gen[CSVScriptSignature] = for { + scriptSig <- randomNonLockTimeScriptSig + } yield CSVScriptSignature(scriptSig) + + def p2pkScriptPubKey: Gen[(P2PKScriptPubKey, ECPrivateKey)] = for { + privKey <- CryptoGenerators.privateKey + pubKey = privKey.publicKey + p2pk = P2PKScriptPubKey(pubKey) + } yield (p2pk, privKey) + + def p2pkhScriptPubKey: Gen[(P2PKHScriptPubKey, ECPrivateKey)] = for { + privKey <- CryptoGenerators.privateKey + pubKey = privKey.publicKey + p2pkh = P2PKHScriptPubKey(pubKey) + } yield (p2pkh, privKey) + + def cltvScriptPubKey: Gen[(CLTVScriptPubKey, Seq[ECPrivateKey])] = for { + num <- NumberGenerator.scriptNumbers + (cltv, privKeys, num) <- cltvScriptPubKey(num) + } yield (cltv, privKeys) + + def cltvScriptPubKey(num: ScriptNumber): Gen[(CLTVScriptPubKey, Seq[ECPrivateKey], ScriptNumber)] = for { + (scriptPubKey, privKeys) <- randomNonLockTimeNonP2SHScriptPubKey + } yield { + val cltv = CLTVScriptPubKey(num, scriptPubKey) + (cltv, privKeys, num) + } + + def csvScriptPubKey(num: ScriptNumber): Gen[(CSVScriptPubKey, Seq[ECPrivateKey], ScriptNumber)] = for { + (scriptPubKey, privKeys) <- randomNonLockTimeNonP2SHScriptPubKey + } yield { + val csv = CSVScriptPubKey(num, scriptPubKey) + (csv, privKeys, num) + } + + def csvScriptPubKey: Gen[(CSVScriptPubKey, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKeys) <- randomNonLockTimeNonP2SHScriptPubKey + num <- NumberGenerator.scriptNumbers + csv = CSVScriptPubKey(num, scriptPubKey) + } yield (csv, privKeys) + + def multiSigScriptPubKey: Gen[(MultiSignatureScriptPubKey, Seq[ECPrivateKey])] = for { + (privateKeys, requiredSigs) <- CryptoGenerators.privateKeySeqWithRequiredSigs + pubKeys = privateKeys.map(_.publicKey) + multiSignatureScriptPubKey = MultiSignatureScriptPubKey(requiredSigs, pubKeys) + } yield (multiSignatureScriptPubKey, privateKeys) + + def smallMultiSigScriptPubKey: Gen[(MultiSignatureScriptPubKey, Seq[ECPrivateKey])] = for { + (privateKeys, requiredSigs) <- CryptoGenerators.smallPrivateKeySeqWithRequiredSigs + pubKeys = privateKeys.map(_.publicKey) + multiSignatureScriptPubKey = MultiSignatureScriptPubKey(requiredSigs, pubKeys) + } yield (multiSignatureScriptPubKey, privateKeys) + + def p2shScriptPubKey: Gen[(P2SHScriptPubKey, Seq[ECPrivateKey])] = for { + (randomScriptPubKey, privKeys) <- randomNonP2SHScriptPubKey + p2sh = P2SHScriptPubKey(randomScriptPubKey) + } yield (p2sh, privKeys) + + def emptyScriptPubKey: Gen[(ScriptPubKey, Seq[ECPrivateKey])] = (EmptyScriptPubKey, Nil) + + /** Creates a basic version 0 P2WPKH scriptpubkey */ + def p2wpkhSPKV0: Gen[(P2WPKHWitnessSPKV0, Seq[ECPrivateKey])] = for { + privKey <- CryptoGenerators.privateKey + } yield (P2WPKHWitnessSPKV0(privKey.publicKey), Seq(privKey)) + + def p2wshSPKV0: Gen[(P2WSHWitnessSPKV0, Seq[ECPrivateKey])] = randomNonP2SHScriptPubKey.map { spk => + (P2WSHWitnessSPKV0(spk._1), spk._2) + } + + def witnessScriptPubKeyV0: Gen[(WitnessScriptPubKeyV0, Seq[ECPrivateKey])] = Gen.oneOf( + p2wpkhSPKV0, p2wshSPKV0) + + /** + * Creates an [[UnassignedWitnessScriptPubKey]], + * currently this is any witness script pubkey besides [[org.bitcoins.core.protocol.script.WitnessScriptPubKeyV0] + */ + def unassignedWitnessScriptPubKey: Gen[(UnassignedWitnessScriptPubKey, Seq[ECPrivateKey])] = for { + (witV0, privKeys) <- p2wpkhSPKV0 + version <- Gen.oneOf(WitnessScriptPubKey.unassignedWitVersions) + unassignedAsm = version +: witV0.asm.tail + } yield (UnassignedWitnessScriptPubKey(unassignedAsm), privKeys) + + /** Generates an arbitrary [[org.bitcoins.core.protocol.script.WitnessScriptPubKey]] */ + def witnessScriptPubKey: Gen[(WitnessScriptPubKey, Seq[ECPrivateKey])] = Gen.oneOf( + p2wpkhSPKV0, + p2wshSPKV0, unassignedWitnessScriptPubKey) + + def witnessCommitment: Gen[(WitnessCommitment, Seq[ECPrivateKey])] = for { + hash <- CryptoGenerators.doubleSha256Digest + } yield (WitnessCommitment(hash), Nil) + + def escrowTimeoutScriptPubKey: Gen[(EscrowTimeoutScriptPubKey, Seq[ECPrivateKey])] = for { + (escrow, k1) <- ScriptGenerators.smallMultiSigScriptPubKey + //We use a p2pkh scriptPubkey here to minimize the script size of EscrowTimeoutScriptPubKey + //otherwise we surpass the 520 byte push op limit + (p2pkh, _) <- ScriptGenerators.p2pkhScriptPubKey + scriptNum <- NumberGenerator.scriptNumbers + timeout = CSVScriptPubKey(scriptNum, p2pkh) + } yield (EscrowTimeoutScriptPubKey(escrow, timeout), k1) + + def escrowTimeoutScriptPubKey2Of2: Gen[(EscrowTimeoutScriptPubKey, Seq[ECPrivateKey])] = for { + privKey1 <- CryptoGenerators.privateKey + privKey2 <- CryptoGenerators.privateKey + escrowPrivKeys = Seq(privKey1, privKey2) + escrow = MultiSignatureScriptPubKey(2, escrowPrivKeys.map(_.publicKey)) + //We use a p2pkh scriptPubkey here to minimize the script size of EscrowTimeoutScriptPubKey + //otherwise we surpass the 520 byte push op limit + (p2pkh, p2pkhPrivKey) <- ScriptGenerators.p2pkhScriptPubKey + privKeys = escrowPrivKeys ++ Seq(p2pkhPrivKey) + (scriptNum, _) <- TransactionGenerators.spendableCSVValues + timeout = CSVScriptPubKey(scriptNum, p2pkh) + } yield (EscrowTimeoutScriptPubKey(escrow, timeout), privKeys) + + def escrowTimeoutScriptSig: Gen[EscrowTimeoutScriptSignature] = for { + scriptSig <- Gen.oneOf(lockTimeScriptSig, multiSignatureScriptSignature) + bool = if (scriptSig.isInstanceOf[MultiSignatureScriptSignature]) OP_1 else OP_0 + asm = scriptSig.asm ++ Seq(bool) + } yield EscrowTimeoutScriptSignature.fromAsm(asm) + + def randomNonP2SHScriptPubKey: Gen[(ScriptPubKey, Seq[ECPrivateKey])] = { + Gen.oneOf(p2pkScriptPubKey.map(privKeyToSeq(_)), p2pkhScriptPubKey.map(privKeyToSeq(_)), + cltvScriptPubKey.suchThat(!_._1.nestedScriptPubKey.isInstanceOf[CSVScriptPubKey]), + csvScriptPubKey.suchThat(!_._1.nestedScriptPubKey.isInstanceOf[CLTVScriptPubKey]), + multiSigScriptPubKey, p2wpkhSPKV0, unassignedWitnessScriptPubKey, escrowTimeoutScriptPubKey) + } + + /** + * This is used for creating time locked scriptPubKeys, we cannot nest CSV/CLTV/P2SH/Witness + * ScriptPubKeys inside of timelock scriptPubKeys + */ + def randomNonLockTimeNonP2SHScriptPubKey: Gen[(ScriptPubKey, Seq[ECPrivateKey])] = { + Gen.oneOf( + p2pkScriptPubKey.map(privKeyToSeq(_)), + p2pkhScriptPubKey.map(privKeyToSeq(_)), + multiSigScriptPubKey) + } + + def randomNonLockTimeScriptSig: Gen[ScriptSignature] = { + Gen.oneOf(p2pkScriptSignature, p2pkhScriptSignature, multiSignatureScriptSignature, + emptyScriptSignature, p2shScriptSignature) + } + + def lockTimeScriptPubKey: Gen[(LockTimeScriptPubKey, Seq[ECPrivateKey])] = Gen.oneOf(cltvScriptPubKey, csvScriptPubKey) + + def lockTimeScriptSig: Gen[LockTimeScriptSignature] = Gen.oneOf(csvScriptSignature, cltvScriptSignature) + + /** Generates an arbitrary [[ScriptPubKey]] */ + def scriptPubKey: Gen[(ScriptPubKey, Seq[ECPrivateKey])] = { + Gen.oneOf(p2pkScriptPubKey.map(privKeyToSeq(_)), p2pkhScriptPubKey.map(privKeyToSeq(_)), + multiSigScriptPubKey, emptyScriptPubKey, + cltvScriptPubKey, csvScriptPubKey, p2wpkhSPKV0, p2wshSPKV0, unassignedWitnessScriptPubKey, + p2shScriptPubKey, witnessCommitment, escrowTimeoutScriptPubKey) + } + + /** Generates an arbitrary [[ScriptSignature]] */ + def scriptSignature: Gen[ScriptSignature] = { + Gen.oneOf(p2pkScriptSignature, p2pkhScriptSignature, multiSignatureScriptSignature, + emptyScriptSignature, p2shScriptSignature, escrowTimeoutScriptSig + //NOTE: This are commented out because it fail serializatoin symmetry + //sicne we cannot properly type CSV/CLTV ScriptSigs w/o it's SPK + //csvScriptSignature, cltvScriptSignature + ) + } + + /** + * Generates a [[ScriptSignature]] corresponding to the type of [[ScriptPubKey]] given. + * Note: Does NOT generate a correct/valid signature + */ + private def pickCorrespondingScriptSignature(scriptPubKey: ScriptPubKey): Gen[ScriptSignature] = scriptPubKey match { + case _: P2PKScriptPubKey => p2pkScriptSignature + case _: P2PKHScriptPubKey => p2pkhScriptSignature + case _: MultiSignatureScriptPubKey => multiSignatureScriptSignature + case EmptyScriptPubKey => emptyScriptSignature + case _: CLTVScriptPubKey => cltvScriptSignature + case _: CSVScriptPubKey => csvScriptSignature + case _: EscrowTimeoutScriptPubKey => escrowTimeoutScriptSig + case _: WitnessScriptPubKeyV0 | _: UnassignedWitnessScriptPubKey => emptyScriptSignature + case x @ (_: P2SHScriptPubKey | _: NonStandardScriptPubKey | _: WitnessCommitment) => + throw new IllegalArgumentException("Cannot pick for p2sh script pubkey, " + + "non standard script pubkey or witness commitment got: " + x) + } + + /** + * Generates a signed [[P2PKScriptSignature]] that spends the [[P2PKScriptPubKey]] correctly + * + * @return the signed [[P2PKScriptSignature]], the [[P2PKScriptPubKey]] it spends, and the + * [[ECPrivateKey]] used to sign the scriptSig + */ + def signedP2PKScriptSignature: Gen[(P2PKScriptSignature, P2PKScriptPubKey, ECPrivateKey)] = for { + privateKey <- CryptoGenerators.privateKey + hashType <- CryptoGenerators.hashType + publicKey = privateKey.publicKey + scriptPubKey = P2PKScriptPubKey(publicKey) + (creditingTx, outputIndex) = TransactionGenerators.buildCreditingTransaction(scriptPubKey) + scriptSig = P2PKScriptSignature(EmptyDigitalSignature) + (spendingTx, inputIndex) = TransactionGenerators.buildSpendingTransaction(creditingTx, scriptSig, outputIndex) + txSigComponentFuture = P2PKSigner.sign(Seq(privateKey), creditingTx.outputs(outputIndex.toInt), spendingTx, inputIndex, hashType, false) + txSigComponent = Await.result(txSigComponentFuture, timeout) + //add the signature to the scriptSig instead of having an empty scriptSig + signedScriptSig = txSigComponent.scriptSignature.asInstanceOf[P2PKScriptSignature] + } yield (signedScriptSig, scriptPubKey, privateKey) + + /** + * Generates a signed [[P2PKHScriptSignature]] that spends the [[P2PKHScriptPubKey]] correctly + * + * @return the signed [[P2PKHScriptSignature]], the [[P2PKHScriptPubKey]] it spends, and the + * [[ECPrivateKey]] used to sign the scriptSig + */ + def signedP2PKHScriptSignature: Gen[(P2PKHScriptSignature, P2PKHScriptPubKey, ECPrivateKey)] = for { + privateKey <- CryptoGenerators.privateKey + hashType <- CryptoGenerators.hashType + publicKey = privateKey.publicKey + scriptPubKey = P2PKHScriptPubKey(publicKey) + (creditingTx, outputIndex) = TransactionGenerators.buildCreditingTransaction(scriptPubKey) + (unsignedTx, inputIndex) = TransactionGenerators.buildSpendingTransaction(creditingTx, EmptyScriptSignature, outputIndex) + txSigComponentFuture = P2PKHSigner.sign(Seq(privateKey), creditingTx.outputs(outputIndex.toInt), unsignedTx, inputIndex, hashType, false) + txSigComponent = Await.result(txSigComponentFuture, timeout) + signedScriptSig = txSigComponent.scriptSignature.asInstanceOf[P2PKHScriptSignature] + } yield (signedScriptSig, scriptPubKey, privateKey) + + /** + * Generates a signed [[MultiSignatureScriptSignature]] that spends the [[MultiSignatureScriptPubKey]] correctly + * ti + * @return the signed [[MultiSignatureScriptSignature]], the [[MultiSignatureScriptPubKey]] it spends and the + * sequence of [[ECPrivateKey]] used to sign the scriptSig + */ + def signedMultiSignatureScriptSignature: Gen[(MultiSignatureScriptSignature, MultiSignatureScriptPubKey, Seq[ECPrivateKey])] = for { + (privateKeys, requiredSigs) <- CryptoGenerators.privateKeySeqWithRequiredSigs + hashType <- CryptoGenerators.hashType + publicKeys = privateKeys.map(_.publicKey) + multiSigScriptPubKey = MultiSignatureScriptPubKey(requiredSigs, publicKeys) + emptyDigitalSignatures = privateKeys.map(_ => EmptyDigitalSignature) + scriptSig = MultiSignatureScriptSignature(emptyDigitalSignatures) + (creditingTx, outputIndex) = TransactionGenerators.buildCreditingTransaction(multiSigScriptPubKey) + (spendingTx, inputIndex) = TransactionGenerators.buildSpendingTransaction(creditingTx, scriptSig, outputIndex) + txSigComponentFuture = MultiSigSigner.sign(privateKeys, creditingTx.outputs(outputIndex.toInt), spendingTx, inputIndex, hashType, false) + txSigComponent = Await.result(txSigComponentFuture, timeout) + signedScriptSig = txSigComponent.scriptSignature.asInstanceOf[MultiSignatureScriptSignature] + } yield (signedScriptSig, multiSigScriptPubKey, privateKeys) + + /** + * Generates a signed [[P2SHScriptSignature]] that spends from a [[P2SHScriptPubKey]] correctly + * + * @return the signed [[P2SHScriptSignature]], the [[P2SHScriptPubKey]] it spends, and the sequence of [[ECPrivateKey]] + * used to sign the scriptSig + */ + def signedP2SHScriptSignature: Gen[(P2SHScriptSignature, P2SHScriptPubKey, Seq[ECPrivateKey])] = for { + (scriptSig, redeemScript, privateKeys) <- chooseSignedScriptSig + p2SHScriptPubKey = P2SHScriptPubKey(redeemScript) + p2SHScriptSignature = P2SHScriptSignature(scriptSig, redeemScript) + } yield (p2SHScriptSignature, p2SHScriptPubKey, privateKeys) + + /** + * Generates a signed [[CLTVScriptSignature]] that spends from a [[CLTVScriptPubKey]] correctly + * + * @return the signed [[CLTVScriptSignature]], the [[CLTVScriptPubKey]] it spends, and the sequences of [[ECPrivateKey]] + * used to sign the scriptSig + */ + def signedCLTVScriptSignature(cltvLockTime: ScriptNumber, lockTime: UInt32, sequence: UInt32): Gen[(CLTVScriptSignature, CLTVScriptPubKey, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKeys) <- randomNonLockTimeNonP2SHScriptPubKey + hashType <- CryptoGenerators.hashType + cltv = CLTVScriptPubKey(cltvLockTime, scriptPubKey) + } yield scriptPubKey match { + case m: MultiSignatureScriptPubKey => + val requiredSigs = m.requiredSigs + val cltvScriptSig = lockTimeHelper(Some(lockTime), sequence, cltv, privKeys, Some(requiredSigs), hashType) + (cltvScriptSig.asInstanceOf[CLTVScriptSignature], cltv, privKeys) + case _: P2PKHScriptPubKey | _: P2PKScriptPubKey => + val cltvScriptSig = lockTimeHelper(Some(lockTime), sequence, cltv, privKeys, None, hashType) + (cltvScriptSig.asInstanceOf[CLTVScriptSignature], cltv, privKeys) + case _: UnassignedWitnessScriptPubKey | _: WitnessScriptPubKeyV0 => + throw new IllegalArgumentException("Cannot created a witness scriptPubKey for a CSVScriptSig since we do not have a witness") + case _: P2SHScriptPubKey | _: CLTVScriptPubKey | _: CSVScriptPubKey | _: NonStandardScriptPubKey + | _: WitnessCommitment | _: EscrowTimeoutScriptPubKey | EmptyScriptPubKey => throw new IllegalArgumentException("We only " + + "want to generate P2PK, P2PKH, and MultiSig ScriptSignatures when creating a CSVScriptSignature") + } + + /** + * Generates a signed [[CSVScriptSignature]] that spends from a [[CSVScriptPubKey]] correctly + * + * @return the signed [[CSVScriptSignature]], the [[CSVScriptPubKey]] it spends, and the sequences of [[ECPrivateKey]] + * used to sign the scriptSig + */ + def signedCSVScriptSignature(csvScriptNum: ScriptNumber, sequence: UInt32): Gen[(CSVScriptSignature, CSVScriptPubKey, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKeys) <- randomNonLockTimeNonP2SHScriptPubKey + hashType <- CryptoGenerators.hashType + csv = CSVScriptPubKey(csvScriptNum, scriptPubKey) + } yield scriptPubKey match { + case m: MultiSignatureScriptPubKey => + val requiredSigs = m.requiredSigs + val csvScriptSig = lockTimeHelper(None, sequence, csv, privKeys, Some(requiredSigs), hashType) + (csvScriptSig.asInstanceOf[CSVScriptSignature], csv, privKeys) + case _: P2PKHScriptPubKey | _: P2PKScriptPubKey => + val csvScriptSig = lockTimeHelper(None, sequence, csv, privKeys, None, hashType) + (csvScriptSig.asInstanceOf[CSVScriptSignature], csv, privKeys) + case _: UnassignedWitnessScriptPubKey | _: WitnessScriptPubKeyV0 => + throw new IllegalArgumentException("Cannot created a witness scriptPubKey for a CSVScriptSig since we do not have a witness") + case _: P2SHScriptPubKey | _: CLTVScriptPubKey | _: CSVScriptPubKey | _: NonStandardScriptPubKey + | _: WitnessCommitment | _: EscrowTimeoutScriptPubKey | EmptyScriptPubKey => throw new IllegalArgumentException("We only " + + "want to generate P2PK, P2PKH, and MultiSig ScriptSignatures when creating a CLTVScriptSignature.") + } + + def signedCSVScriptSignature: Gen[(CSVScriptSignature, CSVScriptPubKey, Seq[ECPrivateKey])] = for { + (csv, privKeys) <- csvScriptPubKey + sequence <- NumberGenerator.uInt32s + scriptSig <- signedCSVScriptSignature(csv.locktime, sequence) + } yield scriptSig + + def signedCLTVScriptSignature: Gen[(CLTVScriptSignature, CLTVScriptPubKey, Seq[ECPrivateKey])] = for { + (cltv, privKeys) <- cltvScriptPubKey + txLockTime <- NumberGenerator.uInt32s + sequence <- NumberGenerator.uInt32s + scriptSig <- signedCLTVScriptSignature(cltv.locktime, txLockTime, sequence) + } yield scriptSig + + /** Generates a [[LockTimeScriptSignature]] and [[LockTimeScriptPubKey]] pair that are valid when run through the interpreter */ + def signedLockTimeScriptSignature: Gen[(LockTimeScriptSignature, LockTimeScriptPubKey, Seq[ECPrivateKey])] = { + Gen.oneOf(signedCSVScriptSignature, signedCLTVScriptSignature) + } + + /** Generates a [[EscrowTimeoutScriptPubKey]] and [[EscrowTimeoutScriptSignature]] where the scriptsig spends the escrow branch */ + def signedMultiSigEscrowTimeoutScriptSig(sequence: UInt32, outputs: Seq[TransactionOutput], amount: CurrencyUnit): Gen[(EscrowTimeoutScriptSignature, EscrowTimeoutScriptPubKey, Seq[ECPrivateKey])] = for { + (_, csvScriptPubkey, _) <- signedCSVScriptSignature + (_, multiSigScriptPubKey, multiSigPrivKeys) <- signedMultiSignatureScriptSignature + csvEscrowTimeout = EscrowTimeoutScriptPubKey(multiSigScriptPubKey, csvScriptPubkey) + r <- signedMultiSigEscrowTimeoutScriptSig(csvEscrowTimeout, multiSigPrivKeys, sequence, outputs, amount) + } yield r + + def signedMultiSigEscrowTimeoutScriptSig(escrowTimeout: EscrowTimeoutScriptPubKey, privKeys: Seq[ECPrivateKey], + sequence: UInt32, outputs: Seq[TransactionOutput], amount: CurrencyUnit): Gen[(EscrowTimeoutScriptSignature, EscrowTimeoutScriptPubKey, Seq[ECPrivateKey])] = for { + hashType <- CryptoGenerators.hashType + scriptSig = csvEscrowTimeoutHelper(sequence, escrowTimeout, privKeys, Some(escrowTimeout.escrow.requiredSigs), + hashType, true, outputs, amount) + } yield (scriptSig, escrowTimeout, privKeys) + + /** Generates a [[EscrowTimeoutScriptPubKey]] and [[EscrowTimeoutScriptSignature]] where the scriptsig spends the timeout branch */ + def timeoutEscrowTimeoutScriptSig(scriptNum: ScriptNumber, sequence: UInt32, outputs: Seq[TransactionOutput]): Gen[(EscrowTimeoutScriptSignature, EscrowTimeoutScriptPubKey, Seq[ECPrivateKey])] = for { + (_, csv, csvPrivKeys) <- signedCSVScriptSignature(scriptNum, sequence) + (multiSigScriptPubKey, _) <- multiSigScriptPubKey + hashType <- CryptoGenerators.hashType + csvEscrowTimeout = EscrowTimeoutScriptPubKey(multiSigScriptPubKey, csv) + requireSigs = if (csv.nestedScriptPubKey.isInstanceOf[MultiSignatureScriptPubKey]) { + val m = csv.nestedScriptPubKey.asInstanceOf[MultiSignatureScriptPubKey] + Some(m.requiredSigs) + } else None + scriptSig = csvEscrowTimeoutHelper(sequence, csvEscrowTimeout, csvPrivKeys, requireSigs, hashType, false, outputs) + } yield (scriptSig, csvEscrowTimeout, csvPrivKeys) + + /** Helper function to generate [[LockTimeScriptSignature]]s */ + private def lockTimeHelper(lockTime: Option[UInt32], sequence: UInt32, lock: LockTimeScriptPubKey, privateKeys: Seq[ECPrivateKey], requiredSigs: Option[Int], + hashType: HashType): LockTimeScriptSignature = { + val tc = TransactionConstants + val pubKeys = privateKeys.map(_.publicKey) + val (creditingTx, outputIndex) = TransactionGenerators.buildCreditingTransaction(tc.validLockVersion, lock) + val (unsignedSpendingTx, inputIndex) = TransactionGenerators.buildSpendingTransaction(tc.validLockVersion, creditingTx, + EmptyScriptSignature, outputIndex, lockTime.getOrElse(tc.lockTime), sequence) + val output = TransactionOutput(CurrencyUnits.zero, lock) + val txSignatureComponent = BaseTxSigComponent(unsignedSpendingTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + + val txSignatures: Seq[ECDigitalSignature] = for { + i <- 0 until requiredSigs.getOrElse(1) + } yield TransactionSignatureCreator.createSig(txSignatureComponent, privateKeys(i), hashType) + + lock match { + case csv: CSVScriptPubKey => + val nestedScriptSig = lockTimeHelperScriptSig(csv, txSignatures, pubKeys) + CSVScriptSignature(nestedScriptSig) + case cltv: CLTVScriptPubKey => + val nestedScriptSig = lockTimeHelperScriptSig(cltv, txSignatures, pubKeys) + CLTVScriptSignature(nestedScriptSig) + } + } + /** Helper function to generate a signed [[EscrowTimeoutScriptSignature]] */ + private def csvEscrowTimeoutHelper(sequence: UInt32, csvEscrowTimeout: EscrowTimeoutScriptPubKey, privateKeys: Seq[ECPrivateKey], + requiredSigs: Option[Int], hashType: HashType, isMultiSig: Boolean, + outputs: Seq[TransactionOutput], amount: CurrencyUnit = CurrencyUnits.zero): EscrowTimeoutScriptSignature = { + val pubKeys = privateKeys.map(_.publicKey) + val (creditingTx, outputIndex) = TransactionGenerators.buildCreditingTransaction( + TransactionConstants.validLockVersion, csvEscrowTimeout, amount) + val (unsignedSpendingTx, inputIndex) = { + if (outputs.isEmpty) { + TransactionGenerators.buildSpendingTransaction( + TransactionConstants.validLockVersion, + creditingTx, EmptyScriptSignature, outputIndex, UInt32.zero, sequence) + } else { + TransactionGenerators.buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, + EmptyScriptSignature, outputIndex, UInt32.zero, sequence, outputs) + } + } + val output = TransactionOutput(CurrencyUnits.zero, csvEscrowTimeout) + val txSignatureComponent = BaseTxSigComponent(unsignedSpendingTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + + val txSignatures: Seq[ECDigitalSignature] = for { + i <- 0 until requiredSigs.getOrElse(1) + } yield TransactionSignatureCreator.createSig(txSignatureComponent, privateKeys(i), hashType) + + if (isMultiSig) { + EscrowTimeoutScriptSignature.fromMultiSig(MultiSignatureScriptSignature(txSignatures)) + } else { + val nestedScriptSig = lockTimeHelperScriptSig(csvEscrowTimeout.timeout, txSignatures, pubKeys) + EscrowTimeoutScriptSignature(nestedScriptSig).get + } + } + + def signedP2SHP2WPKHScriptSignature: Gen[(P2SHScriptSignature, P2SHScriptPubKey, Seq[ECPrivateKey], TransactionWitness, CurrencyUnit)] = for { + (witness, wtxSigComponent, privKeys) <- WitnessGenerators.signedP2WPKHTransactionWitness + p2shScriptPubKey = P2SHScriptPubKey(wtxSigComponent.scriptPubKey) + p2shScriptSig = P2SHScriptSignature(wtxSigComponent.scriptPubKey.asInstanceOf[WitnessScriptPubKey]) + } yield (p2shScriptSig, p2shScriptPubKey, privKeys, witness, wtxSigComponent.amount) + + def signedP2SHP2WSHScriptSignature: Gen[(P2SHScriptSignature, P2SHScriptPubKey, Seq[ECPrivateKey], TransactionWitness, CurrencyUnit)] = for { + (witness, wtxSigComponent, privKeys) <- WitnessGenerators.signedP2WSHTransactionWitness + p2shScriptPubKey = P2SHScriptPubKey(wtxSigComponent.scriptPubKey) + p2shScriptSig = P2SHScriptSignature(wtxSigComponent.scriptPubKey) + } yield (p2shScriptSig, p2shScriptPubKey, privKeys, witness, wtxSigComponent.amount) + + /** + * This function chooses a random signed [[ScriptSignature]] that is NOT a [[P2SHScriptSignature]], [[CSVScriptSignature]], + * [[CLTVScriptSignature]], or any witness type + * + * @return the signed [[ScriptSignature]], the [[ScriptPubKey]] it is spending, + * and the sequence of[[ECPrivateKey]] used to sign it + */ + def chooseSignedScriptSig: Gen[(ScriptSignature, ScriptPubKey, Seq[ECPrivateKey])] = { + Gen.oneOf( + packageToSequenceOfPrivateKeys(signedP2PKScriptSignature), + packageToSequenceOfPrivateKeys(signedP2PKHScriptSignature), + signedMultiSignatureScriptSignature) + } + + /** Generates a random [[ScriptSignature]], the [[ScriptPubKey]] it is spending, and the [[ECPrivateKey]] needed to spend it. */ + def randomScriptSig: Gen[(ScriptSignature, ScriptPubKey, Seq[ECPrivateKey])] = { + val witP2SHP2WPKH = signedP2SHP2WPKHScriptSignature.map(x => (x._1, x._2, x._3)) + val witP2SHP2WSH = signedP2SHP2WSHScriptSignature.map(x => (x._1, x._2, x._3)) + Gen.oneOf( + packageToSequenceOfPrivateKeys(signedP2PKHScriptSignature), + packageToSequenceOfPrivateKeys(signedP2PKScriptSignature), + signedMultiSignatureScriptSignature, signedCLTVScriptSignature, + signedCSVScriptSignature, signedP2SHScriptSignature, witP2SHP2WPKH, witP2SHP2WSH) + } + + /** Simply converts one private key in the generator to a sequence of private keys */ + private def packageToSequenceOfPrivateKeys(gen: Gen[(ScriptSignature, ScriptPubKey, ECPrivateKey)]): Gen[(ScriptSignature, ScriptPubKey, Seq[ECPrivateKey])] = for { + (scriptSig, scriptPubKey, privateKey) <- gen + } yield (scriptSig, scriptPubKey, Seq(privateKey)) + + /** Simply converts one private key in the generator to a sequence of private keys */ + private def privKeyToSeq(tuple: (ScriptPubKey, ECPrivateKey)): (ScriptPubKey, Seq[ECPrivateKey]) = { + val (s, key) = tuple + (s, Seq(key)) + } + + private def lockTimeHelperScriptSig(lock: LockTimeScriptPubKey, sigs: Seq[ECDigitalSignature], + keys: Seq[ECPublicKey]): LockTimeScriptSignature = { + + val nestedScriptSig = lock.nestedScriptPubKey match { + case _: P2PKScriptPubKey => P2PKScriptSignature(sigs.head) + case _: P2PKHScriptPubKey => P2PKHScriptSignature(sigs.head, keys.head) + case _: MultiSignatureScriptPubKey => MultiSignatureScriptSignature(sigs) + case EmptyScriptPubKey => CSVScriptSignature(EmptyScriptSignature) + case _: WitnessScriptPubKeyV0 | _: UnassignedWitnessScriptPubKey => + //bare segwit always has an empty script sig, see BIP141 + CSVScriptSignature(EmptyScriptSignature) + case _: LockTimeScriptPubKey | _: EscrowTimeoutScriptPubKey => + throw new IllegalArgumentException("Cannot have a nested locktimeScriptPubKey inside a lockTimeScriptPubKey") + case x @ (_: NonStandardScriptPubKey | _: P2SHScriptPubKey | _: WitnessCommitment) => + throw new IllegalArgumentException("A NonStandardScriptPubKey/P2SHScriptPubKey/WitnessCommitment cannot be" + + "the underlying scriptSig in a CSVScriptSignature. Got: " + x) + } + + lock match { + case _: CLTVScriptPubKey => CLTVScriptSignature(nestedScriptSig) + case _: CSVScriptPubKey => CSVScriptSignature(nestedScriptSig) + } + } +} + +object ScriptGenerators extends ScriptGenerators diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/StringGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/StringGenerators.scala new file mode 100644 index 0000000000..134773ec5f --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/StringGenerators.scala @@ -0,0 +1,58 @@ +package org.bitcoins.core.gen + +import org.scalacheck.Gen + +/** + * Created by chris on 6/20/16. + */ +trait StringGenerators { + + lazy val validHexChars = "0123456789abcdef".toCharArray + + /** + * Generates a hex char + * + * @return + */ + def hexChar: Gen[Char] = Gen.choose(0, validHexChars.length - 1).map(validHexChars(_)) + + /** + * Generates a random hex string + * + * @return + */ + def hexString: Gen[String] = { + val int = Gen.choose(0, 100) + val hexStringGen: Gen[List[Char]] = int.flatMap { i => + if (i % 2 == 0) Gen.listOfN(i, hexChar) + else Gen.listOfN(i * 2, hexChar) + } + hexStringGen.map(_.mkString) + } + + def strChar: Gen[Char] = { + val char: Gen[Gen[Char]] = for { + randomNum <- Gen.choose(0, 4) + } yield { + if (randomNum == 0) Gen.numChar + else if (randomNum == 1) Gen.alphaUpperChar + else if (randomNum == 2) Gen.alphaLowerChar + else if (randomNum == 3) Gen.alphaChar + else Gen.alphaNumChar + } + char.flatMap(g => g) + } + + def genString(size: Int): Gen[String] = { + val l: Gen[Seq[Char]] = Gen.listOfN(size, strChar) + l.map(_.mkString) + } + + def genString: Gen[String] = for { + randomNum <- Gen.choose(0, 100) + randomString <- genString(randomNum) + } yield randomString + +} + +object StringGenerators extends StringGenerators diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/TransactionGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/TransactionGenerators.scala new file mode 100644 index 0000000000..05b413d049 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/TransactionGenerators.scala @@ -0,0 +1,584 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.crypto._ +import org.bitcoins.core.currency.{ CurrencyUnit, CurrencyUnits, Satoshis } +import org.bitcoins.core.number.{ Int32, Int64, UInt32 } +import org.bitcoins.core.policy.Policy +import org.bitcoins.core.protocol.script._ +import org.bitcoins.core.protocol.transaction.{ TransactionInput, TransactionOutPoint, TransactionOutput, _ } +import org.bitcoins.core.script.constant.ScriptNumber +import org.bitcoins.core.script.locktime.LockTimeInterpreter +import org.bitcoins.core.util.BitcoinSLogger +import org.scalacheck.Gen + +import scala.annotation.tailrec + +/** + * Created by chris on 6/21/16. + */ +trait TransactionGenerators extends BitcoinSLogger { + + /** Responsible for generating [[org.bitcoins.core.protocol.transaction.TransactionOutPoint]] */ + def outPoint: Gen[TransactionOutPoint] = for { + txId <- CryptoGenerators.doubleSha256Digest + vout <- NumberGenerator.uInt32s + } yield TransactionOutPoint(txId, vout) + + /** Generates a random [[org.bitcoins.core.protocol.transaction.TransactionOutput]] */ + def output: Gen[TransactionOutput] = for { + satoshis <- CurrencyUnitGenerator.satoshis + (scriptPubKey, _) <- ScriptGenerators.scriptPubKey + } yield TransactionOutput(satoshis, scriptPubKey) + + def outputs = Gen.listOf(output) + + /** + * Outputs that only have a positive amount of satoshis, techinically the bitcoin protocol allows you + * to have negative value outputs + */ + def realisticOutput: Gen[TransactionOutput] = CurrencyUnitGenerator.positiveRealistic.flatMap { amt => + ScriptGenerators.scriptPubKey.map(spk => TransactionOutput(amt, spk._1)) + } + def realisticOutputs: Gen[Seq[TransactionOutput]] = Gen.choose(0, 5).flatMap(n => Gen.listOfN(n, realisticOutput)) + + /** Generates a small list of [[TransactionOutput]] */ + def smallOutputs: Gen[Seq[TransactionOutput]] = Gen.choose(0, 5).flatMap(i => Gen.listOfN(i, output)) + + /** Creates a small sequence of outputs whose total sum is <= totalAmount */ + def smallOutputs(totalAmount: CurrencyUnit): Gen[Seq[TransactionOutput]] = { + val numOutputs = Gen.choose(0, 5).sample.get + @tailrec + def loop(remaining: Int, remainingAmount: CurrencyUnit, accum: Seq[CurrencyUnit]): Seq[CurrencyUnit] = { + if (remaining <= 0) { + accum + } else { + val amt = Gen.choose(100, remainingAmount.toBigDecimal.toLongExact).map(n => Satoshis(Int64(n))).sample.get + loop(remaining - 1, remainingAmount - amt, amt +: accum) + } + } + val amts = loop(numOutputs, totalAmount, Nil) + val spks = Gen.listOfN(numOutputs, ScriptGenerators.scriptPubKey.map(_._1)) + spks.flatMap { s => + s.zip(amts).map { + case (spk, amt) => + TransactionOutput(amt, spk) + } + } + } + + /** Generates a random [[org.bitcoins.core.protocol.transaction.TransactionInput]] */ + def input: Gen[TransactionInput] = for { + outPoint <- outPoint + scriptSig <- ScriptGenerators.scriptSignature + sequenceNumber <- NumberGenerator.uInt32s + randomNum <- Gen.choose(0, 10) + } yield { + if (randomNum == 0) { + //gives us a coinbase input + CoinbaseInput(scriptSig, sequenceNumber) + } else TransactionInput(outPoint, scriptSig, sequenceNumber) + } + + def inputs: Gen[List[TransactionInput]] = Gen.nonEmptyListOf(input) + + /** Generates a small list of [[TransactionInput]] */ + def smallInputs: Gen[Seq[TransactionInput]] = Gen.choose(1, 5).flatMap(i => Gen.listOfN(i, input)) + + /** Generates a small non empty list of [[TransactionInput]] */ + def smallInputsNonEmpty: Gen[Seq[TransactionInput]] = Gen.choose(1, 5).flatMap(i => Gen.listOfN(i, input)) + /** + * Generates an arbitrary [[org.bitcoins.core.protocol.transaction.Transaction]] + * This transaction's [[TransactionInput]]s will not evaluate to true + * inside of the [[org.bitcoins.core.script.interpreter.ScriptInterpreter]] + */ + def transactions: Gen[Seq[Transaction]] = Gen.listOf(transaction) + + /** Generates a small list of [[Transaction]] */ + def smallTransactions: Gen[Seq[Transaction]] = Gen.choose(0, 10).flatMap(i => Gen.listOfN(i, transaction)) + + def transaction: Gen[Transaction] = Gen.oneOf(baseTransaction, witnessTransaction) + + def baseTransaction: Gen[BaseTransaction] = for { + version <- NumberGenerator.int32s + is <- smallInputs + os <- smallOutputs + lockTime <- NumberGenerator.uInt32s + } yield BaseTransaction(version, is, os, lockTime) + + /** Generates a random [[WitnessTransaction]] */ + def witnessTransaction: Gen[WitnessTransaction] = for { + version <- NumberGenerator.int32s + //we cannot have zero witnesses on a WitnessTx + //https://github.com/bitcoin/bitcoin/blob/e8cfe1ee2d01c493b758a67ad14707dca15792ea/src/primitives/transaction.h#L276-L281 + is <- smallInputsNonEmpty + os <- smallOutputs + lockTime <- NumberGenerator.uInt32s + //we have to have atleast one NON `EmptyScriptWitness` for a tx to be a valid WitnessTransaction, otherwise we + //revert to using the `BaseTransaction` serialization format + //notice we use the old serialization format if all witnesses are empty + //[[https://github.com/bitcoin/bitcoin/blob/e8cfe1ee2d01c493b758a67ad14707dca15792ea/src/primitives/transaction.h#L276-L281]] + witness <- WitnessGenerators.transactionWitness(is.size).suchThat(_.witnesses.exists(_ != EmptyScriptWitness)) + } yield WitnessTransaction(version, is, os, lockTime, witness) + + /** + * Creates a [[ECPrivateKey]], then creates a [[P2PKScriptPubKey]] from that private key + * Finally creates a [[Transaction]] that spends the [[P2PKScriptPubKey]] correctly + */ + def signedP2PKTransaction: Gen[(BaseTxSigComponent, ECPrivateKey)] = for { + (signedScriptSig, scriptPubKey, privateKey) <- ScriptGenerators.signedP2PKScriptSignature + (creditingTx, outputIndex) = buildCreditingTransaction(scriptPubKey) + (signedTx, inputIndex) = buildSpendingTransaction(creditingTx, signedScriptSig, outputIndex) + output = creditingTx.outputs(outputIndex.toInt) + signedTxSignatureComponent = BaseTxSigComponent(signedTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + } yield (signedTxSignatureComponent, privateKey) + + /** + * Creates a [[ECPrivateKey]], then creates a [[P2PKHScriptPubKey]] from that private key + * Finally creates a [[Transaction]] that spends the [[P2PKHScriptPubKey]] correctly + */ + def signedP2PKHTransaction: Gen[(BaseTxSigComponent, ECPrivateKey)] = for { + (signedScriptSig, scriptPubKey, privateKey) <- ScriptGenerators.signedP2PKHScriptSignature + (creditingTx, outputIndex) = buildCreditingTransaction(scriptPubKey) + (signedTx, inputIndex) = buildSpendingTransaction(creditingTx, signedScriptSig, outputIndex) + output = creditingTx.outputs(outputIndex.toInt) + signedTxSignatureComponent = BaseTxSigComponent(signedTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + } yield (signedTxSignatureComponent, privateKey) + + /** + * Creates a sequence of [[ECPrivateKey]], then creates a [[MultiSignatureScriptPubKey]] from those private keys, + * Finally creates a [[Transaction]] that spends the [[MultiSignatureScriptPubKey]] correctly + */ + def signedMultiSigTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (signedScriptSig, scriptPubKey, privateKey) <- ScriptGenerators.signedMultiSignatureScriptSignature + (creditingTx, outputIndex) = buildCreditingTransaction(scriptPubKey) + (signedTx, inputIndex) = buildSpendingTransaction(creditingTx, signedScriptSig, outputIndex) + output = creditingTx.outputs(outputIndex.toInt) + signedTxSignatureComponent = BaseTxSigComponent(signedTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + } yield (signedTxSignatureComponent, privateKey) + + /** + * Creates a transaction which contains a [[P2SHScriptSignature]] that correctly spends a [[P2SHScriptPubKey]] + */ + def signedP2SHTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (signedScriptSig, scriptPubKey, privateKey) <- ScriptGenerators.signedP2SHScriptSignature + (creditingTx, outputIndex) = buildCreditingTransaction(signedScriptSig.redeemScript) + (signedTx, inputIndex) = buildSpendingTransaction(creditingTx, signedScriptSig, outputIndex) + output = TransactionOutput(creditingTx.outputs(outputIndex.toInt).value, scriptPubKey) + signedTxSignatureComponent = BaseTxSigComponent(signedTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + } yield (signedTxSignatureComponent, privateKey) + + /** Generates a validly constructed CLTV transaction, which has a 50/50 chance of being spendable or unspendable. */ + def randomCLTVTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = { + Gen.oneOf(unspendableCLTVTransaction, spendableCLTVTransaction) + } + + /** + * Creates a [[ECPrivateKey]], then creates a [[CLTVScriptPubKey]] from that private key + * Finally creates a [[Transaction]] that CANNNOT spend the [[CLTVScriptPubKey]] because the LockTime requirement + * is not satisfied (i.e. the transaction's lockTime has not surpassed the CLTV value in the [[CLTVScriptPubKey]]) + * + * @return + */ + def unspendableCLTVTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (cltvLockTime, txLockTime) <- unspendableCLTVValues + sequence <- NumberGenerator.uInt32s.suchThat(n => n < UInt32.max) + (scriptSig, scriptPubKey, privKeys) <- ScriptGenerators.signedCLTVScriptSignature(cltvLockTime, txLockTime, sequence) + unspendable = lockTimeTxHelper(scriptSig, scriptPubKey, privKeys, sequence, Some(txLockTime)) + } yield unspendable + + /** + * Creates a [[ECPrivateKey]], then creates a [[CLTVScriptPubKey]] from that private key + * Finally creates a [[Transaction]] that can successfully spend the [[CLTVScriptPubKey]] + */ + def spendableCLTVTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (cltvLockTime, txLockTime) <- spendableCLTVValues + sequence <- NumberGenerator.uInt32s.suchThat(n => n < UInt32.max) + (scriptSig, scriptPubKey, privKeys) <- ScriptGenerators.signedCLTVScriptSignature(cltvLockTime, txLockTime, sequence) + spendable = lockTimeTxHelper(scriptSig, scriptPubKey, privKeys, sequence, Some(txLockTime)) + } yield spendable + + /** + * Creates a [[ECPrivateKey]], then creates a [[CSVScriptPubKey]] from that private key + * Finally creates a [[Transaction]] that can successfully spend the [[CSVScriptPubKey]] + */ + def spendableCSVTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (csvScriptNum, sequence) <- spendableCSVValues + tx <- csvTransaction(csvScriptNum, sequence) + } yield tx + + /** Creates a CSV transaction that's timelock has not been met */ + def unspendableCSVTransaction: Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (csvScriptNum, sequence) <- unspendableCSVValues + tx <- csvTransaction(csvScriptNum, sequence) + } yield tx + + def csvTransaction(csvScriptNum: ScriptNumber, sequence: UInt32): Gen[(BaseTxSigComponent, Seq[ECPrivateKey])] = for { + (signedScriptSig, csvScriptPubKey, privateKeys) <- ScriptGenerators.signedCSVScriptSignature(csvScriptNum, sequence) + } yield lockTimeTxHelper(signedScriptSig, csvScriptPubKey, privateKeys, sequence, None) + + /** + * Generates a [[Transaction]] that has a valid [[EscrowTimeoutScriptSignature]] that specifically spends the + * [[EscrowTimeoutScriptPubKey]] using the multisig escrow branch + */ + def spendableMultiSigEscrowTimeoutTransaction(outputs: Seq[TransactionOutput]): Gen[BaseTxSigComponent] = for { + sequence <- NumberGenerator.uInt32s + amount <- CurrencyUnitGenerator.satoshis + (scriptSig, scriptPubKey, privKeys) <- ScriptGenerators.signedMultiSigEscrowTimeoutScriptSig(sequence, outputs, amount) + (creditingTx, outputIndex) = buildCreditingTransaction(TransactionConstants.validLockVersion, scriptPubKey, amount) + (spendingTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, scriptSig, + outputIndex, TransactionConstants.lockTime, sequence, outputs) + output = creditingTx.outputs(outputIndex.toInt) + baseTxSigComponent = BaseTxSigComponent(spendingTx, inputIndex, output, Policy.standardScriptVerifyFlags) + } yield baseTxSigComponent + + /** + * Generates a [[Transaction]] that has a valid [[EscrowTimeoutScriptSignature]] that specfically spends the + * [[EscrowTimeoutScriptPubKey]] using the timeout branch + */ + def spendableTimeoutEscrowTimeoutTransaction(outputs: Seq[TransactionOutput]): Gen[BaseTxSigComponent] = for { + (csvScriptNum, sequence) <- spendableCSVValues + (scriptSig, scriptPubKey, privKeys) <- ScriptGenerators.timeoutEscrowTimeoutScriptSig(csvScriptNum, sequence, outputs) + (creditingTx, outputIndex) = buildCreditingTransaction(TransactionConstants.validLockVersion, scriptPubKey) + (spendingTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, + scriptSig, outputIndex, UInt32.zero, sequence, outputs) + output = creditingTx.outputs(outputIndex.toInt) + baseTxSigComponent = BaseTxSigComponent(spendingTx, inputIndex, output, Policy.standardScriptVerifyFlags) + } yield baseTxSigComponent + + /** Generates a [[Transaction]] that has a valid [[EscrowTimeoutScriptSignature]] */ + def spendableEscrowTimeoutTransaction(outputs: Seq[TransactionOutput] = Nil): Gen[BaseTxSigComponent] = { + Gen.oneOf( + spendableMultiSigEscrowTimeoutTransaction(outputs), + spendableTimeoutEscrowTimeoutTransaction(outputs)) + } + + /** Generates a [[Transaction]] that has a valid [[EscrowTimeoutScriptSignature]] */ + def spendableEscrowTimeoutTransaction: Gen[BaseTxSigComponent] = { + Gen.oneOf( + spendableMultiSigEscrowTimeoutTransaction(Nil), + spendableTimeoutEscrowTimeoutTransaction(Nil)) + } + /** Generates a CSVEscrowTimeoutTransaction that should evaluate to false when run through the [[ScriptInterpreter]] */ + def unspendableTimeoutEscrowTimeoutTransaction: Gen[BaseTxSigComponent] = for { + (csvScriptNum, sequence) <- unspendableCSVValues + (scriptSig, scriptPubKey, privKeys) <- ScriptGenerators.timeoutEscrowTimeoutScriptSig(csvScriptNum, sequence, Nil) + (creditingTx, outputIndex) = buildCreditingTransaction(TransactionConstants.validLockVersion, scriptPubKey) + (spendingTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, scriptSig, outputIndex, + TransactionConstants.lockTime, sequence) + output = creditingTx.outputs(outputIndex.toInt) + baseTxSigComponent = BaseTxSigComponent(spendingTx, inputIndex, output, Policy.standardScriptVerifyFlags) + } yield baseTxSigComponent + + def unspendableMultiSigEscrowTimeoutTransaction: Gen[BaseTxSigComponent] = for { + sequence <- NumberGenerator.uInt32s + (multiSigScriptPubKey, _) <- ScriptGenerators.multiSigScriptPubKey + (lock, _) <- ScriptGenerators.lockTimeScriptPubKey + escrow = EscrowTimeoutScriptPubKey(multiSigScriptPubKey, lock) + multiSigScriptSig <- ScriptGenerators.multiSignatureScriptSignature + (creditingTx, outputIndex) = buildCreditingTransaction(TransactionConstants.validLockVersion, escrow) + escrowScriptSig = EscrowTimeoutScriptSignature.fromMultiSig(multiSigScriptSig) + (spendingTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, + escrowScriptSig, outputIndex, TransactionConstants.lockTime, sequence) + output = creditingTx.outputs(outputIndex.toInt) + baseTxSigComponent = BaseTxSigComponent(spendingTx, inputIndex, output, Policy.standardScriptVerifyFlags) + } yield baseTxSigComponent + + def unspendableEscrowTimeoutTransaction: Gen[BaseTxSigComponent] = { + Gen.oneOf(unspendableTimeoutEscrowTimeoutTransaction, unspendableMultiSigEscrowTimeoutTransaction) + } + + /** Generates a [[WitnessTransaction]] that has all of it's inputs signed correctly */ + def signedP2WPKHTransaction: Gen[(WitnessTxSigComponent, Seq[ECPrivateKey])] = for { + (_, wBaseTxSigComponent, privKeys) <- WitnessGenerators.signedP2WPKHTransactionWitness + } yield (wBaseTxSigComponent, privKeys) + + /** Generates a [[WitnessTransaction]] that has an input spends a raw P2WSH [[WitnessScriptPubKey]] */ + def signedP2WSHP2PKTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (_, wBaseTxSigComponent, privKeys) <- WitnessGenerators.signedP2WSHP2PKTransactionWitness + } yield (wBaseTxSigComponent, privKeys) + + /** Generates a [[WitnessTransaction]] that has an input spends a raw P2WSH [[WitnessScriptPubKey]] */ + def signedP2WSHP2PKHTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (_, wBaseTxSigComponent, privKeys) <- WitnessGenerators.signedP2WSHP2PKHTransactionWitness + } yield (wBaseTxSigComponent, privKeys) + + def signedP2WSHMultiSigTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (_, wBaseTxSigComponent, privKeys) <- WitnessGenerators.signedP2WSHMultiSigTransactionWitness + } yield (wBaseTxSigComponent, privKeys) + + def signedP2WSHMultiSigEscrowTimeoutTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (_, wBaseTxSigComponent, privKeys) <- WitnessGenerators.signedP2WSHMultiSigEscrowTimeoutWitness + } yield (wBaseTxSigComponent, privKeys) + + def spendableP2WSHTimeoutEscrowTimeoutTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (_, wBaseTxSigComponent, privKeys) <- WitnessGenerators.spendableP2WSHTimeoutEscrowTimeoutWitness + } yield (wBaseTxSigComponent, privKeys) + + def signedP2WSHEscrowTimeoutTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = { + Gen.oneOf(signedP2WSHMultiSigEscrowTimeoutTransaction, spendableP2WSHTimeoutEscrowTimeoutTransaction) + } + + /** Creates a signed P2SH(P2WPKH) transaction */ + def signedP2SHP2WPKHTransaction: Gen[(WitnessTxSigComponent, Seq[ECPrivateKey])] = for { + (signedScriptSig, scriptPubKey, privKeys, witness, amount) <- ScriptGenerators.signedP2SHP2WPKHScriptSignature + (creditingTx, outputIndex) = buildCreditingTransaction(signedScriptSig.redeemScript, amount) + (signedTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, + signedScriptSig, outputIndex, witness) + output = TransactionOutput(creditingTx.outputs(outputIndex.toInt).value, scriptPubKey) + signedTxSignatureComponent = WitnessTxSigComponent(signedTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + } yield (signedTxSignatureComponent, privKeys) + + def signedP2WSHTransaction: Gen[(WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = { + Gen.oneOf(signedP2WSHP2PKTransaction, signedP2WSHP2PKHTransaction, signedP2WSHMultiSigTransaction, + signedP2WSHEscrowTimeoutTransaction) + } + /** Creates a signed P2SH(P2WSH) transaction */ + def signedP2SHP2WSHTransaction: Gen[(WitnessTxSigComponent, Seq[ECPrivateKey])] = for { + (witness, wBaseTxSigComponent, privKeys) <- WitnessGenerators.signedP2WSHTransactionWitness + p2shScriptPubKey = P2SHScriptPubKey(wBaseTxSigComponent.scriptPubKey) + p2shScriptSig = P2SHScriptSignature(wBaseTxSigComponent.scriptPubKey.asInstanceOf[WitnessScriptPubKey]) + (creditingTx, outputIndex) = buildCreditingTransaction(p2shScriptSig.redeemScript, wBaseTxSigComponent.amount) + sequence = wBaseTxSigComponent.transaction.inputs(wBaseTxSigComponent.inputIndex.toInt).sequence + locktime = wBaseTxSigComponent.transaction.lockTime + (signedTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, p2shScriptSig, outputIndex, locktime, sequence, witness) + output = TransactionOutput(creditingTx.outputs(outputIndex.toInt).value, p2shScriptPubKey) + signedTxSignatureComponent = WitnessTxSigComponent(signedTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + } yield (signedTxSignatureComponent, privKeys) + + /** + * Builds a spending transaction according to bitcoin core + * @return the built spending transaction and the input index for the script signature + */ + def buildSpendingTransaction(version: Int32, creditingTx: Transaction, scriptSignature: ScriptSignature, + outputIndex: UInt32, locktime: UInt32, sequence: UInt32): (Transaction, UInt32) = { + val output = TransactionOutput(CurrencyUnits.zero, EmptyScriptPubKey) + buildSpendingTransaction(version, creditingTx, scriptSignature, outputIndex, locktime, sequence, Seq(output)) + } + + def buildSpendingTransaction(version: Int32, creditingTx: Transaction, scriptSignature: ScriptSignature, + outputIndex: UInt32, locktime: UInt32, sequence: UInt32, outputs: Seq[TransactionOutput]): (Transaction, UInt32) = { + val os = if (outputs.isEmpty) { + Seq(TransactionOutput(CurrencyUnits.zero, EmptyScriptPubKey)) + } else { + outputs + } + val outpoint = TransactionOutPoint(creditingTx.txId, outputIndex) + val input = TransactionInput(outpoint, scriptSignature, sequence) + val tx = BaseTransaction(version, Seq(input), os, locktime) + (tx, UInt32.zero) + } + + /** + * Builds a spending transaction according to bitcoin core with max sequence and a locktime of zero. + * @return the built spending transaction and the input index for the script signature + */ + def buildSpendingTransaction(creditingTx: Transaction, scriptSignature: ScriptSignature, outputIndex: UInt32): (Transaction, UInt32) = { + buildSpendingTransaction(TransactionConstants.version, creditingTx, scriptSignature, outputIndex, + TransactionConstants.lockTime, TransactionConstants.sequence) + } + + /** Builds a spending [[WitnessTransaction]] with the given parameters */ + def buildSpendingTransaction(creditingTx: Transaction, scriptSignature: ScriptSignature, outputIndex: UInt32, + locktime: UInt32, sequence: UInt32, witness: TransactionWitness): (WitnessTransaction, UInt32) = { + buildSpendingTransaction(TransactionConstants.version, creditingTx, scriptSignature, outputIndex, locktime, sequence, witness) + } + + def buildSpendingTransaction(version: Int32, creditingTx: Transaction, scriptSignature: ScriptSignature, outputIndex: UInt32, + locktime: UInt32, sequence: UInt32, witness: TransactionWitness): (WitnessTransaction, UInt32) = { + + val outputs = dummyOutputs + buildSpendingTransaction(version, creditingTx, scriptSignature, outputIndex, locktime, sequence, witness, outputs) + } + + def dummyOutputs: Seq[TransactionOutput] = Seq(TransactionOutput(CurrencyUnits.zero, EmptyScriptPubKey)) + + def buildSpendingTransaction(version: Int32, creditingTx: Transaction, scriptSignature: ScriptSignature, outputIndex: UInt32, + locktime: UInt32, sequence: UInt32, witness: TransactionWitness, outputs: Seq[TransactionOutput]): (WitnessTransaction, UInt32) = { + val outpoint = TransactionOutPoint(creditingTx.txId, outputIndex) + val input = TransactionInput(outpoint, scriptSignature, sequence) + (WitnessTransaction(version, Seq(input), outputs, locktime, witness), UInt32.zero) + } + + def buildSpendingTransaction(creditingTx: Transaction, scriptSignature: ScriptSignature, outputIndex: UInt32, + witness: TransactionWitness): (WitnessTransaction, UInt32) = { + buildSpendingTransaction(TransactionConstants.version, creditingTx, scriptSignature, outputIndex, witness) + } + + def buildSpendingTransaction(version: Int32, creditingTx: Transaction, scriptSignature: ScriptSignature, outputIndex: UInt32, + witness: TransactionWitness): (WitnessTransaction, UInt32) = { + val locktime = TransactionConstants.lockTime + val sequence = TransactionConstants.sequence + buildSpendingTransaction(version, creditingTx, scriptSignature, outputIndex, locktime, sequence, witness) + } + + /** + * Mimics this test utility found in bitcoin core + * https://github.com/bitcoin/bitcoin/blob/605c17844ea32b6d237db6d83871164dc7d59dab/src/test/script_tests.cpp#L57 + * @return the transaction and the output index of the scriptPubKey + */ + def buildCreditingTransaction(scriptPubKey: ScriptPubKey): (Transaction, UInt32) = { + //this needs to be all zeros according to these 3 lines in bitcoin core + //https://github.com/bitcoin/bitcoin/blob/605c17844ea32b6d237db6d83871164dc7d59dab/src/test/script_tests.cpp#L64 + //https://github.com/bitcoin/bitcoin/blob/80d1f2e48364f05b2cdf44239b3a1faa0277e58e/src/primitives/transaction.h#L32 + //https://github.com/bitcoin/bitcoin/blob/605c17844ea32b6d237db6d83871164dc7d59dab/src/uint256.h#L40 + buildCreditingTransaction(TransactionConstants.version, scriptPubKey) + } + + def buildCreditingTransaction(scriptPubKey: ScriptPubKey, amount: CurrencyUnit): (Transaction, UInt32) = { + buildCreditingTransaction(TransactionConstants.version, scriptPubKey, amount) + } + + /** + * Builds a crediting transaction with a transaction version parameter. + * Example: useful for creating transactions with scripts containing OP_CHECKSEQUENCEVERIFY. + * @return + */ + def buildCreditingTransaction(version: Int32, scriptPubKey: ScriptPubKey): (Transaction, UInt32) = { + buildCreditingTransaction(version, scriptPubKey, CurrencyUnits.zero) + } + + def buildCreditingTransaction(version: Int32, output: TransactionOutput): (Transaction, UInt32) = { + val outpoint = EmptyTransactionOutPoint + val scriptSignature = ScriptSignature("0000") + val input = TransactionInput(outpoint, scriptSignature, TransactionConstants.sequence) + val tx = BaseTransaction(version, Seq(input), Seq(output), TransactionConstants.lockTime) + (tx, UInt32.zero) + } + + def buildCreditingTransaction(version: Int32, scriptPubKey: ScriptPubKey, amount: CurrencyUnit): (Transaction, UInt32) = { + buildCreditingTransaction(version, TransactionOutput(amount, scriptPubKey)) + } + + private def lockTimeTxHelper(signedScriptSig: LockTimeScriptSignature, lock: LockTimeScriptPubKey, + privKeys: Seq[ECPrivateKey], sequence: UInt32, lockTime: Option[UInt32]): (BaseTxSigComponent, Seq[ECPrivateKey]) = { + val (creditingTx, outputIndex) = buildCreditingTransaction(TransactionConstants.validLockVersion, lock) + //Transaction version must not be less than 2 for a CSV transaction + val (signedSpendingTx, inputIndex) = buildSpendingTransaction(TransactionConstants.validLockVersion, creditingTx, + signedScriptSig, outputIndex, lockTime.getOrElse(TransactionConstants.lockTime), sequence) + val output = creditingTx.outputs(outputIndex.toInt) + val baseTxSigComponent = BaseTxSigComponent(signedSpendingTx, inputIndex, + output, Policy.standardScriptVerifyFlags) + (baseTxSigComponent, privKeys) + } + + /** + * Determines if the transaction input's sequence value and CSV script sequence value are of the same type + * (i.e. determines whether both are a timestamp or block-height) + */ + private def csvLockTimesOfSameType(sequenceNumbers: (ScriptNumber, UInt32)): Boolean = { + LockTimeInterpreter.isCSVLockByRelativeLockTime(sequenceNumbers._1, sequenceNumbers._2) || + LockTimeInterpreter.isCSVLockByBlockHeight(sequenceNumbers._1, sequenceNumbers._2) + } + + /** + * Generates a pair of CSV values: a transaction input sequence, and a CSV script sequence value, such that the txInput + * sequence mask is always greater than the script sequence mask (i.e. generates values for a validly constructed and spendable CSV transaction) + */ + def spendableCSVValues: Gen[(ScriptNumber, UInt32)] = { + Gen.oneOf( + validScriptNumberAndSequenceForBlockHeight, + validScriptNumberAndSequenceForRelativeLockTime) + } + + /** + * To indicate that we should evaulate a OP_CSV operation based on + * blockheight we need 1 << 22 bit turned off. See BIP68 for more details + */ + private def lockByBlockHeightBitSet: UInt32 = UInt32("ffbfffff") + + /** Generates a [[UInt32]] s.t. the block height bit is set according to BIP68 */ + private def sequenceForBlockHeight: Gen[UInt32] = validCSVSequence.map { n => + val result: UInt32 = n & lockByBlockHeightBitSet + require(LockTimeInterpreter.isCSVLockByBlockHeight(result), "Block height locktime bit was not set: " + result) + result + } + + /** Generates a [[ScriptNumber]] and [[UInt32]] s.t. the pair can be spent by an OP_CSV operation */ + private def validScriptNumberAndSequenceForBlockHeight: Gen[(ScriptNumber, UInt32)] = { + sequenceForBlockHeight.flatMap { s => + val seqMasked = TransactionConstants.sequenceLockTimeMask + val validScriptNums = s & seqMasked + Gen.choose(0L, validScriptNums.toLong).map { sn => + val scriptNum = ScriptNumber(sn & lockByBlockHeightBitSet.toLong) + require(LockTimeInterpreter.isCSVLockByBlockHeight(scriptNum)) + require(LockTimeInterpreter.isCSVLockByBlockHeight(s)) + (scriptNum, s) + } + } + } + + /** Generates a [[UInt32]] with the locktime bit set according to BIP68 */ + private def sequenceForRelativeLockTime: Gen[UInt32] = validCSVSequence.map { n => + val result = n | TransactionConstants.sequenceLockTimeTypeFlag + require(LockTimeInterpreter.isCSVLockByRelativeLockTime(result), "Relative locktime bit was not set: " + result) + result + } + + /** Generates a valid [[ScriptNumber]] and [[UInt32]] s.t. the pair will evaluate to true by a OP_CSV operation */ + private def validScriptNumberAndSequenceForRelativeLockTime: Gen[(ScriptNumber, UInt32)] = { + sequenceForRelativeLockTime.flatMap { s => + val seqMasked = TransactionConstants.sequenceLockTimeMask + val validScriptNums = s & seqMasked + Gen.choose(0L, validScriptNums.toLong).map { sn => + val scriptNum = ScriptNumber(sn | TransactionConstants.sequenceLockTimeTypeFlag.toLong) + require(LockTimeInterpreter.isCSVLockByRelativeLockTime(scriptNum)) + require(LockTimeInterpreter.isCSVLockByRelativeLockTime(s)) + (scriptNum, s) + } + } + } + /** Generates a [[UInt32]] s.t. the locktime enabled flag is set. See BIP68 for more info */ + private def validCSVSequence: Gen[UInt32] = NumberGenerator.uInt32s.map { n => + //makes sure the 1 << 31 is TURNED OFF, + //need this to generate spendable CSV values without discarding a bunch of test cases + val result = n & UInt32(0x7FFFFFFF) + require(LockTimeInterpreter.isLockTimeBitOff(ScriptNumber(result.toLong))) + result + } + + /** + * Generates a pair of CSV values: a transaction input sequence, and a CSV script sequence value, such that the txInput + * sequence mask is always less than the script sequence mask (i.e. generates values for a validly constructed and NOT spendable CSV transaction). + */ + def unspendableCSVValues: Gen[(ScriptNumber, UInt32)] = (for { + sequence <- NumberGenerator.uInt32s + csvScriptNum <- NumberGenerator.uInt32s.map(x => + ScriptNumber(x.toLong)).suchThat(x => LockTimeInterpreter.isLockTimeBitOff(x)) + } yield (csvScriptNum, sequence)).suchThat(x => !csvLockTimesOfSameType(x)) + + /** generates a [[ScriptNumber]] and [[UInt32]] locktime for a transaction such that the tx will be spendable */ + def spendableCLTVValues: Gen[(ScriptNumber, UInt32)] = for { + txLockTime <- NumberGenerator.uInt32s + cltvLockTime <- sameLockTimeTypeSpendable(txLockTime) + } yield (cltvLockTime, txLockTime) + + /** Generates a [[ScriptNumber]] and [[UInt32]] locktime for a transaction such that the tx will be unspendable */ + def unspendableCLTVValues: Gen[(ScriptNumber, UInt32)] = for { + txLockTime <- NumberGenerator.uInt32s + cltvLockTime <- sameLockTimeUnspendable(txLockTime) + } yield (cltvLockTime, txLockTime) + + private def sameLockTimeTypeSpendable(txLockTime: UInt32): Gen[ScriptNumber] = { + if (txLockTime < TransactionConstants.locktimeThreshold) { + Gen.choose(0, txLockTime.toLong).map(ScriptNumber(_)) + } else { + Gen.choose(TransactionConstants.locktimeThreshold.toLong, txLockTime.toLong).map(ScriptNumber(_)) + } + } + + private def sameLockTimeUnspendable(txLockTime: UInt32): Gen[ScriptNumber] = { + if (txLockTime < TransactionConstants.locktimeThreshold) { + Gen.choose(txLockTime.toLong + 1, TransactionConstants.locktimeThreshold.toLong).map(ScriptNumber(_)) + } else { + Gen.choose(txLockTime.toLong + 1, UInt32.max.toLong).map(ScriptNumber(_)) + } + } +} + +object TransactionGenerators extends TransactionGenerators diff --git a/testkit/src/main/scala/org/bitcoins/core/gen/WitnessGenerators.scala b/testkit/src/main/scala/org/bitcoins/core/gen/WitnessGenerators.scala new file mode 100644 index 0000000000..1994c2a9d3 --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/core/gen/WitnessGenerators.scala @@ -0,0 +1,252 @@ +package org.bitcoins.core.gen + +import org.bitcoins.core.crypto._ +import org.bitcoins.core.currency.CurrencyUnit +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.policy.Policy +import org.bitcoins.core.protocol.script._ +import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.core.script.crypto.HashType +import org.bitcoins.core.util.BitcoinSLogger +import org.bitcoins.core.wallet.EscrowTimeoutHelper +import org.scalacheck.Gen + +/** + * Created by chris on 11/28/16. + */ +sealed abstract class WitnessGenerators extends BitcoinSLogger { + + /** Generates a random [[org.bitcoins.core.protocol.script.ScriptWitness]] */ + def scriptWitness: Gen[ScriptWitness] = { + + //TODO: I need to come back and uncomment out this code after fixing + //#111 on the issue tracker. We should be able to support an arbtirary byte vector, + //not only pre-defined script witness types + + //0 include here to generate the EmptyScriptWitness + + /* val stack: Gen[Seq[scodec.bits.ByteVector]] = Gen.choose(0,10).flatMap(n => Gen.listOfN(n, NumberGenerator.bytes)) + stack.map { s: Seq[scodec.bits.ByteVector] => + val spkBytes = if (s.nonEmpty) s.head else Nil + val cmpctSPK = CompactSizeUInt(UInt64(spkBytes.size)) + val scriptSigBytes: scodec.bits.ByteVector = if (s.size > 1) s.tail.flatten else Nil + val cmpctScriptSig = CompactSizeUInt(UInt64(scriptSigBytes.size)) + + val scriptSig = if (scriptSigBytes.isEmpty) EmptyScriptSignature else NonStandardScriptSignature(cmpctScriptSig.bytes ++ scriptSigBytes) + val spk = if (spkBytes.isEmpty) EmptyScriptPubKey else NonStandardScriptPubKey(cmpctSPK.bytes ++ spkBytes) + P2WSHWitnessV0(spk,scriptSig) + }*/ + Gen.oneOf(p2wpkhWitnessV0, p2wshWitnessV0) + } + + /** Generates a [[TransactionWitness]] with the specified number of witnesses */ + def transactionWitness(numWitnesses: Int): Gen[TransactionWitness] = for { + inputWitnesses <- Gen.listOfN(numWitnesses, Gen.option(scriptWitness)) + } yield TransactionWitness.fromWitOpt(inputWitnesses.toVector) + + def transactionWitness: Gen[TransactionWitness] = for { + num <- Gen.choose(1, 10) + wit <- transactionWitness(num) + } yield wit + + /** Generates a validly signed [[TransactionWitness]] */ + def signedP2WPKHTransactionWitness: Gen[(TransactionWitness, WitnessTxSigComponent, Seq[ECPrivateKey])] = for { + privKey <- CryptoGenerators.privateKey + amount <- CurrencyUnitGenerator.satoshis + hashType <- CryptoGenerators.hashType + witScriptPubKey = P2WPKHWitnessSPKV0(privKey.publicKey) + unsignedScriptWitness = P2WPKHWitnessV0(privKey.publicKey) + unsignedWTxSigComponent = createUnsignedRawWTxSigComponent(witScriptPubKey, amount, + unsignedScriptWitness, None) + createdSig = TransactionSignatureCreator.createSig(unsignedWTxSigComponent, privKey, hashType) + scriptWitness = P2WPKHWitnessV0(privKey.publicKey, createdSig) + + } yield { + val (witness, signedWtxSigComponent) = createSignedWTxComponent(scriptWitness, unsignedWTxSigComponent) + (witness, signedWtxSigComponent, Seq(privKey)) + } + + def signedP2WSHP2PKTransactionWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKeys) <- ScriptGenerators.p2pkScriptPubKey + amount <- CurrencyUnitGenerator.satoshis + hashType <- CryptoGenerators.hashType + witScriptPubKey = P2WSHWitnessSPKV0(scriptPubKey) + unsignedScriptWitness = P2WSHWitnessV0(scriptPubKey) + u = createUnsignedRawWTxSigComponent(witScriptPubKey, amount, + unsignedScriptWitness, None) + createdSig = TransactionSignatureCreator.createSig(u, privKeys, hashType) + signedScriptWitness = P2WSHWitnessV0(scriptPubKey, P2PKScriptSignature(createdSig)) + oldTx = u.transaction + txWitness = TransactionWitness(oldTx.witness.witnesses.updated(u.inputIndex.toInt, signedScriptWitness)) + wtx = WitnessTransaction(oldTx.version, oldTx.inputs, oldTx.outputs, oldTx.lockTime, txWitness) + signedWtxSigComponent = WitnessTxSigComponentRaw(wtx, u.inputIndex, u.output, u.flags) + } yield (txWitness, signedWtxSigComponent, Seq(privKeys)) + + def signedP2WSHP2PKHTransactionWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKey) <- ScriptGenerators.p2pkhScriptPubKey + amount <- CurrencyUnitGenerator.satoshis + hashType <- CryptoGenerators.hashType + witScriptPubKey = P2WSHWitnessSPKV0(scriptPubKey) + unsignedScriptWitness = P2WSHWitnessV0(scriptPubKey) + u = createUnsignedRawWTxSigComponent(witScriptPubKey, amount, unsignedScriptWitness, None) + createdSig = TransactionSignatureCreator.createSig(u, privKey, hashType) + signedScriptWitness = P2WSHWitnessV0(scriptPubKey, P2PKHScriptSignature(createdSig, privKey.publicKey)) + oldTx = u.transaction + txWitness = TransactionWitness(oldTx.witness.witnesses.updated(u.inputIndex.toInt, signedScriptWitness)) + wtx = WitnessTransaction(oldTx.version, oldTx.inputs, oldTx.outputs, oldTx.lockTime, txWitness) + signedWtxSigComponent = WitnessTxSigComponentRaw(wtx, u.inputIndex, u.output, u.flags) + } yield (txWitness, signedWtxSigComponent, Seq(privKey)) + + def signedP2WSHMultiSigTransactionWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKeys) <- ScriptGenerators.multiSigScriptPubKey + amount <- CurrencyUnitGenerator.satoshis + hashType <- CryptoGenerators.hashType + witScriptPubKey = P2WSHWitnessSPKV0(scriptPubKey) + unsignedScriptWitness = P2WSHWitnessV0(scriptPubKey) + u = createUnsignedRawWTxSigComponent(witScriptPubKey, amount, + unsignedScriptWitness, None) + signedScriptSig = multiSigScriptSigGenHelper(privKeys, scriptPubKey, u, hashType) + signedScriptWitness = P2WSHWitnessV0(scriptPubKey, signedScriptSig) + oldTx = u.transaction + txWitness = TransactionWitness(oldTx.witness.witnesses.updated(u.inputIndex.toInt, signedScriptWitness)) + wtx = WitnessTransaction(oldTx.version, oldTx.inputs, oldTx.outputs, oldTx.lockTime, txWitness) + signedWtxSigComponent = WitnessTxSigComponentRaw(wtx, u.inputIndex, u.output, u.flags) + } yield (txWitness, signedWtxSigComponent, privKeys) + + /** + * Generates a random signed [[TransactionWitness]] with the corresponding [[WitnessTxSigComponent]] + * and [[ECPrivateKey]]s + */ + def signedP2WSHTransactionWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = { + Gen.oneOf(signedP2WSHP2PKTransactionWitness, signedP2WSHP2PKHTransactionWitness, + signedP2WSHMultiSigTransactionWitness, signedP2WSHEscrowTimeoutWitness) + } + + def signedP2WSHMultiSigEscrowTimeoutWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (scriptPubKey, privKeys) <- ScriptGenerators.escrowTimeoutScriptPubKey + amount <- CurrencyUnitGenerator.satoshis + hashType <- CryptoGenerators.hashType + witScriptPubKey = P2WSHWitnessSPKV0(scriptPubKey) + unsignedScriptWitness = P2WSHWitnessV0(scriptPubKey) + u = createUnsignedRawWTxSigComponent(witScriptPubKey, amount, + unsignedScriptWitness, None) + signedScriptSig = csvEscrowTimeoutGenHelper(privKeys, scriptPubKey, u, hashType) + witness = EscrowTimeoutHelper.buildEscrowTimeoutScriptWitness(signedScriptSig, scriptPubKey, u) + oldTx = u.transaction + wTx = WitnessTransaction(oldTx.version, oldTx.inputs, oldTx.outputs, oldTx.lockTime, witness) + signedWTxSigComponent = WitnessTxSigComponentRaw(wTx, u.inputIndex, u.output, u.flags) + } yield (witness, signedWTxSigComponent, privKeys) + + def spendableP2WSHTimeoutEscrowTimeoutWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = for { + (p2pkh, privKey) <- ScriptGenerators.p2pkhScriptPubKey + (scriptNum, sequence) <- TransactionGenerators.spendableCSVValues + csv = CSVScriptPubKey(scriptNum, p2pkh) + (m, _) <- ScriptGenerators.smallMultiSigScriptPubKey + scriptPubKey = EscrowTimeoutScriptPubKey(m, csv) + amount <- CurrencyUnitGenerator.satoshis + hashType <- CryptoGenerators.hashType + witScriptPubKey = P2WSHWitnessSPKV0(scriptPubKey) + unsignedScriptWitness = P2WSHWitnessV0(scriptPubKey) + u = createUnsignedRawWTxSigComponent( + witScriptPubKey, + amount, unsignedScriptWitness, Some(sequence)) + createdSig = TransactionSignatureCreator.createSig(u, privKey, hashType) + scriptSig = CSVScriptSignature(P2PKHScriptSignature(createdSig, privKey.publicKey)) + signedScriptWitness = P2WSHWitnessV0(scriptPubKey, EscrowTimeoutScriptSignature.fromLockTime(scriptSig)) + //ScriptWitness(scriptPubKey.asm.flatMap(_.bytes) +: Seq(ScriptNumber.zero.bytes, privKey.publicKey.bytes, + //createdSig.bytes)) + oldTx = u.transaction + txWitness = TransactionWitness(oldTx.witness.witnesses.updated(u.inputIndex.toInt, signedScriptWitness)) + wtx = WitnessTransaction(oldTx.version, oldTx.inputs, oldTx.outputs, oldTx.lockTime, txWitness) + signedWtxSigComponent = WitnessTxSigComponentRaw(wtx, u.inputIndex, u.output, u.flags) + } yield (txWitness, signedWtxSigComponent, Seq(privKey)) + + def signedP2WSHEscrowTimeoutWitness: Gen[(TransactionWitness, WitnessTxSigComponentRaw, Seq[ECPrivateKey])] = { + Gen.oneOf(signedP2WSHMultiSigEscrowTimeoutWitness, spendableP2WSHTimeoutEscrowTimeoutWitness) + } + + /** Helps generate a signed [[MultiSignatureScriptSignature]] */ + private def multiSigScriptSigGenHelper( + privateKeys: Seq[ECPrivateKey], + scriptPubKey: MultiSignatureScriptPubKey, + unsignedWtxSigComponent: WitnessTxSigComponent, + hashType: HashType): MultiSignatureScriptSignature = { + val requiredSigs = scriptPubKey.requiredSigs + val txSignatures = for { + i <- 0 until requiredSigs + } yield TransactionSignatureCreator.createSig(unsignedWtxSigComponent, privateKeys(i), hashType) + + //add the signature to the scriptSig instead of having an empty scriptSig + val signedScriptSig = MultiSignatureScriptSignature(txSignatures) + signedScriptSig + } + + def csvEscrowTimeoutGenHelper(privateKeys: Seq[ECPrivateKey], scriptPubKey: EscrowTimeoutScriptPubKey, + unsignedWtxSigComponent: WitnessTxSigComponent, + hashType: HashType): EscrowTimeoutScriptSignature = { + if (scriptPubKey.escrow.requiredSigs == 0) { + EscrowTimeoutScriptSignature.fromMultiSig(MultiSignatureScriptSignature(Nil)) + } else if (privateKeys.size == 1) { + val signature = csvEscrowTimeoutGenSignature(privateKeys.head, unsignedWtxSigComponent, hashType) + EscrowTimeoutScriptSignature.fromMultiSig(MultiSignatureScriptSignature(Seq(signature))) + } else { + val multiSig = multiSigScriptSigGenHelper(privateKeys, scriptPubKey.escrow, unsignedWtxSigComponent, hashType) + EscrowTimeoutScriptSignature.fromMultiSig(multiSig) + } + } + + def csvEscrowTimeoutGenSignature( + privKey: ECPrivateKey, + unsignedWtxSigComponent: WitnessTxSigComponent, hashType: HashType): ECDigitalSignature = { + + val signature = TransactionSignatureCreator.createSig(unsignedWtxSigComponent, privKey, hashType) + signature + } + + /** Generates a random [[org.bitcoins.core.protocol.script.P2WPKHWitnessV0]] */ + def p2wpkhWitnessV0: Gen[P2WPKHWitnessV0] = for { + publicKey <- CryptoGenerators.publicKey + sig <- CryptoGenerators.digitalSignature + } yield P2WPKHWitnessV0(publicKey, sig) + + /** Generates a random [[org.bitcoins.core.protocol.script.P2WSHWitnessV0]] */ + def p2wshWitnessV0: Gen[P2WSHWitnessV0] = for { + (redeem, _) <- ScriptGenerators.scriptPubKey + scriptSig <- ScriptGenerators.scriptSignature + } yield P2WSHWitnessV0(redeem, scriptSig) + + /** Takes a signed [[ScriptWitness]] and an unsignedTx and adds the witness to the unsigned [[WitnessTransaction]] */ + def createSignedWTxComponent(witness: ScriptWitness, unsignedWTxComponent: WitnessTxSigComponent): (TransactionWitness, WitnessTxSigComponent) = { + val signedTxWitness = TransactionWitness.fromWitOpt(Vector(Some(witness))) + val unsignedSpendingTx = unsignedWTxComponent.transaction + val signedSpendingTx = WitnessTransaction(unsignedSpendingTx.version, unsignedSpendingTx.inputs, unsignedSpendingTx.outputs, + unsignedSpendingTx.lockTime, signedTxWitness) + val signedWtxSigComponent = unsignedWTxComponent match { + case wtxP2SH: WitnessTxSigComponentP2SH => + WitnessTxSigComponent(signedSpendingTx, unsignedWTxComponent.inputIndex, + wtxP2SH.output, unsignedWTxComponent.flags) + case wtxRaw: WitnessTxSigComponentRaw => + WitnessTxSigComponent(signedSpendingTx, unsignedWTxComponent.inputIndex, + wtxRaw.output, unsignedWTxComponent.flags) + } + + (signedTxWitness, signedWtxSigComponent) + } + + /** Creates a unsigned [[WitnessTxSigComponent]] from the given parameters */ + def createUnsignedRawWTxSigComponent(witScriptPubKey: WitnessScriptPubKey, amount: CurrencyUnit, + unsignedScriptWitness: ScriptWitness, sequence: Option[UInt32]): WitnessTxSigComponentRaw = { + val tc = TransactionConstants + val flags = Policy.standardScriptVerifyFlags + val witness = TransactionWitness.fromWitOpt(Vector(Some(unsignedScriptWitness))) + val (creditingTx, outputIndex) = TransactionGenerators.buildCreditingTransaction(witScriptPubKey, amount) + val (unsignedSpendingTx, inputIndex) = TransactionGenerators.buildSpendingTransaction(tc.validLockVersion, creditingTx, + EmptyScriptSignature, outputIndex, tc.lockTime, + sequence.getOrElse(tc.sequence), witness) + val output = creditingTx.outputs(outputIndex.toInt) + val unsignedWtxSigComponent = WitnessTxSigComponentRaw(unsignedSpendingTx, inputIndex, output, flags) + unsignedWtxSigComponent + } +} + +object WitnessGenerators extends WitnessGenerators diff --git a/testkit/src/main/scala/org/bitcoins/eclair/rpc/EclairRpcTestUtil.scala b/testkit/src/main/scala/org/bitcoins/eclair/rpc/EclairRpcTestUtil.scala new file mode 100644 index 0000000000..7b0bd4a10f --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/eclair/rpc/EclairRpcTestUtil.scala @@ -0,0 +1,343 @@ +package org.bitcoins.eclair.rpc + +import java.io.{ File, PrintWriter } +import java.net.URI + +import akka.actor.ActorSystem +import com.typesafe.config.{ Config, ConfigFactory } +import org.bitcoins.core.config.RegTest +import org.bitcoins.core.currency.CurrencyUnit +import org.bitcoins.core.protocol.ln.channel.{ ChannelId, ChannelState, FundedChannelId } +import org.bitcoins.core.protocol.ln.currency.MilliSatoshis +import org.bitcoins.core.util.BitcoinSLogger +import org.bitcoins.eclair.rpc.client.EclairRpcClient +import org.bitcoins.eclair.rpc.config.EclairInstance +import org.bitcoins.rpc.client.BitcoindRpcClient +import org.bitcoins.rpc.config.BitcoindInstance +import org.bitcoins.rpc.{ BitcoindRpcTestUtil, RpcUtil } + +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt + +trait EclairTestUtil extends BitcoinSLogger { + import collection.JavaConverters._ + + def randomDirName: String = + 0.until(5).map(_ => scala.util.Random.alphanumeric.head).mkString + + def randomEclairDatadir(): File = new File(s"/tmp/${randomDirName}/.eclair/") + + def cannonicalDatadir = new File(s"${System.getenv("HOME")}/.reg_eclair/") + + lazy val network = RegTest + + def bitcoindInstance( + port: Int = randomPort, + rpcPort: Int = randomPort, + zmqPort: Int = randomPort): BitcoindInstance = { + val uri = new URI("http://localhost:" + port) + val rpcUri = new URI("http://localhost:" + rpcPort) + val auth = BitcoindRpcTestUtil.authCredentials(uri, rpcUri, zmqPort, false) + + BitcoindInstance( + network = network, + uri = uri, + rpcUri = rpcUri, + authCredentials = auth, + zmqPortOpt = Some(zmqPort)) + } + + //cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153 + private def commonConfig( + bitcoindInstance: BitcoindInstance, + port: Int = randomPort, + apiPort: Int = randomPort): Config = { + val configMap = { + Map( + "eclair.chain" -> "regtest", + "eclair.spv" -> false, + "eclair.server.public-ips.1" -> "127.0.0.1", + "eclair.server.binding-ip" -> "0.0.0.0", + "eclair.server.port" -> port, + + "eclair.bitcoind.rpcuser" -> bitcoindInstance.authCredentials.username, + "eclair.bitcoind.rpcpassword" -> bitcoindInstance.authCredentials.password, + "eclair.bitcoind.rpcport" -> bitcoindInstance.authCredentials.rpcPort, + "eclair.bitcoind.zmq" -> s"tcp://127.0.0.1:${bitcoindInstance.zmqPortOpt.get}", + + "eclair.api.enabled" -> true, + "eclair.api.binding-ip" -> "127.0.0.1", + "eclair.api.password" -> "abc123", + "eclair.api.port" -> apiPort, + + "eclair.mindepth-blocks" -> 2, + "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, + "eclair.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false, + "eclair.db.driver" -> "org.sqlite.JDBC", + "eclair.db.regtest.url" -> "jdbc:sqlite:regtest/", + + "eclair.alias" -> "suredbits") + } + val c = ConfigFactory.parseMap(configMap.asJava) + c + } + + def eclairDataDir(bitcoindRpcClient: BitcoindRpcClient, isCannonical: Boolean): File = { + val bitcoindInstance = bitcoindRpcClient.instance + if (isCannonical) { + //assumes that the ${HOME}/.eclair/eclair.conf file is created AND a bitcoind instance is running + cannonicalDatadir + } else { + //creates a random eclair datadir, but still assumes that a bitcoind instance is running right now + val datadir = randomEclairDatadir + datadir.mkdirs() + logger.trace(s"Creating temp eclair dir ${datadir.getAbsolutePath}") + + val config = commonConfig(bitcoindInstance) + + new PrintWriter(new File(datadir, "eclair.conf")) { + write(config.root().render()) + close + } + datadir + } + } + + /** Assumes bitcoind is running already and you have specified correct bindings in eclair.conf */ + def cannonicalEclairInstance(): EclairInstance = { + val datadir = cannonicalDatadir + eclairInstance(datadir) + } + + def eclairInstance(datadir: File): EclairInstance = { + val instance = EclairInstance.fromDatadir(datadir) + instance + } + + /** Starts the given bitcoind instance and then starts the eclair instance */ + def eclairInstance(bitcoindRpc: BitcoindRpcClient): EclairInstance = { + val datadir = eclairDataDir(bitcoindRpc, false) + eclairInstance(datadir) + } + + def randomEclairInstance(bitcoindRpc: BitcoindRpcClient): EclairInstance = { + val datadir = eclairDataDir(bitcoindRpc, false) + eclairInstance(datadir) + } + + def randomEclairClient(bitcoindRpcOpt: Option[BitcoindRpcClient] = None)(implicit system: ActorSystem): EclairRpcClient = { + val bitcoindRpc = { + if (bitcoindRpcOpt.isDefined) { + bitcoindRpcOpt.get + } else { + BitcoindRpcTestUtil.startedBitcoindRpcClient() + } + } + + val randInstance = randomEclairInstance(bitcoindRpc) + val eclairRpc = new EclairRpcClient(randInstance) + eclairRpc.start() + + RpcUtil.awaitCondition( + () => eclairRpc.isStarted(), + duration = 1.seconds) + + eclairRpc + } + + def cannonicalEclairClient()(implicit system: ActorSystem): EclairRpcClient = { + val inst = cannonicalEclairInstance() + new EclairRpcClient(inst) + } + def randomPort: Int = { + val firstAttempt = Math.abs(scala.util.Random.nextInt % 15000) + if (firstAttempt < network.port) { + firstAttempt + network.port + } else firstAttempt + } + + def deleteTmpDir(dir: File): Boolean = { + if (!dir.isDirectory) { + dir.delete() + } else { + dir.listFiles().foreach(deleteTmpDir) + dir.delete() + } + } + + /** + * Doesn't return until the given channelId + * is in the [[ChannelState.NORMAL]] for this [[EclairRpcClient]] + * @param client + * @param chanId + */ + def awaitUntilChannelNormal(client: EclairRpcClient, chanId: ChannelId)(implicit system: ActorSystem): Unit = { + awaitUntilChannelState(client, chanId, ChannelState.NORMAL) + } + + def awaitUntilChannelClosing(client: EclairRpcClient, chanId: ChannelId)(implicit system: ActorSystem): Unit = { + awaitUntilChannelState(client, chanId, ChannelState.CLOSING) + } + + private def awaitUntilChannelState(client: EclairRpcClient, chanId: ChannelId, state: ChannelState)(implicit system: ActorSystem): Unit = { + logger.debug(s"Awaiting ${chanId} to enter ${state} state") + def isState(): Future[Boolean] = { + val chanF = client.channel(chanId) + chanF.map { chan => + if (!(chan.state == state)) { + logger.trace(s"ChanId ${chanId} has not entered ${state} yet. Currently in ${chan.state}") + } + chan.state == state + }(system.dispatcher) + } + + RpcUtil.awaitConditionF( + conditionF = () => isState(), + duration = 1.seconds) + + logger.debug(s"${chanId} has successfully entered the ${state} state") + () + } + + /** + * Creates two eclair nodes that are connected together and returns their + * respective [[EclairRpcClient]]s + */ + def createNodePair(bitcoindRpcClientOpt: Option[BitcoindRpcClient])(implicit system: ActorSystem): (EclairRpcClient, EclairRpcClient) = { + val bitcoindRpcClient = { + bitcoindRpcClientOpt.getOrElse(BitcoindRpcTestUtil.startedBitcoindRpcClient()) + } + + val e1Instance = EclairTestUtil.eclairInstance(bitcoindRpcClient) + val e2Instance = EclairTestUtil.eclairInstance(bitcoindRpcClient) + + val client = new EclairRpcClient(e1Instance) + val otherClient = new EclairRpcClient(e2Instance) + + logger.debug(s"Temp eclair directory created ${client.getDaemon.authCredentials.datadir}") + logger.debug(s"Temp eclair directory created ${otherClient.getDaemon.authCredentials.datadir}") + + client.start() + otherClient.start() + + RpcUtil.awaitCondition( + condition = () => client.isStarted(), + duration = 1.second) + + RpcUtil.awaitCondition( + condition = () => otherClient.isStarted(), + duration = 1.second) + + logger.debug(s"Both clients started") + + connectLNNodes(client, otherClient) + + (client, otherClient) + } + + def connectLNNodes(client: EclairRpcClient, otherClient: EclairRpcClient)( + implicit + system: ActorSystem): Unit = { + implicit val dispatcher = system.dispatcher + val infoF = otherClient.getInfo + + val connection: Future[String] = infoF.flatMap { info => + client.connect(info.nodeId, "localhost", info.port) + } + + def isConnected(): Future[Boolean] = { + val nodeIdF = infoF.map(_.nodeId) + nodeIdF.flatMap { nodeId => + connection.flatMap { _ => + val connected: Future[Boolean] = client.isConnected(nodeId) + connected + } + } + } + + logger.debug(s"Awaiting connection between clients") + RpcUtil.awaitConditionF( + conditionF = () => isConnected(), + duration = 1.second) + logger.debug(s"Successfully connected two clients") + + () + } + + /** Opens a channel from n1 -> n2 */ + def openChannel( + n1: EclairRpcClient, + n2: EclairRpcClient, + amt: CurrencyUnit, + pushMSat: MilliSatoshis)(implicit system: ActorSystem): Future[FundedChannelId] = { + + val bitcoindRpcClient = getBitcoindRpc(n1) + implicit val ec = system.dispatcher + val fundedChannelIdF: Future[FundedChannelId] = { + n2.nodeId.flatMap { nodeId => + n1.getInfo.map { info => + logger.debug(s"Opening a channel from ${info.nodeId} -> ${nodeId} with amount ${amt}") + + } + n1.open( + nodeId = nodeId, + fundingSatoshis = amt, + pushMsat = Some(pushMSat), + feerateSatPerByte = None, + channelFlags = None) + } + } + val gen = fundedChannelIdF.flatMap(_ => bitcoindRpcClient.generate(6)) + + val opened = { + gen.flatMap { _ => + fundedChannelIdF.map { fcid => + awaitChannelOpened(n1, fcid) + fcid + } + } + } + opened.map { + case _ => + logger.debug( + s"Channel successfully opened ${n1.getNodeURI} -> ${n2.getNodeURI} with amount $amt") + } + opened + } + + def awaitChannelOpened( + client1: EclairRpcClient, + chanId: ChannelId)(implicit system: ActorSystem): Unit = { + EclairTestUtil.awaitUntilChannelNormal(client1, chanId) + } + + def getBitcoindRpc(eclairRpcClient: EclairRpcClient)(implicit system: ActorSystem): BitcoindRpcClient = { + val bitcoindRpc = { + val eclairAuth = eclairRpcClient.instance.authCredentials + val bitcoindRpcPort = eclairAuth.bitcoinRpcPort.get + + val bitcoindInstance = BitcoindInstance( + network = eclairRpcClient.instance.network, + uri = new URI("http://localhost:18333"), + rpcUri = new URI(s"http://localhost:${bitcoindRpcPort}"), + authCredentials = eclairRpcClient.instance.authCredentials.bitcoinAuthOpt.get, + None) + new BitcoindRpcClient(bitcoindInstance) + } + bitcoindRpc + } + + /** Shuts down an eclair daemon and the bitcoind daemon it is associated with */ + def shutdown(eclairRpcClient: EclairRpcClient)(implicit system: ActorSystem): Unit = { + val bitcoindRpc = getBitcoindRpc(eclairRpcClient) + + eclairRpcClient.stop() + + bitcoindRpc.stop() + + RpcUtil.awaitServerShutdown(bitcoindRpc) + } +} + +object EclairTestUtil extends EclairTestUtil \ No newline at end of file diff --git a/testkit/src/main/scala/org/bitcoins/rpc/BitcoindRpcTestUtil.scala b/testkit/src/main/scala/org/bitcoins/rpc/BitcoindRpcTestUtil.scala new file mode 100644 index 0000000000..25be55ffdd --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/rpc/BitcoindRpcTestUtil.scala @@ -0,0 +1,280 @@ +package org.bitcoins.rpc + +import java.io.{ File, PrintWriter } +import java.net.URI + +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import org.bitcoins.core.config.RegTest +import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.util.BitcoinSLogger +import org.bitcoins.rpc.client.BitcoindRpcClient +import org.bitcoins.rpc.config.{ BitcoindAuthCredentials, BitcoindInstance } + +import scala.concurrent.duration.{ DurationInt, FiniteDuration } +import scala.concurrent.{ ExecutionContext, Future, Promise } +import scala.util.{ Failure, Success, Try } + +trait BitcoindRpcTestUtil extends BitcoinSLogger { + + def randomDirName: String = + 0.until(5).map(_ => scala.util.Random.alphanumeric.head).mkString + + /** + * Creates a datadir and places the username/password combo + * in the bitcoin.conf in the datadir + */ + def authCredentials( + uri: URI, + rpcUri: URI, + zmqPort: Int, + pruneMode: Boolean): BitcoindAuthCredentials = { + val d = "/tmp/" + randomDirName + val f = new java.io.File(d) + f.mkdir() + val conf = new java.io.File(f.getAbsolutePath + "/bitcoin.conf") + conf.createNewFile() + val username = "random_user_name" + val pass = randomDirName + val pw = new PrintWriter(conf) + pw.write("rpcuser=" + username + "\n") + pw.write("rpcpassword=" + pass + "\n") + pw.write("rpcport=" + rpcUri.getPort + "\n") + pw.write("port=" + uri.getPort + "\n") + pw.write("daemon=1\n") + pw.write("server=1\n") + pw.write("debug=1\n") + pw.write("regtest=1\n") + pw.write("walletbroadcast=0\n") + + pw.write(s"zmqpubhashtx=tcp://127.0.0.1:${zmqPort}\n") + pw.write(s"zmqpubhashblock=tcp://127.0.0.1:${zmqPort}\n") + pw.write(s"zmqpubrawtx=tcp://127.0.0.1:${zmqPort}\n") + pw.write(s"zmqpubrawblock=tcp://127.0.0.1:${zmqPort}\n") + + if (pruneMode) { + logger.info(s"Creating pruned node for ${f.getAbsolutePath}") + pw.write("prune=1\n") + } + + pw.close() + BitcoindAuthCredentials(username, pass, rpcUri.getPort, f) + } + + lazy val network = RegTest + + def instance( + port: Int = randomPort, + rpcPort: Int = randomPort, + zmqPort: Int = randomPort, + pruneMode: Boolean = false): BitcoindInstance = { + val uri = new URI("http://localhost:" + port) + val rpcUri = new URI("http://localhost:" + rpcPort) + val auth = authCredentials(uri, rpcUri, zmqPort, pruneMode) + val instance = BitcoindInstance( + network = network, + uri = uri, + rpcUri = rpcUri, + authCredentials = auth, + zmqPortOpt = Some(zmqPort)) + + instance + } + + def randomPort: Int = { + val firstAttempt = Math.abs(scala.util.Random.nextInt % 15000) + if (firstAttempt < network.port) { + firstAttempt + network.port + } else firstAttempt + } + + def startServers(servers: Vector[BitcoindRpcClient])(implicit system: ActorSystem): Unit = { + servers.foreach(_.start()) + servers.foreach(RpcUtil.awaitServer(_)) + } + + def deleteTmpDir(dir: File): Boolean = { + if (!dir.isDirectory) { + dir.delete() + } else { + dir.listFiles().foreach(deleteTmpDir) + dir.delete() + } + } + + def awaitConnection( + from: BitcoindRpcClient, + to: BitcoindRpcClient, + duration: FiniteDuration = 100.milliseconds, + maxTries: Int = 50)(implicit system: ActorSystem): Unit = { + implicit val ec = system.dispatcher + + def isConnected(): Future[Boolean] = { + from.getAddedNodeInfo(to.getDaemon.uri) + .map { info => + info.nonEmpty && info.head.connected.contains(true) + } + } + + RpcUtil.awaitConditionF( + conditionF = () => isConnected(), + duration = duration, + maxTries = maxTries) + } + + def awaitSynced( + client1: BitcoindRpcClient, + client2: BitcoindRpcClient, + duration: FiniteDuration = 1.second, + maxTries: Int = 50)(implicit system: ActorSystem): Unit = { + implicit val ec = system.dispatcher + + def isSynced(): Future[Boolean] = { + client1.getBestBlockHash.flatMap { hash1 => + client2.getBestBlockHash.map { hash2 => + hash1 == hash2 + } + } + } + + RpcUtil.awaitConditionF( + conditionF = () => isSynced(), + duration = duration, + maxTries = maxTries) + } + + def awaitSameBlockHeight( + client1: BitcoindRpcClient, + client2: BitcoindRpcClient, + duration: FiniteDuration = 1.second, + maxTries: Int = 50)(implicit system: ActorSystem): Unit = { + implicit val ec = system.dispatcher + + def isSameBlockHeight(): Future[Boolean] = { + client1.getBlockCount.flatMap { count1 => + client2.getBlockCount.map { count2 => + count1 == count2 + } + } + } + + RpcUtil.awaitConditionF( + conditionF = () => isSameBlockHeight(), + duration = duration, + maxTries = maxTries) + } + + def awaitDisconnected( + from: BitcoindRpcClient, + to: BitcoindRpcClient, + duration: FiniteDuration = 100.milliseconds, + maxTries: Int = 50)(implicit system: ActorSystem): Unit = { + implicit val ec = system.dispatcher + + def isDisconnected(): Future[Boolean] = { + val f = from.getAddedNodeInfo(to.getDaemon.uri) + .map(info => info.isEmpty || info.head.connected.contains(false)) + + f + + } + + RpcUtil.awaitConditionF( + conditionF = () => isDisconnected(), + duration = duration, + maxTries = maxTries) + } + + /** Returns a pair of RpcClients that are connected with 100 blocks in the chain */ + def createNodePair( + port1: Int = randomPort, + rpcPort1: Int = randomPort, + port2: Int = randomPort, + rpcPort2: Int = randomPort)(implicit system: ActorSystem): Future[(BitcoindRpcClient, BitcoindRpcClient)] = { + implicit val m: ActorMaterializer = ActorMaterializer.create(system) + implicit val ec = m.executionContext + val client1: BitcoindRpcClient = new BitcoindRpcClient(instance(port1, rpcPort1)) + val client2: BitcoindRpcClient = new BitcoindRpcClient(instance(port2, rpcPort2)) + + client1.start() + client2.start() + + val try1 = Try(RpcUtil.awaitServer(client1)) + if (try1.isFailure) { + deleteNodePair(client1, client2) + throw try1.failed.get + } + + val try2 = Try(RpcUtil.awaitServer(client2)) + if (try2.isFailure) { + deleteNodePair(client1, client2) + throw try2.failed.get + } + + client1.addNode(client2.getDaemon.uri, "add").flatMap { _ => + val try3 = Try(awaitConnection( + from = client1, + to = client2, + duration = 1.seconds)) + + if (try3.isFailure) { + logger.error(s"Failed to connect client1 and client 2 ${try3.failed.get.getMessage}") + deleteNodePair(client1, client2) + throw try3.failed.get + } + client1.generate(100).map { _ => + + val try4 = Try(awaitSynced(client1, client2)) + + if (try4.isFailure) { + deleteNodePair(client1, client2) + throw try4.failed.get + } + (client1, client2) + } + } + } + + def deleteNodePair(client1: BitcoindRpcClient, client2: BitcoindRpcClient): Unit = { + client1.stop() + client2.stop() + deleteTmpDir(client1.getDaemon.authCredentials.datadir) + deleteTmpDir(client2.getDaemon.authCredentials.datadir) + () + } + + def hasSeenBlock(client1: BitcoindRpcClient, hash: DoubleSha256Digest)(implicit ec: ExecutionContext): Future[Boolean] = { + val p = Promise[Boolean]() + + client1.getBlock(hash).onComplete { + case Success(_) => p.success(true) + case Failure(_) => p.success(false) + } + + p.future + } + + def startedBitcoindRpcClient(instance: BitcoindInstance = BitcoindRpcTestUtil.instance())(implicit system: ActorSystem): BitcoindRpcClient = { + implicit val ec = system.dispatcher + //start the bitcoind instance so eclair can properly use it + val rpc = new BitcoindRpcClient(instance)(system) + rpc.start() + + logger.debug(s"Starting bitcoind at ${instance.authCredentials.datadir}") + RpcUtil.awaitServer(rpc) + + val blocksToGenerate = 102 + //fund the wallet by generating 102 blocks, need this to get over coinbase maturity + val _ = rpc.generate(blocksToGenerate) + + def isBlocksGenerated(): Future[Boolean] = { + rpc.getBlockCount.map(_ >= blocksToGenerate) + } + + RpcUtil.awaitConditionF(() => isBlocksGenerated()) + + rpc + } +} + +object BitcoindRpcTestUtil extends BitcoindRpcTestUtil \ No newline at end of file