Successfully parsing raw bitcoin transactions, fixing a variety of bugs throughout the raw parsing api, and adding helpers inside of RawBitcoinSerializer

This commit is contained in:
Chris Stewart 2016-01-15 14:45:01 -06:00
parent 472e712daf
commit 40d15cd260
8 changed files with 176 additions and 26 deletions

View File

@ -24,4 +24,42 @@ trait RawBitcoinSerializer[T] {
*/
def write(t : T) : String
/**
* Adds the amount padding bytes needed to fix the size of the hex string
* for instance, vouts are required to be 4 bytes. If the number is just 1
* it will only take 1 byte. We need to pad the byte with an extra 3 bytes so the result is
* 01000000 instead of just 01
* @param bytesNeeded
* @param hex
* @return
*/
def addPadding(bytesNeeded : Int, hex : String) : String = {
val hexCharsNeeded = bytesNeeded * 2
val paddingNeeded = hexCharsNeeded - hex.size
val padding = for { i <- 0 until paddingNeeded} yield "0"
val paddedHex = hex + padding.mkString
paddedHex
}
/**
* Adds a preceding zero to a hex string.
* Example: if '1' was passed in, it would return the hex string '01'
* @param hex
* @return
*/
def addPrecedingZero(hex : String) = {
if (hex.size == 1) "0" + hex else hex
}
/**
* Flips the hex chars in a hex strings
* Example: abcd would become badc
* https://stackoverflow.com/questions/34799611/easiest-way-to-flip-the-endianness-of-a-byte-in-scala/34802270#34802270
* @param hex
* @return
*/
def flipHalfByte(hex : String) = hex.grouped(2).map(_.reverse).mkString
}

View File

@ -50,7 +50,27 @@ trait RawTransactionInputParser extends RawBitcoinSerializer[Seq[TransactionInpu
loop(bytes.tail, List(), numInputs).reverse
}
override def write(inputs : Seq[TransactionInput]) = ???
override def write(inputs : Seq[TransactionInput]) = {
val serializedInputs : Seq[String] = for {
input <- inputs
} yield {
val outPoint = RawTransactionOutPointParser.write(input.previousOutput)
val scriptSig = RawScriptSignatureParser.write(input.scriptSignature)
val sequenceWithoutPadding = input.sequence.toHexString
val paddingNeeded = 8 - sequenceWithoutPadding.size
val padding = for { i <- 0 until paddingNeeded} yield "0"
val sequence = sequenceWithoutPadding + padding.mkString
outPoint + scriptSig + sequence
}
val inputsSizeWithoutPadding = inputs.size.toHexString
val inputsSize = if (inputsSizeWithoutPadding.size == 1) "0" + inputsSizeWithoutPadding else inputsSizeWithoutPadding
logger.debug("Input size: " + inputsSize)
inputsSize + serializedInputs.mkString
}
}
object RawTransactionInputParser extends RawTransactionInputParser

View File

@ -19,9 +19,10 @@ trait RawTransactionOutPointParser extends RawBitcoinSerializer[TransactionOutPo
}
def write(outPoint : TransactionOutPoint) : String = {
val indexBytes : List[Byte] = List(0x00,0x00,0x00,outPoint.vout.toByte)
val indexHexWithoutPadding : String = addPrecedingZero(outPoint.vout.toHexString)
val indexHex = addPadding(4,indexHexWithoutPadding)
val littleEndianTxId = ScalacoinUtil.encodeHex(ScalacoinUtil.decodeHex(outPoint.txId).reverse)
littleEndianTxId + ScalacoinUtil.encodeHex(indexBytes)
littleEndianTxId + indexHex
}
}

View File

