2024 11 09 schnorrsig hashtype (#5764)

* Add SchnorrDigitalSignature.hashTypeOpt, add DigitalSignature.{hashTypeOpt,appendHashType}

* Remove TaprootKeyPath.hashTypeOpt param
This commit is contained in:
Chris Stewart 2024-11-09 12:52:45 -06:00 committed by GitHub
parent 35fdb07e2d
commit 17f965fd45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 137 additions and 96 deletions

View File

@ -13,13 +13,15 @@ class OrderedSchnorrSignaturesTest extends BitcoinSUnitTest {
SchnorrNonce(
"c4b89873c8753de3f0a9e94c4a6190badaa983513a6624a3469eb4577904bfea"
),
FieldElement.one
FieldElement.one,
hashTypeOpt = None
),
SchnorrDigitalSignature(
SchnorrNonce(
"92efe81609c773d97da2b084eb691f48ef5e926acc6eecd629f80fb1184711bc"
),
FieldElement.one
FieldElement.one,
hashTypeOpt = None
)
)

View File

@ -71,7 +71,8 @@ sealed trait CompletedOracleEvent extends OracleEvent {
def signatures: OrderedSchnorrSignatures = {
val unsorted = nonces.toVector
.zip(attestations)
.map(sigPieces => SchnorrDigitalSignature(sigPieces._1, sigPieces._2))
.map(sigPieces =>
SchnorrDigitalSignature(sigPieces._1, sigPieces._2, hashTypeOpt = None))
OrderedSchnorrSignatures.fromUnsorted(unsorted)
}
@ -84,7 +85,10 @@ sealed trait CompletedOracleEvent extends OracleEvent {
// announcementSignatures evaluate to true
val unsorted = ann.eventTLV.nonces
.zip(attestations)
.map(sigPieces => SchnorrDigitalSignature(sigPieces._1, sigPieces._2))
.map(sigPieces =>
SchnorrDigitalSignature(sigPieces._1,
sigPieces._2,
hashTypeOpt = None))
OracleAttestmentV0TLV(eventName,
pubkey,
unsorted,

View File

@ -37,7 +37,7 @@ case class EventDb(
eventDescriptorTLV: EventDescriptorTLV) {
lazy val sigOpt: Option[SchnorrDigitalSignature] =
attestationOpt.map(SchnorrDigitalSignature(nonce, _))
attestationOpt.map(SchnorrDigitalSignature(nonce, _, hashTypeOpt = None))
lazy val toOracleEvent: OracleEvent = OracleEvent.fromEventDbs(Vector(this))
}

View File

@ -74,13 +74,13 @@ trait TransactionSignatureChecker {
pubKey: SchnorrPublicKey,
witness: TaprootKeyPath,
taprootOptions: TaprootSerializationOptions): ScriptResult = {
if (witness.hashTypeOpt.contains(HashType.sigHashDefault)) {
if (witness.signature.hashTypeOpt.contains(HashType.sigHashDefault)) {
// cannot have DEFAULT hash type explicitly defined with BIP341
ScriptErrorSchnorrSigHashType
} else {
checkSchnorrSignature(txSigComponent = txSigComponent,
pubKey = pubKey,
schnorrSignature = witness.signature,
hashType = witness.hashType,
taprootOptions)
}
}
@ -89,7 +89,6 @@ trait TransactionSignatureChecker {
txSigComponent: TxSigComponent,
pubKey: SchnorrPublicKey,
schnorrSignature: SchnorrDigitalSignature,
hashType: HashType,
taprootOptions: TaprootSerializationOptions): ScriptResult = {
require(
txSigComponent.sigVersion == SigVersionTaprootKeySpend
@ -97,6 +96,8 @@ trait TransactionSignatureChecker {
s"SigVerison must be Taproot or Tapscript, got=${txSigComponent.sigVersion}"
)
val hashType =
schnorrSignature.hashTypeOpt.getOrElse(HashType.sigHashDefault)
// bip341 restricts valid hash types: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#common-signature-message
val validHashType = checkTaprootHashType(hashType)
if (!validHashType) {

View File

@ -179,7 +179,8 @@ object DLCUtil {
val index = allAdaptorPoints.indexOf(adaptorPoint)
val outcome: OracleOutcome = contractInfo.allOutcomes(index)
(SchnorrDigitalSignature(outcome.aggregateNonce, s), outcome)
(SchnorrDigitalSignature(outcome.aggregateNonce, s, hashTypeOpt = None),
outcome)
}
}

View File

@ -263,22 +263,18 @@ object TaprootWitness extends Factory[TaprootWitness] {
/** Spending a taproot output via the key path spend */
case class TaprootKeyPath(
signature: SchnorrDigitalSignature,
hashTypeOpt: Option[HashType],
annexOpt: Option[ByteVector])
extends TaprootWitness {
val hashType: HashType = hashTypeOpt.getOrElse(HashType.sigHashDefault)
val hashType: HashType =
signature.hashTypeOpt.getOrElse(HashType.sigHashDefault)
override val stack: Vector[ByteVector] = {
val sig = if (hashType == HashType.sigHashDefault) {
Vector(signature.bytes)
} else Vector(signature.bytes :+ hashType.byte)
annexOpt match {
case Some(annex) =>
annex +: sig
annex +: Vector(signature.bytes)
case None =>
sig
Vector(signature.bytes)
}
}
}
@ -294,36 +290,8 @@ object TaprootKeyPath extends Factory[TaprootKeyPath] {
}
}
def apply(
signature: SchnorrDigitalSignature,
hashType: HashType,
annexOpt: Option[ByteVector]): TaprootKeyPath = {
if (hashType == HashType.sigHashDefault) {
new TaprootKeyPath(signature, None, annexOpt)
} else {
new TaprootKeyPath(signature, Some(hashType), annexOpt)
}
}
def apply(
signature: SchnorrDigitalSignature,
hashTypeOpt: Option[HashType],
annexOpt: Option[ByteVector]): TaprootKeyPath = {
if (hashTypeOpt.contains(HashType.sigHashDefault)) {
new TaprootKeyPath(signature, None, annexOpt)
} else {
new TaprootKeyPath(signature, hashTypeOpt, annexOpt)
}
}
def apply(
signature: SchnorrDigitalSignature,
annexOpt: Option[ByteVector]): TaprootKeyPath = {
TaprootKeyPath(signature, None, annexOpt)
}
def apply(signature: SchnorrDigitalSignature): TaprootKeyPath = {
TaprootKeyPath(signature, None, None)
TaprootKeyPath(signature, None)
}
def fromStack(vec: Vector[ByteVector]): TaprootKeyPath = {
@ -347,15 +315,11 @@ object TaprootKeyPath extends Factory[TaprootKeyPath] {
}
}
val keyPath = if (sigBytes.length == 64) {
val keyPath = if (sigBytes.length == 64 || sigBytes.length == 65) {
// means SIGHASH_DEFAULT is implicitly encoded
// see: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#Common_signature_message
val sig = SchnorrDigitalSignature.fromBytes(sigBytes)
new TaprootKeyPath(sig, None, annexOpt)
} else if (sigBytes.length == 65) {
val sig = SchnorrDigitalSignature.fromBytes(sigBytes.dropRight(1))
val hashType = HashType.fromByte(sigBytes.last)
new TaprootKeyPath(sig, Some(hashType), annexOpt)
new TaprootKeyPath(sig, annexOpt)
} else {
sys.error(
s"Unknown sig bytes length, should be 64 or 65, got=${sigBytes.length}")

View File

@ -650,10 +650,8 @@ case class InputPSBTMap(elements: Vector[InputPSBTRecord])
keySpendSignatureOpt match {
case Some(keySpendSignature) =>
val sig = keySpendSignature.signature
val hashType =
sigHashTypeOpt.map(_.hashType).getOrElse(SIGHASH_DEFAULT)
val witnessScript = TaprootKeyPath(sig, hashType, None)
val witnessScript = TaprootKeyPath(sig, None)
Success(wipeAndAdd(EmptyScriptSignature, Some(witnessScript)))
case None =>
// todo add script spend support

View File

@ -38,7 +38,10 @@ class MuSigTest extends BitcoinSCryptoTest {
val sig = signAgg(Vector(s), aggNonce)
assert(sig == SchnorrDigitalSignature(aggNonce.schnorrNonce, s))
assert(
sig == SchnorrDigitalSignature(aggNonce.schnorrNonce,
s,
hashTypeOpt = None))
val aggPub = keySet.aggPubKey

View File

@ -186,11 +186,19 @@ trait LibSecp256k1CryptoRuntime extends CryptoRuntime {
data: ByteVector,
schnorrPubKey: SchnorrPublicKey,
signature: SchnorrDigitalSignature): Boolean = {
if (signature.hashTypeOpt.isDefined) {
// drop hashType byte
NativeSecp256k1.schnorrVerify(signature.bytes.dropRight(1).toArray,
data.toArray,
schnorrPubKey.bytes.toArray)
} else {
NativeSecp256k1.schnorrVerify(signature.bytes.toArray,
data.toArray,
schnorrPubKey.bytes.toArray)
}
}
// TODO: add a native implementation
override def schnorrComputeSigPoint(
data: ByteVector,

View File

@ -339,7 +339,7 @@ trait CryptoRuntime {
val challenge = x.multiply(FieldElement(e))
val sig = k.add(challenge)
SchnorrDigitalSignature(rx, sig)
SchnorrDigitalSignature(rx, sig, hashTypeOpt = None)
}
def schnorrVerify(

View File

@ -184,8 +184,9 @@ trait CryptoUtil extends CryptoRuntime {
override def schnorrVerify(
data: ByteVector,
schnorrPubKey: SchnorrPublicKey,
signature: SchnorrDigitalSignature): Boolean =
signature: SchnorrDigitalSignature): Boolean = {
cryptoRuntime.schnorrVerify(data, schnorrPubKey, signature)
}
override def schnorrComputeSigPoint(
data: ByteVector,

View File

@ -1,3 +1,6 @@
package org.bitcoins.crypto
abstract class DigitalSignature extends NetworkElement
abstract class DigitalSignature extends NetworkElement {
def hashTypeOpt: Option[HashType]
def appendHashType(hashType: HashType): DigitalSignature
}

View File

@ -1,10 +1,31 @@
package org.bitcoins.crypto
import org.bitcoins.crypto.HashType.byte
import scodec.bits.ByteVector
case class SchnorrDigitalSignature(rx: SchnorrNonce, sig: FieldElement)
case class SchnorrDigitalSignature(
rx: SchnorrNonce,
sig: FieldElement,
hashTypeOpt: Option[HashType])
extends DigitalSignature {
override val bytes: ByteVector = rx.bytes ++ sig.bytes
override val bytes: ByteVector = {
rx.bytes ++
sig.bytes ++
hashTypeOpt
.map(h => ByteVector.fromByte(byte(h)))
.getOrElse(ByteVector.empty)
}
require(
bytes.length == 64 || bytes.length == 65,
s"SchnorrDigitalSignature must be 64/65 bytes in size, got=${bytes.length}")
override def appendHashType(hashType: HashType): SchnorrDigitalSignature = {
require(this.hashTypeOpt.isEmpty,
"Cannot append HashType to signature which already has HashType")
val bytesWithHashType = bytes.:+(hashType.byte)
SchnorrDigitalSignature.fromBytes(bytesWithHashType)
}
}
object SchnorrDigitalSignature extends Factory[SchnorrDigitalSignature] {
@ -12,13 +33,18 @@ object SchnorrDigitalSignature extends Factory[SchnorrDigitalSignature] {
// If the sig is 65 bytes long, return sig[64] 0x00[20] and
// Verify(q, hashTapSighash(0x00 || SigMsg(sig[64], 0)), sig[0:64]).
override def fromBytes(bytes: ByteVector): SchnorrDigitalSignature = {
require(bytes.length == 64,
require(bytes.length == 64 || bytes.length == 65,
s"SchnorrDigitalSignature must be exactly 64 bytes, got $bytes")
SchnorrDigitalSignature(SchnorrNonce(bytes.take(32)),
FieldElement(bytes.drop(32)))
val r = bytes.take(32)
val s = bytes.drop(32).take(32)
val hashTypeOpt = if (bytes.length == 65) Some(bytes(64)) else None
SchnorrDigitalSignature(SchnorrNonce(r),
FieldElement(s),
hashTypeOpt.map(HashType.fromByte))
}
lazy val dummy: SchnorrDigitalSignature =
SchnorrDigitalSignature(FieldElement.one.getPublicKey.schnorrNonce,
FieldElement.one)
FieldElement.one,
hashTypeOpt = None)
}

View File

@ -170,6 +170,6 @@ object MuSigUtil {
case None => sSum
}
SchnorrDigitalSignature(aggPubNonce.schnorrNonce, s)
SchnorrDigitalSignature(aggPubNonce.schnorrNonce, s, hashTypeOpt = None)
}
}

View File

@ -314,7 +314,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces.head,
completedEvent.attestation
completedEvent.attestation,
hashTypeOpt = None
) == sig
)
assert(
@ -368,7 +369,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces.head,
completedEvent.attestations.head
completedEvent.attestations.head,
hashTypeOpt = None
) == signSig
)
@ -382,7 +384,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(1),
completedEvent.attestations(1)
completedEvent.attestations(1),
hashTypeOpt = None
) == sig100
)
@ -396,7 +399,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(2),
completedEvent.attestations(2)
completedEvent.attestations(2),
hashTypeOpt = None
) == sig10
)
@ -410,7 +414,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(3),
completedEvent.attestations(3)
completedEvent.attestations(3),
hashTypeOpt = None
) == sig1
)
case _: PendingOracleEvent | _: CompletedOracleEvent =>
@ -458,7 +463,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces.head,
completedEvent.attestations.head
completedEvent.attestations.head,
hashTypeOpt = None
) == signSig
)
@ -472,7 +478,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(1),
completedEvent.attestations(1)
completedEvent.attestations(1),
hashTypeOpt = None
) == sig100
)
@ -486,7 +493,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(2),
completedEvent.attestations(2)
completedEvent.attestations(2),
hashTypeOpt = None
) == sig10
)
@ -500,7 +508,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(3),
completedEvent.attestations(3)
completedEvent.attestations(3),
hashTypeOpt = None
) == sig1
)
case _: PendingOracleEvent | _: CompletedOracleEvent =>
@ -550,7 +559,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces.head,
completedEvent.attestations.head
completedEvent.attestations.head,
hashTypeOpt = None
) == sig100
)
@ -564,7 +574,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(1),
completedEvent.attestations(1)
completedEvent.attestations(1),
hashTypeOpt = None
) == sig10
)
@ -578,7 +589,8 @@ class DLCOracleTest extends DLCOracleFixture {
assert(
SchnorrDigitalSignature(
completedEvent.nonces(2),
completedEvent.attestations(2)
completedEvent.attestations(2),
hashTypeOpt = None
) == sig1
)
case _: PendingOracleEvent | _: CompletedOracleEvent =>
@ -725,7 +737,7 @@ class DLCOracleTest extends DLCOracleFixture {
futureTime,
None,
None,
SchnorrDigitalSignature(nonce, FieldElement.one),
SchnorrDigitalSignature(nonce, FieldElement.one, hashTypeOpt = None),
testDescriptor
)

View File

@ -34,7 +34,7 @@ class EventDAOTest extends DLCOracleDAOFixture {
RValueDb(nonce, eventName, HDPurpose(0), HDCoinType.Bitcoin, 0, 0, 0)
val dummySig: SchnorrDigitalSignature =
SchnorrDigitalSignature(nonce, FieldElement.one)
SchnorrDigitalSignature(nonce, FieldElement.one, hashTypeOpt = None)
def descriptor: EventDescriptorTLV = TLVGen.eventDescriptorTLV.sampleSome

View File

@ -49,7 +49,7 @@ class EventOutcomeDAOTest extends DLCOracleDAOFixture {
time,
None,
None,
SchnorrDigitalSignature(nonce, FieldElement.one),
SchnorrDigitalSignature(nonce, FieldElement.one, hashTypeOpt = None),
descriptor
)

View File

@ -216,7 +216,7 @@ class DLCMultiOracleEnumExecutionTest extends BitcoinSDualWalletTest {
.reduce(_.add(_))
val aggregateSignature =
SchnorrDigitalSignature(aggR, aggS)
SchnorrDigitalSignature(aggR, aggS, hashTypeOpt = None)
aggregateSignature == statusB.oracleSig
}
}

View File

@ -225,7 +225,7 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest {
.reduce(_.add(_))
val aggregateSignature =
SchnorrDigitalSignature(aggR, aggS)
SchnorrDigitalSignature(aggR, aggS, hashTypeOpt = None)
aggregateSignature == statusB.oracleSig
}
}

