Skip to content

Commit

Permalink
first segwit tx works
Browse files Browse the repository at this point in the history
  • Loading branch information
h0ngcha0 committed Jan 3, 2018
1 parent 8a92c4d commit 308b0fa
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 35 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ project/target/
.idea
.idea_modules/
logs/
.ensime_cache
.ensime_cache
ensime.sbt
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ scalacOptions := Seq(
libraryDependencies ++= Seq(
"com.madgag.spongycastle" % "core" % "1.52.0.0",
"org.scodec" %% "scodec-core" % "1.10.3",
"io.github.yzernik" %% "bitcoin-scodec" % "0.2.9-hc-3",
"io.github.yzernik" %% "bitcoin-scodec" % "0.2.9-hc-7",
"com.iheart" %% "ficus" % "1.4.1",
"org.typelevel" %% "cats-core" % "1.0.0-MF",
"com.github.mpilquist" %% "simulacrum" % "0.10.0",
Expand Down
6 changes: 3 additions & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
logLevel := Level.Warn

addSbtPlugin("io.spray" % "sbt-revolver" % "0.8.0")
addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.6")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.2")

addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M14")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC12")

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5")

Expand Down
2 changes: 0 additions & 2 deletions project/repo.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
ivyScala := ivyScala.value map { _.copy(overrideScalaVersion = true) }