@ -1,7 +1,7 @@
package org.scalacoin.script.crypto
import org.scalacoin.script.constant.{ScriptConstantImpl, ScriptConstant, ScriptToken}
import org.scalacoin.util.ScalacoinUtil
import org.scalacoin.util.{CryptoUtil, ScalacoinUtil}
/**
@ -14,7 +14,7 @@ trait CryptoInterpreter extends ScalacoinUtil {
require(script.headOption.isDefined && script.head == OP_HASH160, "Script operation must be OP_HASH160")
val stackTop = stack.head
val hash = stackTop match {
case ScriptConstantImpl(x) => sha256Hash160(x)
case ScriptConstantImpl(x) => CryptoUtil.sha256Hash160(x)
case _ => throw new RuntimeException("Stack top should be of type ScriptConstant to call hash160 on it")
}
(hash :: stack, script.tail)
@ -39,16 +39,5 @@ trait CryptoInterpreter extends ScalacoinUtil {
/*def codeSeparator()*/
/**
* Does the following computation
* RIPEMD160(SHA256(hex))
* @param hex
* @return
*/
private def sha256Hash160(hex : String) : ScriptConstant = {
val bytes = decodeHex(hex)
val hash = org.bitcoinj.core.Utils.sha256hash160(bytes.toArray)
ScriptConstantImpl(encodeHex(hash))
}
}

View File

@ -0,0 +1,45 @@
package org.scalacoin.util
import org.bitcoinj.core.Sha256Hash
import org.scalacoin.script.constant.{ScriptConstantImpl, ScriptConstant}
/**
* Created by chris on 1/14/16.
* Utility cryptographic functions
*/
trait CryptoUtil extends ScalacoinUtil {
/**
* Does the following computation
* RIPEMD160(SHA256(hex))
* @param hex
* @return
*/
def sha256Hash160(hex : String) : ScriptConstant = {
val bytes = decodeHex(hex)
val hash = org.bitcoinj.core.Utils.sha256hash160(bytes.toArray)
ScriptConstantImpl(encodeHex(hash))
}
/**
* Performs sha256(sha256(hex))
* @param hex
* @return
*/
def doubleSHA256(hex : String) : String = {
doubleSHA256(decodeHex(hex))
}
/**
* Performs sha256(sha256(hex))
* @param hex
* @return
*/
def doubleSHA256(bytes : List[Byte]) : String = {
val hash : List[Byte] = Sha256Hash.hashTwice(bytes.toArray).toList
encodeHex(hash.reverse)
}
}
object CryptoUtil extends CryptoUtil

View File