View File

@ -238,7 +238,7 @@ class DLCMultiOracleNumericExecutionTest
.reduce(_.add(_))
val aggregateSignature =
SchnorrDigitalSignature(aggR, aggS)
SchnorrDigitalSignature(aggR, aggS, hashTypeOpt = None)
aggregateSignature == statusB.oracleSig
}
}

View File

@ -236,7 +236,7 @@ class DLCNumericExecutionTest extends BitcoinSDualWalletTest {
.reduce(_.add(_))
val aggregateSignature =
SchnorrDigitalSignature(aggR, aggS)
SchnorrDigitalSignature(aggR, aggS, hashTypeOpt = None)
aggregateSignature == statusB.oracleSig
}
}

View File

@ -1871,7 +1871,8 @@ case class DLCWallet(override val walletApi: Wallet)(implicit
OracleSignatures.computeAggregateSignature(outcome, sigsUsed)
aggSig = SchnorrDigitalSignature(
outcome.aggregateNonce,
oracleSigSum.fieldElement
oracleSigSum.fieldElement,
hashTypeOpt = None
)
_ <- updateAggregateSignature(contractId, aggSig)

View File

@ -1290,7 +1290,7 @@ trait DLCTest {
sVals.reduce(_.add(_))
}
val aggSig = SchnorrDigitalSignature(aggR, aggS)
val aggSig = SchnorrDigitalSignature(aggR, aggS, hashTypeOpt = None)
// Must use stored adaptor sigs because adaptor signing nonce is not deterministic (auxRand)
val offerRefundSig = dlcOffer.dlcTxSigner.signRefundTx

View File

@ -234,11 +234,29 @@ sealed abstract class CryptoGenerators {
}
}
def schnorrDigitalSignature: Gen[SchnorrDigitalSignature] = {
def schnorrDigitalSignatureNoHashType: Gen[SchnorrDigitalSignature] = {
for {
privKey <- privateKey
hash <- CryptoGenerators.doubleSha256Digest
} yield privKey.schnorrSign(hash.bytes)
sigNoHashType = privKey.schnorrSign(hash.bytes)
} yield {
sigNoHashType
}
}
def schnorrDigitalSignatureHashType: Gen[SchnorrDigitalSignature] = {
for {
privKey <- privateKey
hash <- CryptoGenerators.doubleSha256Digest
sigNoHashType = privKey.schnorrSign(hash.bytes)
hashType <- hashType
} yield {
sigNoHashType.appendHashType(hashType)
}
}
def schnorrDigitalSignature: Gen[SchnorrDigitalSignature] = {
Gen.oneOf(schnorrDigitalSignatureHashType,
schnorrDigitalSignatureNoHashType)
}
def adaptorSignature: Gen[ECAdaptorSignature] = {

View File

@ -146,7 +146,7 @@ trait TLVGen {
def oracleAnnouncementV0TLV: Gen[OracleAnnouncementV0TLV] = {
for {
sig <- CryptoGenerators.schnorrDigitalSignature
sig <- CryptoGenerators.schnorrDigitalSignatureNoHashType
pubkey <- CryptoGenerators.schnorrPublicKey
eventTLV <- oracleEventV0TLV
} yield OracleAnnouncementV0TLV(sig, pubkey, eventTLV)
@ -159,7 +159,7 @@ trait TLVGen {
numSigs <- Gen.choose(1, 10)
unsorted <-
Gen
.listOfN(numSigs, CryptoGenerators.schnorrDigitalSignature)
.listOfN(numSigs, CryptoGenerators.schnorrDigitalSignatureNoHashType)
.map(_.toVector)
sigs = OrderedSchnorrSignatures.fromUnsorted(unsorted)
outcomes <-

View File

@ -45,9 +45,8 @@ sealed abstract class WitnessGenerators {
def taprootKeyPath: Gen[TaprootKeyPath] = {
for {
signature <- CryptoGenerators.schnorrDigitalSignature
hashType <- CryptoGenerators.hashType
annexOpt <- Gen.option(annex)
} yield TaprootKeyPath(signature, hashType, annexOpt)
} yield TaprootKeyPath(signature, annexOpt)
}
def taprootScriptPath: Gen[TaprootScriptPath] = {