Skip to content

Commit

Permalink
Turned BitcoinTransaction into an almost-fully mutable struct
Browse files Browse the repository at this point in the history
Simplified attaching signature/witness data to a draft transaction
  • Loading branch information
craigwrong committed Dec 9, 2024
1 parent 903ddcd commit 96512cd
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 57 deletions.
24 changes: 4 additions & 20 deletions src/bitcoin-base/transaction/BitcoinTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ public struct BitcoinTransaction: Equatable, Sendable {
// MARK: - Instance Properties

/// The transaction's version.
public let version: TransactionVersion
public var version: TransactionVersion

/// Lock time value applied to this transaction. It represents the earliest time at which this transaction should be considered valid.
public let locktime: TransactionLocktime
public var locktime: TransactionLocktime

/// All of the inputs consumed (coins spent) by this transaction.
public let inputs: [TransactionInput]
public var inputs: [TransactionInput]

/// The new outputs to be created by this transaction.
public let outputs: [TransactionOutput]
public var outputs: [TransactionOutput]

// MARK: - Computed Properties

Expand Down Expand Up @@ -71,22 +71,6 @@ public struct BitcoinTransaction: Equatable, Sendable {
return .init(transaction: id, output: outputIndex)
}

public func withUnlockScript(_ script: BitcoinScript, input inputIndex: Int) -> Self {
precondition(inputs.indices.contains(inputIndex))
let oldInput = inputs[inputIndex]
let newInput = TransactionInput(outpoint: oldInput.outpoint, sequence: oldInput.sequence, script: script, witness: oldInput.witness)
let newInputs = inputs[..<inputIndex] + [newInput] + inputs[inputIndex.advanced(by: 1)...]
return .init(version: version, locktime: locktime, inputs: .init(newInputs), outputs: outputs)
}

public func withWitness(_ witnessElements: [Data], input inputIndex: Int) -> Self {
precondition(inputs.indices.contains(inputIndex))
let oldInput = inputs[inputIndex]
let newInput = TransactionInput(outpoint: oldInput.outpoint, sequence: oldInput.sequence, script: oldInput.script, witness: .init(witnessElements))
let newInputs = inputs[..<inputIndex] + [newInput] + inputs[inputIndex.advanced(by: 1)...]
return .init(version: version, locktime: locktime, inputs: .init(newInputs), outputs: outputs)
}

// MARK: - Type Properties

/// The total amount of bitcoin supply is actually less than this number. But `maxMoney` as a limit for any amount is a consensus-critical constant.
Expand Down
4 changes: 2 additions & 2 deletions src/bitcoin-base/transaction/TransactionOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public struct TransactionOutput: Equatable, Sendable {
}

/// The amount in _satoshis_ encumbered by this output.
public let value: BitcoinAmount
public var value: BitcoinAmount

/// The script that locks this output.
public let script: BitcoinScript
public var script: BitcoinScript
}

/// Data extensions.
Expand Down
8 changes: 4 additions & 4 deletions src/bitcoin-base/transaction/TransationInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ public struct TransactionInput: Equatable, Sendable {
// MARK: - Instance Properties

/// A reference to a previously unspent output of a prior transaction.
public let outpoint: TransactionOutpoint
public var outpoint: TransactionOutpoint

/// The sequence number for this input.
public let sequence: InputSequence
public var sequence: InputSequence

/// The script that unlocks the output associated with this input.
public let script: BitcoinScript
public var script: BitcoinScript

/// BIP141 - Segregated witness data associated with this input.
public let witness: InputWitness?
public var witness: InputWitness?
}