@ -2,6 +2,7 @@ package org.scalacoin.marshallers.transaction
import org.scalacoin.protocol.transaction.TransactionInput
import org.scalacoin.script.constant.{ScriptConstantImpl, OP_0}
import org.scalacoin.util.TestUtil
import org.scalatest.{ FlatSpec, MustMatchers}
/**
@ -14,7 +15,11 @@ class RawTransactionInputParserTest extends FlatSpec with MustMatchers with RawT
"85d6b0da2edf96b282030d3f4f79d14cc8c882cfef1b3064170c850660317de100000000" +
"6f0047304402207df6dd8dad22d49c3c83d8031733c32a53719278eb7985d3b35b375d776f84f102207054f9209a1e87d55feafc90aa04c33008e5bae9191da22aeaa16efde96f41f00125512102b022902a0fdd71e831c37e4136c2754a59887be0618fb75336d7ab67e2982ff551ae" +
"ffffffff"
//from txid 44e504f5b7649d215be05ad9f09026dee95201244a3b218013c504a6a49a26ff
val rawTxInputs = "02df80e3e6eba7dcd4650281d3c13f140dafbb823a7227a78eb6ee9f6cedd040011b0000006a473044022040f91c48f4011bf2e2edb6621bfa8fb802241de939cb86f1872c99c580ef0fe402204fc27388bc525e1b655b5f5b35f9d601d28602432dd5672f29e0a47f5b8bbb26012102c114f376c98d12a0540c3a81ab99bb1c5234245c05e8239d09f48229f9ebf011ffffffff" +
"df80e3e6eba7dcd4650281d3c13f140dafbb823a7227a78eb6ee9f6cedd04001340000006b483045022100cf317c320d078c5b884c44e7488825dab5bcdf3f88c66314ac925770cd8773a7022033fde60d33cc2842ea73fce5d9cf4f8da6fadf414a75b7085efdcd300407f438012102605c23537b27b80157c770cd23e066cd11db3800d3066a38b9b592fc08ae9c70ffffffff"
"RawTransactionInputParser" must "parse a raw serialized transaction input" in {
println(TestUtil.rawTransaction)
val txInputs : Seq[TransactionInput] = read(rawTxInput)
txInputs.head.previousOutput.vout must be (0)
txInputs.head.previousOutput.txId must be ("e17d316006850c1764301befcf82c8c84cd1794f3f0d0382b296df2edab0d685")
@ -28,14 +33,37 @@ class RawTransactionInputParserTest extends FlatSpec with MustMatchers with RawT
it must "parse a multiple raw serialized inputs" in {
val rawTxInputs = "02df80e3e6eba7dcd4650281d3c13f140dafbb823a7227a78eb6ee9f6cedd040011b0000006a473044022040f91c48f4011bf2e2edb6621bfa8fb802241de939cb86f1872c99c580ef0fe402204fc27388bc525e1b655b5f5b35f9d601d28602432dd5672f29e0a47f5b8bbb26012102c114f376c98d12a0540c3a81ab99bb1c5234245c05e8239d09f48229f9ebf011ffffffff" +
"df80e3e6eba7dcd4650281d3c13f140dafbb823a7227a78eb6ee9f6cedd04001340000006b483045022100cf317c320d078c5b884c44e7488825dab5bcdf3f88c66314ac925770cd8773a7022033fde60d33cc2842ea73fce5d9cf4f8da6fadf414a75b7085efdcd300407f438012102605c23537b27b80157c770cd23e066cd11db3800d3066a38b9b592fc08ae9c70ffffffff"
val txInputs : Seq[TransactionInput] = RawTransactionInputParser.read(rawTxInputs)
txInputs.size must be (2)
val firstInput = txInputs.head
val secondInput = txInputs(1)
firstInput.previousOutput.txId must be ("0140d0ed6c9feeb68ea727723a82bbaf0d143fc1d3810265d4dca7ebe6e380df")
firstInput.previousOutput.vout must be (27)
secondInput.previousOutput.txId must be ("0140d0ed6c9feeb68ea727723a82bbaf0d143fc1d3810265d4dca7ebe6e380df")
secondInput.previousOutput.vout must be (52)
}
it must "write a single input" in {
val txInputs = RawTransactionInputParser.read(rawTxInput)
val serializedInputs = RawTransactionInputParser.write(txInputs)
serializedInputs must be (rawTxInput)
}
it must "write multiple inputs" in {
val txInputs = RawTransactionInputParser.read(rawTxInputs)
val serializedInputs = RawTransactionInputParser.write(txInputs)
serializedInputs must be(rawTxInputs)
}
it must "write multiple inputs from a tx with a locktime" in {
//from txid bdc221db675c06dbee2ae75d33e31cad4e2555efea10c337ff32c8cdf97f8e74
val rawTxInputs = "02" +
"fc37adbd036fb51b3f4f6f70474270939d6ff8c4ea697639f2b57dd6359e3070010000008b483045022100ad8e961fe3c22b2647d92b078f4c0cf81b3106ea5bf8b900ab8646aa4430216f022071d4edc2b5588be20ac4c2d07edd8ed069e10b2402d3dce2d3b835ccd075f283014104fa79182bbc26c708b5d9f36b8635947d4a834ea356cf612ede08395c295f962e0b1dc2557aba34188640e51a58ed547f2c89c8265cd0c04ff890d8435648746e0000000036219231b3043efdfb9405bbc2610baa73e340dddfe9c2a07b09bd3785ca6330000000008b483045022100cb097f8720d0c4665e8771fff5181b30584fd9e7d437fae21b440c94fe76d56902206f9b539ae26ec9688c54272d6a3309d93f17fb9835f382fff1ebeead84af2763014104fa79182bbc26c708b5d9f36b8635947d4a834ea356cf612ede08395c295f962e0b1dc2557aba34188640e51a58ed547f2c89c8265cd0c04ff890d8435648746effffffff"
val txInputs = RawTransactionInputParser.read(rawTxInputs)
val serializedTx = RawTransactionInputParser.write(txInputs)
serializedTx must be (rawTxInputs)
}
}

View File

@ -10,6 +10,7 @@ import org.scalatest.{FlatSpec, MustMatchers}
class RawTransactionOutPointParserTest extends FlatSpec with MustMatchers {
//txid cad1082e674a7bd3bc9ab1bc7804ba8a57523607c876b8eb2cbe645f2b1803d6
val rawOutPoint = "85d6b0da2edf96b282030d3f4f79d14cc8c882cfef1b3064170c850660317de100000000"
val rawOutPointLargeVout = "df80e3e6eba7dcd4650281d3c13f140dafbb823a7227a78eb6ee9f6cedd0400134000000"
"RawTransactionOutPointMarshaller" must "read a raw outpoint into a native scala TransactionOutPoint" in {
val outPoint = RawTransactionOutPointParser.read(rawOutPoint)
outPoint.txId must be ("e17d316006850c1764301befcf82c8c84cd1794f3f0d0382b296df2edab0d685")
@ -18,8 +19,7 @@ class RawTransactionOutPointParserTest extends FlatSpec with MustMatchers {
it must "parse a large vout for an outpoint" in {
val outPointLargeVout = "df80e3e6eba7dcd4650281d3c13f140dafbb823a7227a78eb6ee9f6cedd04001340000006b"
val outPoint = RawTransactionOutPointParser.read(outPointLargeVout)
val outPoint = RawTransactionOutPointParser.read(rawOutPointLargeVout)
outPoint.vout must be (52)
outPoint.txId must be ("0140d0ed6c9feeb68ea727723a82bbaf0d143fc1d3810265d4dca7ebe6e380df")
}
@ -29,5 +29,21 @@ class RawTransactionOutPointParserTest extends FlatSpec with MustMatchers {
actualSerialization must be (rawOutPoint)
}
it must "write a outpoint that has a large vout" in {
val outPoint = RawTransactionOutPointParser.read(rawOutPointLargeVout)
val serializedOutpoint = RawTransactionOutPointParser.write(outPoint)
serializedOutpoint must be (rawOutPointLargeVout)
}
it must "write this outpoint with vout index 1" in {
//from txid bdc221db675c06dbee2ae75d33e31cad4e2555efea10c337ff32c8cdf97f8e74
val rawOutPoint = "fc37adbd036fb51b3f4f6f70474270939d6ff8c4ea697639f2b57dd6359e307001000000"
val outPoint = RawTransactionOutPointParser.read(rawOutPoint)
outPoint.txId must be ("70309e35d67db5f2397669eac4f86f9d93704247706f4f3f1bb56f03bdad37fc")
val serializedOutPoint = RawTransactionOutPointParser.write(outPoint)
serializedOutPoint must be (rawOutPoint)
}
}

View File

@ -10,23 +10,36 @@ import org.scalatest.{FlatSpec, MustMatchers}
class RawTransactionParserTest extends FlatSpec with MustMatchers {
"RawTransactionParser" must "parse a raw transaction" in {
/* val tx : Transaction = RawTransactionParser.read(TestUtil.rawTransaction)
val tx : Transaction = RawTransactionParser.read(TestUtil.rawTransaction)
tx.version must be (1)
tx.inputs.size must be (2)
tx.outputs.size must be (2)
tx.lockTime must be (0)*/
//tx.txId must be ("44e504f5b7649d215be05ad9f09026dee95201244a3b218013c504a6a49a26ff")
tx.lockTime must be (0)
tx.txId must be ("44e504f5b7649d215be05ad9f09026dee95201244a3b218013c504a6a49a26ff")
}
it must "parse a transaction correctly with a locktime" in {
//txid bdc221db675c06dbee2ae75d33e31cad4e2555efea10c337ff32c8cdf97f8e74
val rawTx = "0100000002fc37adbd036fb51b3f4f6f70474270939d6ff8c4ea697639f2b57dd6359e3070010000008b483045022100ad8e961fe3c22b2647d92b078f4c0cf81b3106ea5bf8b900ab8646aa4430216f022071d4edc2b5588be20ac4c2d07edd8ed069e10b2402d3dce2d3b835ccd075f283014104fa79182bbc26c708b5d9f36b8635947d4a834ea356cf612ede08395c295f962e0b1dc2557aba34188640e51a58ed547f2c89c8265cd0c04ff890d8435648746e0000000036219231b3043efdfb9405bbc2610baa73e340dddfe9c2a07b09bd3785ca6330000000008b483045022100cb097f8720d0c4665e8771fff5181b30584fd9e7d437fae21b440c94fe76d56902206f9b539ae26ec9688c54272d6a3309d93f17fb9835f382fff1ebeead84af2763014104fa79182bbc26c708b5d9f36b8635947d4a834ea356cf612ede08395c295f962e0b1dc2557aba34188640e51a58ed547f2c89c8265cd0c04ff890d8435648746effffffff02905f0100000000001976a914a45bc47d00c3d2b0d0ea37cbf74b94cd1986ea7988aca0860100000000001976a914a45bc47d00c3d2b0d0ea37cbf74b94cd1986ea7988ac77d3a655"
val tx : Transaction = RawTransactionParser.read(rawTx)
tx.lockTime must be (1441324800)
tx.txId must be ("bdc221db675c06dbee2ae75d33e31cad4e2555efea10c337ff32c8cdf97f8e74")
tx.lockTime must be (1436996471)
}
/* it must "write a raw transaction" in {
val tx : Transaction = RawTransactionParser.read(TestUtil.rawTransaction)
val serializedTx = RawTransactionParser.write(tx)
serializedTx must be (TestUtil.rawTransaction)
}*/
it must "write a transaction with a locktime" in {
//txid bdc221db675c06dbee2ae75d33e31cad4e2555efea10c337ff32c8cdf97f8e74
val rawTxWithLockTime = "0100000002fc37adbd036fb51b3f4f6f70474270939d6ff8c4ea697639f2b57dd6359e3070010000008b483045022100ad8e961fe3c22b2647d92b078f4c0cf81b3106ea5bf8b900ab8646aa4430216f022071d4edc2b5588be20ac4c2d07edd8ed069e10b2402d3dce2d3b835ccd075f283014104fa79182bbc26c708b5d9f36b8635947d4a834ea356cf612ede08395c295f962e0b1dc2557aba34188640e51a58ed547f2c89c8265cd0c04ff890d8435648746e0000000036219231b3043efdfb9405bbc2610baa73e340dddfe9c2a07b09bd3785ca6330000000008b483045022100cb097f8720d0c4665e8771fff5181b30584fd9e7d437fae21b440c94fe76d56902206f9b539ae26ec9688c54272d6a3309d93f17fb9835f382fff1ebeead84af2763014104fa79182bbc26c708b5d9f36b8635947d4a834ea356cf612ede08395c295f962e0b1dc2557aba34188640e51a58ed547f2c89c8265cd0c04ff890d8435648746effffffff02905f0100000000001976a914a45bc47d00c3d2b0d0ea37cbf74b94cd1986ea7988aca0860100000000001976a914a45bc47d00c3d2b0d0ea37cbf74b94cd1986ea7988ac77d3a655"
val tx = RawTransactionParser.read(rawTxWithLockTime)
val serializedTx = RawTransactionParser.write(tx)
serializedTx must be (rawTxWithLockTime)
}
}