resolvers in ThisBuild ++= Seq(
"Local Maven" at Path.userHome.asFile.toURI.toURL + ".m2/repository",
Resolver.jcenterRepo
Expand Down
24 changes: 24 additions & 0 deletions src/main/scala/me/hongchao/bitcoin4s/Utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package me.hongchao.bitcoin4s

import java.nio.{ByteBuffer, ByteOrder}

import scodec.Attempt
import scodec.bits.BitVector

package object Utils {
implicit class Rich[T](value: T) {
def toHex: String = "%02x".format(value)
Expand All @@ -24,6 +27,17 @@ package object Utils {
}
}

implicit class RichAttemptByteVector(attemptByteVector: Attempt[BitVector]) {
def toBytes(): Array[Byte] = {
attemptByteVector.toEither match {
case Left(error) =>
throw new RuntimeException(error.messageWithContext)
case Right(v) =>
v.toByteArray
}
}
}

implicit class RichSeq[T](seq: Seq[T]) {
def forceTake(n: Int): Seq[T] = {
val maybeNElements = seq.take(n)
Expand Down Expand Up @@ -84,4 +98,14 @@ package object Utils {
buffer.putInt((input & 0xffffffff).toInt)
bin
}

def uInt64ToBytes(input: Long): Seq[Byte] = uInt64ToBytes(input, ByteOrder.LITTLE_ENDIAN)

def uInt64ToBytes(input: Long, order: ByteOrder): Seq[Byte] = {
val bin = new Array[Byte](8)
val buffer = ByteBuffer.wrap(bin).order(order)
buffer.putLong(input)
bin
}

}
10 changes: 9 additions & 1 deletion src/main/scala/me/hongchao/bitcoin4s/crypto/Hash.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package me.hongchao.bitcoin4s.crypto

import org.spongycastle.crypto.Digest
import org.spongycastle.crypto.digests.{RIPEMD160Digest, SHA256Digest, SHA512Digest, SHA1Digest}
import org.spongycastle.crypto.digests.{RIPEMD160Digest, SHA1Digest, SHA256Digest, SHA512Digest}
import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator
import org.spongycastle.crypto.params.KeyParameter
import org.spongycastle.util.encoders.Hex


trait Hash {
Expand All @@ -18,6 +19,7 @@ trait Hash {
}

object Hash {

case object Sha1 extends Hash {
val digest = new SHA1Digest
}
Expand Down Expand Up @@ -56,4 +58,10 @@ object Hash {
keyParams.getKey
}
}

val zeros: Array[Byte] = Hash256(fromHex("0000000000000000000000000000000000000000000000000000000000000000"))

def fromHex(hex: String): Array[Byte] = {
Hex.decode(hex.stripPrefix("0x"))
}
}
8 changes: 3 additions & 5 deletions src/main/scala/me/hongchao/bitcoin4s/script/CryptoOp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import me.hongchao.bitcoin4s.script.FlowControlOp.OP_VERIFY
import me.hongchao.bitcoin4s.script.InterpreterError._
import me.hongchao.bitcoin4s.script.TransactionOps._
import me.hongchao.bitcoin4s.script.Interpreter._
import me.hongchao.bitcoin4s.script.SigVersion.SIGVERSION_BASE

import scala.annotation.tailrec
import cats.implicits._
Expand Down Expand Up @@ -291,7 +290,8 @@ object CryptoOp {
currentScript = currentScript,
inputIndex = state.inputIndex,
sigHashType = sigHashType,
sigVersion = state.sigVersion
sigVersion = state.sigVersion,
amount = state.amount
)

pubKey.verify(hashedTransaction, ecdsaSignature)
Expand All @@ -307,9 +307,7 @@ object CryptoOp {
case ScriptExecutionStage.ExecutingScriptP2SH =>
state.scriptP2sh.get // FIXME: get rid of get
case ScriptExecutionStage.ExecutingScriptWitness =>
//state.scriptP2sh.get
//FIXME: figure out what
Seq.empty
state.scriptWitness.get
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/main/scala/me/hongchao/bitcoin4s/script/Interpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ object InterpreterState {
scriptWitnessStack: Option[Seq[ScriptElement]],
flags: Seq[ScriptFlag],
transaction: Tx,
inputIndex: Int
inputIndex: Int,
amount: Long
): InterpreterState = {
InterpreterState(
scriptPubKey = scriptPubKey,
Expand All @@ -43,7 +44,8 @@ object InterpreterState {
transaction = transaction,
inputIndex = inputIndex,
flags = flags,
sigVersion = SigVersion.SIGVERSION_BASE
sigVersion = SigVersion.SIGVERSION_BASE,
amount = amount
)
}
}
Expand All @@ -53,13 +55,15 @@ case class InterpreterState(
scriptSig: Seq[ScriptElement],
currentScript: Seq[ScriptElement],
scriptP2sh: Option[Seq[ScriptElement]] = None,
scriptWitness: Option[Seq[ScriptElement]] = None,
scriptWitnessStack: Option[Seq[ScriptElement]] = None,
stack: Seq[ScriptElement] = Seq.empty,
altStack: Seq[ScriptElement] = Seq.empty,
flags: Seq[ScriptFlag],
opCount: Int = 0,
transaction: Tx,
inputIndex: Int,
amount: Long,
sigVersion: SigVersion,
scriptExecutionStage: ScriptExecutionStage = ScriptExecutionStage.ExecutingScriptSig
) {
Expand Down Expand Up @@ -371,6 +375,7 @@ object Interpreter {
tailRecMEvaluated(head.bytes.toBoolean())
case head :: tail =>
val maybeRebuiltScriptPubkeyAndStackFromWitness = tryRebuildScriptPubkeyAndStackFromWitness(state.scriptPubKey, state.scriptWitnessStack)

if (state.ScriptFlags.witness() && maybeRebuiltScriptPubkeyAndStackFromWitness.isDefined) {
val (rebuiltScriptPubkey, rebuiltStack) = maybeRebuiltScriptPubkeyAndStackFromWitness.get

Expand All @@ -380,6 +385,7 @@ object Interpreter {
stack = rebuiltStack,
altStack = Seq.empty,
opCount = 0,
scriptWitness = Some(rebuiltScriptPubkey),
sigVersion = SigVersion.SIGVERSION_WITNESS_V0,
scriptExecutionStage = ExecutingScriptWitness
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ sealed trait SigVersion {

object SigVersion {
case object SIGVERSION_BASE extends SigVersion { val value = 0 }
case object SIGVERSION_WITNESS_V0 extends SigVersion {val value = 1 }
case object SIGVERSION_WITNESS_V0 extends SigVersion { val value = 1 }
}
73 changes: 68 additions & 5 deletions src/main/scala/me/hongchao/bitcoin4s/script/TransactionOps.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package me.hongchao.bitcoin4s.script

import io.github.yzernik.bitcoinscodec.messages.Tx
import io.github.yzernik.bitcoinscodec.structures.{OutPoint, TxIn, TxOut}
import me.hongchao.bitcoin4s.script.CryptoOp.OP_CODESEPARATOR
import scodec.bits.ByteVector
import scodec.bits.{BitVector, ByteVector}
import me.hongchao.bitcoin4s.Utils._
import me.hongchao.bitcoin4s.crypto.Hash
import me.hongchao.bitcoin4s.script.SigVersion.{SIGVERSION_BASE, SIGVERSION_WITNESS_V0}
import scodec.Attempt

object TransactionOps {
val transactionVersion = 1

implicit class RichTx(tx: Tx) {
def transactionId(): ByteVector = {
// For witness stuff, whats the transaction id?
Tx.codec(transactionVersion).encode(tx).toEither match {
case Left(error) =>
throw new RuntimeException(error.messageWithContext)
Expand All @@ -24,12 +27,13 @@ object TransactionOps {
currentScript: Seq[ScriptElement],
inputIndex: Int,
sigHashType: SignatureHashType,
sigVersion: SigVersion
sigVersion: SigVersion,
amount: Long
): Seq[Byte] = sigVersion match {
case SIGVERSION_BASE =>
normalSigningHash(currentScript, inputIndex, sigHashType)
case SIGVERSION_WITNESS_V0 =>
segwitSigningHash()
segwitSigningHash(currentScript, inputIndex, amount, sigHashType)
}

def normalSigningHash(pubKeyScript: Seq[ScriptElement], inputIndex: Int, sigHashType: SignatureHashType): Seq[Byte] = {
Expand Down Expand Up @@ -71,8 +75,67 @@ object TransactionOps {
Hash.Hash256(serialisedTx ++ uint32ToBytes(sigHashType.value))
}

def segwitSigningHash(): Seq[Byte] = {
throw new NotImplementedError("Segwit hashing for transaction is not implemented")
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
// https://github.com/bitcoin/bitcoin/blob/f8528134fc188abc5c7175a19680206964a8fade/src/script/interpreter.cpp#L1113
def segwitSigningHash(pubKeyScript: Seq[ScriptElement], inputIndex: Int, amount: Long, sigHashType: SignatureHashType): Seq[Byte] = {
val prevOutsHash: Array[Byte] = if(!sigHashType.SIGHASH_ANYONECANPAY()) {
val prevOut = tx.tx_in.toArray.flatMap { txIn =>
OutPoint.codec.encode(txIn.previous_output).toBytes
}
Hash.Hash256(prevOut)
} else {
Hash.zeros
}

val txIn = tx.tx_in(inputIndex)

val sequencesHash = if (!sigHashType.SIGHASH_ANYONECANPAY() && !sigHashType.SIGHASH_NONE() && !sigHashType.SIGHASH_SINGLE()) {
val sequenceBytes = tx.tx_in.toArray.flatMap { txIn => uint32ToBytes(txIn.sequence) }
Hash.Hash256(sequenceBytes)
} else {
Hash.zeros
}

val prevOutBytes = OutPoint.codec.encode(txIn.previous_output).toBytes

val encodedScriptBytes = {
val scriptBytes = pubKeyScript.flatMap(_.bytes).toArray
TxIn.scriptCodec.encode(ByteVector(scriptBytes)).toBytes
}

val amountBytes = uInt64ToBytes(amount)

val sequenceBytes = uint32ToBytes(txIn.sequence)

val outputHash = if (!sigHashType.SIGHASH_SINGLE() && !sigHashType.SIGHASH_NONE()) {
val outputBytes: Array[Byte] = tx.tx_out.toArray.flatMap(TxOut.codec.encode(_).toBytes)
Hash.Hash256(outputBytes)
} else if (sigHashType.SIGHASH_SINGLE() && inputIndex < tx.tx_out.length) {
val txOut = tx.tx_out(inputIndex)
val outputBytes = TxOut.codec.encode(txOut).toBytes
Hash.Hash256(outputBytes)
} else {
Hash.zeros
}

val versionBytes = uint32ToBytes(tx.version)

val locktimeBytes = uint32ToBytes(tx.lock_time)
val sigHashTypeBytes = uint32ToBytes(sigHashType.value)

val preImage: Seq[Byte] =
versionBytes ++
prevOutsHash ++
sequencesHash ++
prevOutBytes ++
encodedScriptBytes ++
amountBytes ++
sequenceBytes ++
outputHash ++
locktimeBytes ++
sigHashTypeBytes

Hash.Hash256(preImage.toArray)
}

def removeSigScript(): Tx = {
Expand Down
7 changes: 4 additions & 3 deletions src/test/scala/me/hongchao/bitcoin4s/script/ScriptSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ class ScriptSpec extends Spec with ScriptTestRunner {
val rawScriptTests = scriptTestsConfig
.filter(_.length > 3)

val scriptTests = rawScriptTests.collect {
lazy val scriptTests = rawScriptTests.collect {
case elements @ (head :: tail) =>
if (head.isInstanceOf[ConfigList]) {
val witnessElement = head.toList.map(_.render)
val amount = (BigDecimal(witnessElement.last) * 10000000).toLong
val amount = (BigDecimal(witnessElement.last) * 100000000).toLong
val stringTail = tail.map(stripDoubleQuotes)
val comments = (stringTail.length == 5).option(stringTail.last).getOrElse("")
val witnesses = witnessElement.reverse.tail.flatMap { rawWitness =>
Expand Down Expand Up @@ -128,7 +128,8 @@ class ScriptSpec extends Spec with ScriptTestRunner {

val filteredScriptTests = scriptTests.filter { test =>
checkedExpectedResults.contains(test.expectedResult) && test.comments.equals("Basic P2WPKH")
}.take(1)
//checkedExpectedResults.contains(test.expectedResult)
}

filteredScriptTests.zipWithIndex.foreach(Function.tupled(run))
}
Expand Down
Loading

0 comments on commit 308b0fa

Please sign in to comment.