/// Data extensions.
Expand Down
12 changes: 6 additions & 6 deletions src/bitcoin-wallet/TransactionSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ public class TransactionSigner {

if let witnessScript {
let witness = [Data()] + signatures
transaction = transaction.withWitness(witness, input: inputIndex)
transaction.inputs[inputIndex].witness = .init(witness)
if lockScript.isPayToScriptHash {
let redeemScriptP2WSH = BitcoinScript.payToWitnessScriptHash(witnessScript)
transaction = transaction.withUnlockScript([.encodeMinimally(redeemScriptP2WSH.data)], input: inputIndex)
transaction.inputs[inputIndex].script = [.encodeMinimally(redeemScriptP2WSH.data)]
}
} else {
let unlockScript = BitcoinScript([.zero] + signatures.map { ScriptOperation.encodeMinimally($0) })
transaction = transaction.withUnlockScript(unlockScript, input: inputIndex)
transaction.inputs[inputIndex].script = unlockScript
}
return transaction
}
Expand Down Expand Up @@ -127,14 +127,14 @@ public class TransactionSigner {
}
if lockScript.isPayToWitnessKeyHash || lockScript.isPayToScriptHash || lockScript.isPayToTaproot {
// For pay-to-witness-public-key-hash we sign a different hash and we add the signature and public key to the input's _witness_.
transaction = transaction.withWitness(witnessData, input: inputIndex)
transaction.inputs[inputIndex].witness = .init(witnessData)
}
if lockScript.isPayToPublicKey || lockScript.isPayToPublicKeyHash {
let ops = witnessData.map { ScriptOperation.pushBytes($0) }
transaction = transaction.withUnlockScript(.init(ops), input: inputIndex)
transaction.inputs[inputIndex].script = .init(ops)
}
if lockScript.isPayToScriptHash {
transaction = transaction.withUnlockScript([.encodeMinimally(redeemScript.data)], input: inputIndex)
transaction.inputs[inputIndex].script = [.encodeMinimally(redeemScript.data)]
}
return transaction
}
Expand Down
54 changes: 29 additions & 25 deletions test/bitcoin/BaseDocumentationExamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct BaseDocumentationExamples {

// A transaction spending all of the outputs from our coinbase transaction.
// These outpoints all happen to come from the same transaction but they don't necessarilly have to.
let spend = BitcoinTransaction(inputs: [
var spend = BitcoinTransaction(inputs: [
.init(outpoint: fund.outpoint(0)),
.init(outpoint: fund.outpoint(1)),
.init(outpoint: fund.outpoint(2)),
Expand All @@ -44,31 +44,31 @@ struct BaseDocumentationExamples {
let sighash0 = hasher.value
let signature0 = sk.sign(hash: sighash0)
let signatureExt0 = ExtendedSignature(signature0, .all)
var tx_signed = spend.withUnlockScript([.pushBytes(signatureExt0.data)], input: 0)
spend.inputs[0].script = [.pushBytes(signatureExt0.data)]

// For pay-to-public-key-hash we need to also add the public key to the unlock script.
hasher.set(input: 1, prevout: prevout1)
let sighash1 = hasher.value
let signature1 = sk.sign(hash: sighash1)
let signatureExt1 = ExtendedSignature(signature1, .all)
tx_signed = tx_signed.withUnlockScript([.pushBytes(signatureExt1.data), .pushBytes(sk.publicKey.data)], input: 1)
spend.inputs[1].script = [.pushBytes(signatureExt1.data), .pushBytes(sk.publicKey.data)]

// For pay-to-witness-public-key-hash we sign a different hash and we add the signature and public key to the input's _witness_.
hasher.set(input: 2, sigVersion: .witnessV0, prevout: prevout2)
let sighash2 = hasher.value
let signature2 = sk.sign(hash: sighash2)
let signatureExt2 = ExtendedSignature(signature2, .all)
tx_signed = tx_signed.withWitness([signatureExt2.data, sk.publicKey.data], input: 2)
spend.inputs[2].witness = .init([signatureExt2.data, sk.publicKey.data])

// For pay-to-taproot with key we need a different sighash and a _tweaked_ version of our secret key to sign it. We use the default sighash type which is equal to _all_.
hasher.set(input: 3, sigVersion: .witnessV1, prevouts: [prevout0, prevout1, prevout2, prevout3], sighashType: Optional.none)
let sighash3 = hasher.value
let signature3 = sk.taprootSecretKey().sign(hash: sighash3, signatureType: .schnorr)
let signatureExt3 = ExtendedSignature(signature3, Optional.none)
// The witness only requires the signature
tx_signed = tx_signed.withWitness([signatureExt3.data], input: 3)
spend.inputs[3].witness = .init([signatureExt3.data])

let result = tx_signed.verifyScript(prevouts: [prevout0, prevout1, prevout2, prevout3])
let result = spend.verifyScript(prevouts: [prevout0, prevout1, prevout2, prevout3])
#expect(result)
}

Expand All @@ -80,7 +80,7 @@ struct BaseDocumentationExamples {
.init(value: 100, script: .payToMultiSignature(2, of: sk1.publicKey, sk2.publicKey, sk3.publicKey)),
])

let spend = BitcoinTransaction(inputs: [.init(outpoint: fund.outpoint(0))], outputs: [
var spend = BitcoinTransaction(inputs: [.init(outpoint: fund.outpoint(0))], outputs: [
.init(value: 100)
])

Expand All @@ -100,9 +100,9 @@ struct BaseDocumentationExamples {
let signatureExt1 = ExtendedSignature(signature1, sighashType)

// Signatures need to appear in the right order, plus a dummy value
let tx_signed = spend.withUnlockScript([.zero, .pushBytes(signatureExt0.data), .pushBytes(signatureExt1.data)], input: input)
spend.inputs[input].script = [.zero, .pushBytes(signatureExt0.data), .pushBytes(signatureExt1.data)]

let result = tx_signed.verifyScript(prevouts: [prevout])
let result = spend.verifyScript(prevouts: [prevout])
#expect(result)
}

Expand All @@ -115,7 +115,7 @@ struct BaseDocumentationExamples {
.init(value: 100, script: .payToScriptHash(redeemScript)),
])

let spend = BitcoinTransaction(inputs: [
var spend = BitcoinTransaction(inputs: [
.init(outpoint: fund.outpoint(0)),
], outputs: [.init(value: 100)])

Expand All @@ -132,9 +132,9 @@ struct BaseDocumentationExamples {
let signatureExt1 = ExtendedSignature(signature1, sighashType)

// Signatures need to appear in the right order, plus a dummy value
let tx_signed = spend.withUnlockScript([.zero, .pushBytes(signatureExt0.data), .pushBytes(signatureExt1.data), .encodeMinimally(redeemScript.data)], input: input)
spend.inputs[input].script = [.zero, .pushBytes(signatureExt0.data), .pushBytes(signatureExt1.data), .encodeMinimally(redeemScript.data)]

let result = tx_signed.verifyScript(prevouts: [prevout])
let result = spend.verifyScript(prevouts: [prevout])
#expect(result)
}

Expand All @@ -148,7 +148,7 @@ struct BaseDocumentationExamples {
.init(value: 100, script: .payToWitnessScriptHash(redeemScript)),
])

let spend = BitcoinTransaction(inputs: [
var spend = BitcoinTransaction(inputs: [
.init(outpoint: fund.outpoint(0)),
], outputs: [
.init(value: 100)
Expand All @@ -168,9 +168,9 @@ struct BaseDocumentationExamples {
let signatureExt1 = ExtendedSignature(signature1, sighashType)

// Signatures need to appear in the right order, plus a dummy value
let tx_signed = spend.withWitness([Data(), signatureExt0.data, signatureExt1.data, redeemScript.data], input: input)
spend.inputs[input].witness = .init([Data(), signatureExt0.data, signatureExt1.data, redeemScript.data])

let result = tx_signed.verifyScript(prevouts: [prevout])
let result = spend.verifyScript(prevouts: [prevout])
#expect(result)
}

Expand All @@ -186,7 +186,7 @@ struct BaseDocumentationExamples {
let prevout = fund.outputs[0]

// Spending transaction.
let spend = BitcoinTransaction(inputs: [
var spend = BitcoinTransaction(inputs: [
.init(outpoint: fund.outpoint(0)),
], outputs: [
.init(value: 100)
Expand All @@ -203,9 +203,11 @@ struct BaseDocumentationExamples {
let sighash = hasher.value
let signature = sk.sign(hash: sighash)
let signatureExt = ExtendedSignature(signature, sighashType)
let tx_signed = spend.withWitness([signatureExt.data, publicKey.data], input: input).withUnlockScript([.encodeMinimally(redeemScript.data)], input: input)

let result = tx_signed.verifyScript(prevouts: [prevout])
spend.inputs[input].witness = .init([signatureExt.data, publicKey.data])
spend.inputs[input].script = [.encodeMinimally(redeemScript.data)]

let result = spend.verifyScript(prevouts: [prevout])
#expect(result)
}

Expand All @@ -222,7 +224,7 @@ struct BaseDocumentationExamples {
let prevout = fund.outputs[0]

// Spending transaction.
let spend = BitcoinTransaction(inputs: [
var spend = BitcoinTransaction(inputs: [
.init(outpoint: fund.outpoint(0)),
], outputs: [
.init(value: 100)
Expand All @@ -241,9 +243,11 @@ struct BaseDocumentationExamples {
let signatureExt1 = ExtendedSignature(signature1, sighashType)

// Signatures need to appear in the right order, plus a dummy value
let tx_signed = spend.withWitness([Data(), signatureExt0.data, signatureExt1.data, witnessScript.data], input: input).withUnlockScript([.encodeMinimally(redeemScript.data)], input: input)

let result = tx_signed.verifyScript(prevouts: [prevout])
spend.inputs[input].witness = .init([Data(), signatureExt0.data, signatureExt1.data, witnessScript.data])
spend.inputs[input].script = [.encodeMinimally(redeemScript.data)]

let result = spend.verifyScript(prevouts: [prevout])
#expect(result)
}

Expand Down Expand Up @@ -273,7 +277,7 @@ struct BaseDocumentationExamples {

let prevouts = [fund.outputs[0]]
// Spending transaction.
let spend = BitcoinTransaction(inputs: [
var spend = BitcoinTransaction(inputs: [
.init(outpoint: fund.outpoint(0)),
], outputs: [.init(value: 100)])

Expand All @@ -292,15 +296,15 @@ struct BaseDocumentationExamples {
let signature3 = sk3.sign(hash: sighash, signatureType: .schnorr)
let signatureExt3 = ExtendedSignature(signature3, sighashType)

let tx_signed = spend.withWitness([
spend.inputs[input].witness = .init([
signatureExt3.data,
Data(),
signatureExt1.data,
tapscript,
controlBlocks[0]
], input: input)
])

let result = tx_signed.verifyScript(prevouts: prevouts)
let result = spend.verifyScript(prevouts: prevouts)
#expect(result)
}

Expand Down

0 comments on commit 96512cd

Please sign in to comment.