From 76bc74b46182bfd3e450f8026578305d64927dc2 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 30 Apr 2024 17:44:09 +0200 Subject: [PATCH] Add recoverUnsignedTx to EthereumSignedTransaction --- .../Transaction/EthereumTransaction.swift | 136 +++++++++--------- .../TransactionTests/TransactionTests.swift | 105 ++++++++++---- 2 files changed, 147 insertions(+), 94 deletions(-) diff --git a/Sources/Core/Transaction/EthereumTransaction.swift b/Sources/Core/Transaction/EthereumTransaction.swift index 126b28a8..f06b9c89 100644 --- a/Sources/Core/Transaction/EthereumTransaction.swift +++ b/Sources/Core/Transaction/EthereumTransaction.swift @@ -124,19 +124,8 @@ public struct EthereumTransaction: Codable { guard let nonce = nonce, let gasPrice = gasPrice, let gasLimit = gasLimit, let value = value else { throw EthereumSignedTransaction.Error.transactionInvalid } - let rlp = RLPItem( - nonce: nonce, - gasPrice: gasPrice, - gasLimit: gasLimit, - to: to, - value: value, - data: data, - v: chainId, - r: 0, - s: 0 - ) - let rawRlp = try RLPEncoder().encode(rlp) - let signature = try privateKey.sign(message: rawRlp) + let messageToSign = try self.messageToSign(chainId: chainId) + let signature = try privateKey.sign(message: messageToSign) let v: BigUInt if chainId.quantity == 0 { @@ -190,24 +179,8 @@ public struct EthereumTransaction: Codable { if chainId.quantity == BigUInt(0) { throw EthereumSignedTransaction.Error.chainIdNotSet(msg: "EIP1559 transactions need a chainId") } - - let rlp = RLPItem( - nonce: nonce, - gasPrice: gasPrice ?? EthereumQuantity(integerLiteral: 0), - maxFeePerGas: maxFeePerGas, - maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: gasLimit, - to: to, - value: value, - data: data, - chainId: chainId, - accessList: accessList, - transactionType: transactionType - ) - let rawRlp = try RLPEncoder().encode(rlp) - var messageToSign = Bytes() - messageToSign.append(0x02) - messageToSign.append(contentsOf: rawRlp) + + var messageToSign = try self.messageToSign(chainId: chainId) let signature = try privateKey.sign(message: messageToSign) let v = BigUInt(signature.v) @@ -233,6 +206,58 @@ public struct EthereumTransaction: Codable { } } +public extension EthereumTransaction { + + fileprivate func messageToSign(chainId: EthereumQuantity) throws -> Bytes { + let rlpEncoder = RLPEncoder() + + if self.transactionType == .legacy { + guard let nonce = nonce, let gasPrice = gasPrice, let gasLimit = gasLimit, let value = value else { + throw EthereumSignedTransaction.Error.transactionInvalid + } + let rlp = RLPItem( + nonce: nonce, + gasPrice: gasPrice, + gasLimit: gasLimit, + to: to, + value: value, + data: data, + v: chainId, + r: 0, + s: 0 + ) + let rawRlp = try RLPEncoder().encode(rlp) + return rawRlp + } else if self.transactionType == .eip1559 { + guard let nonce = nonce, let maxFeePerGas = maxFeePerGas, let maxPriorityFeePerGas = maxPriorityFeePerGas, + let gasLimit = gasLimit, let value = value else { + throw EthereumSignedTransaction.Error.transactionInvalid + } + let rlp = RLPItem( + nonce: nonce, + gasPrice: gasPrice ?? EthereumQuantity(integerLiteral: 0), + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas, + gasLimit: gasLimit, + to: to, + value: value, + data: data, + chainId: chainId, + accessList: accessList, + transactionType: transactionType + ) + let rawRlp = try rlpEncoder.encode(rlp) + var messageToSign = Bytes() + messageToSign.append(0x02) + messageToSign.append(contentsOf: rawRlp) + + return messageToSign + } else { + throw EthereumSignedTransaction.Error.transactionInvalid + } + } +} + public struct EthereumSignedTransaction { // MARK: - Properties @@ -720,7 +745,7 @@ extension EthereumSignedTransaction { } public func publicKey() throws -> EthereumPublicKey { - let messageToSign = try self.unsignedMessage() + let messageToSign = try self.unsignedTransaction().messageToSign(chainId: self.chainId) var recId: BigUInt if v.quantity >= BigUInt(35) + (BigUInt(2) * chainId.quantity) { recId = v.quantity - BigUInt(35) - (BigUInt(2) * chainId.quantity) @@ -734,40 +759,19 @@ extension EthereumSignedTransaction { return try EthereumPublicKey(message: messageToSign, v: EthereumQuantity(quantity: recId), r: self.r, s: self.s) } - private func unsignedMessage() throws -> Bytes { - let rlpEncoder = RLPEncoder() - - if self.transactionType == .legacy { - let legacyrlp = RLPItem( - nonce: self.nonce, - gasPrice: self.gasPrice, - gasLimit: self.gasLimit, - to: self.to, - value: self.value, - data: self.data, - v: self.chainId, - r: 0, - s: 0 - ) - let rawRlp = try RLPEncoder().encode(legacyrlp) - return rawRlp - } else if self.transactionType == .eip1559 { - let rlp = self.rlp() - - if let arr = rlp.array{ - let unsignedRlp = Array(arr[0..<(arr.count - 3)]) - var messageToSign = Bytes() - let unsignedRlpBytes = try rlpEncoder.encode(RLPItem.array(unsignedRlp)) - messageToSign.append(0x02) - messageToSign.append(contentsOf: unsignedRlpBytes) - return messageToSign - } else { - throw Error.transactionInvalid - } - - } else { - throw Error.rlpItemInvalid - } + public func unsignedTransaction() throws -> EthereumTransaction { + return EthereumTransaction( + nonce: self.nonce, + gasPrice: self.gasPrice, + maxFeePerGas: self.maxFeePerGas, + maxPriorityFeePerGas: self.maxPriorityFeePerGas, + gasLimit: self.gasLimit, + to: self.to, + value: self.value, + data: self.data, + accessList: self.accessList, + transactionType: self.transactionType + ) } } diff --git a/Tests/Web3Tests/TransactionTests/TransactionTests.swift b/Tests/Web3Tests/TransactionTests/TransactionTests.swift index eec86cad..2290a547 100644 --- a/Tests/Web3Tests/TransactionTests/TransactionTests.swift +++ b/Tests/Web3Tests/TransactionTests/TransactionTests.swift @@ -14,7 +14,7 @@ class TransactionTests: QuickSpec { override func spec() { describe("transaction tests") { context("signing legacy") { - + let p = try? EthereumPrivateKey( hexPrivateKey: "0x94eca03b4541a0eb0d173e321b6f960d08cfe4c5a75fa00ebe0a3d283c609c3a" ) @@ -22,36 +22,36 @@ class TransactionTests: QuickSpec { it("should not be nil") { expect(t).toNot(beNil()) } - + guard let to = t, let privateKey = p else { return } - + let tx = EthereumTransaction(nonce: 0, gasPrice: EthereumQuantity(quantity: 21.gwei), gasLimit: 21000, to: to, value: EthereumQuantity(quantity: 1.eth)) - + // Sign transaction with private key let newTx = try? tx.sign(with: privateKey, chainId: 3) it("should not be nil") { expect(newTx).toNot(beNil()) } - + let expectedTransaction = "0xf86c808504e3b2920082520894867aeeeed428ed9ba7f97fc7e16f16dfcf02f375880de0b6b3a76400008029a099060c9146c68716da3a79533866dc941a03b171911d675f518c97a73882f7a6a0019167adb26b602501c954e7793e798407836f524b9778f5be6ebece5fc998c6" - + it("should produce the expected rlp encoding") { expect(try? RLPEncoder().encode(newTx!.rlp()).hexString(prefix: true)) == expectedTransaction } - + // Check validity it("should be a valid tx") { expect(newTx!.verifySignature()) == true } - + let afterHashValue = newTx!.hashValue it("should create a different hashValue") { expect(tx.hashValue) != afterHashValue } } - + context("signing eip1559") { let p = try? EthereumPrivateKey( hexPrivateKey: "0x94eca03b4541a0eb0d173e321b6f960d08cfe4c5a75fa00ebe0a3d283c609c3a" @@ -60,13 +60,13 @@ class TransactionTests: QuickSpec { it("should not be nil") { expect(t).toNot(beNil()) } - + guard let to = t, let privateKey = p else { return } - + // Basic TX - + let basicTx = EthereumTransaction( nonce: 0, gasPrice: EthereumQuantity(quantity: 21.gwei), @@ -81,18 +81,18 @@ class TransactionTests: QuickSpec { it("should not be nil") { expect(basicSignature).toNot(beNil()) } - + let expectedBasicTx = "0x02f8730180843b9aca008504e3b2920082520894867aeeeed428ed9ba7f97fc7e16f16dfcf02f375880de0b6b3a764000080c001a007f4bf6cdde42fbf8bf2da94b8285521fb160c760413ba92de04fb90af108460a03178961acc860c5e0f29dc9f43d28e684ef195ee286f9c4620f74042135f7eb0" it("should produce the expected transaction") { expect(try? basicSignature?.rawTransaction().bytes.hexString(prefix: true)) == expectedBasicTx } - + it("should be a valid tx") { expect(basicSignature!.verifySignature()) == true } - + // Complicated TX - + let extendedTx = try! EthereumTransaction( nonce: 0, gasPrice: EthereumQuantity(quantity: 21.gwei), @@ -115,12 +115,12 @@ class TransactionTests: QuickSpec { it("should not be nil") { expect(extendedSignature).toNot(beNil()) } - + let expectedExtendedTx = "0x02f8f70380843b9aca008504e3b2920082520894867aeeeed428ed9ba7f97fc7e16f16dfcf02f375880de0b6b3a76400009102f8730180843b9aca008504e3b2920082f872f85994de0b295669a9fd93d5f28d9ec85e40f4cb697baef842a00000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000000007d694bb9bc244d798123fde783fcc1c72d3bb8c189413c080a0e0cd5f5e03d10e3d792fb652f6d1ea470cb6cdf745462980dff1652904cc4ed5a06f8b372427d15b68158597cd547c0f77165563da6a0b954d575920888edaf36c" it("should produce the expected transaction") { expect(try? extendedSignature?.rawTransaction().bytes.hexString(prefix: true)) == expectedExtendedTx } - + it("should be a valid tx") { expect(extendedSignature!.verifySignature()) == true } @@ -130,7 +130,7 @@ class TransactionTests: QuickSpec { hexPrivateKey: "0x94eca03b4541a0eb0d173e321b6f960d08cfe4c5a75fa00ebe0a3d283c609c3a" ) let t = p?.address - + guard let to = t, let privateKey = p else { return } @@ -138,7 +138,7 @@ class TransactionTests: QuickSpec { // Legacy Tx let tx = EthereumTransaction(nonce: 0, gasPrice: EthereumQuantity(quantity: 21.gwei), gasLimit: 21000, to: to, value: EthereumQuantity(quantity: 1.eth)) - + // Sign transaction with private key let newTx = try? tx.sign(with: privateKey, chainId: 3) it("should not be nil") { @@ -150,15 +150,15 @@ class TransactionTests: QuickSpec { let rlpEncodedBasicTx = try? rlpDecoder.decode(rlpEncodedBasicTxBytes!) let expectedSignedBasicTx = try? EthereumSignedTransaction(rlp: rlpEncodedBasicTx!) - + let expectedTransaction = "0xf86c808504e3b2920082520894867aeeeed428ed9ba7f97fc7e16f16dfcf02f375880de0b6b3a76400008029a099060c9146c68716da3a79533866dc941a03b171911d675f518c97a73882f7a6a0019167adb26b602501c954e7793e798407836f524b9778f5be6ebece5fc998c6" - + it("should produce the expected transaction") { expect(try? expectedSignedBasicTx!.rawTransaction().bytes.hexString(prefix: true)) == expectedTransaction } // Modern Tx - + let extendedTx = try! EthereumTransaction( nonce: 0, gasPrice: EthereumQuantity(quantity: 21.gwei), @@ -178,18 +178,18 @@ class TransactionTests: QuickSpec { transactionType: .eip1559 ) let extendedSignature = try? extendedTx.sign(with: privateKey, chainId: 3) - + let rlpEncodedTxBytes = try? rlpEncoder.encode(extendedSignature!.rlp()) let rlpEncodedTx = try? rlpDecoder.decode(rlpEncodedTxBytes!) let expectedSignedTx = try? EthereumSignedTransaction(rlp: rlpEncodedTx!) - + let expectedExtendedTx = "0x02f8f70380843b9aca008504e3b2920082520894867aeeeed428ed9ba7f97fc7e16f16dfcf02f375880de0b6b3a76400009102f8730180843b9aca008504e3b2920082f872f85994de0b295669a9fd93d5f28d9ec85e40f4cb697baef842a00000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000000007d694bb9bc244d798123fde783fcc1c72d3bb8c189413c080a0e0cd5f5e03d10e3d792fb652f6d1ea470cb6cdf745462980dff1652904cc4ed5a06f8b372427d15b68158597cd547c0f77165563da6a0b954d575920888edaf36c" it("should produce the expected transaction") { expect(try? expectedSignedTx!.rawTransaction().bytes.hexString(prefix: true)) == expectedExtendedTx } - + it("should be a valid tx") { expect(expectedSignedTx!.verifySignature()) == true } @@ -207,7 +207,7 @@ class TransactionTests: QuickSpec { hexPrivateKey: "0x94eca03b4541a0eb0d173e321b6f960d08cfe4c5a75fa00ebe0a3d283c609c3a" ) let t = p?.address - + guard let to = t, let privateKey = p else { return } @@ -268,7 +268,7 @@ class TransactionTests: QuickSpec { it("should equal the extendedTx") { expect(extFrom == expectedExtFrom) == true } - + // Legacy tx let rawTx = try? EthereumData(ethereumValue: "0xf8aa41850336f420fc830160429484018071282d4b2996272659d9c01cb08dd7327f80b844a9059cbb00000000000000000000000025b2ad0f7c48390278a39d58efeb94056fc49f1c000000000000000000000000000000000000000000000006e04233f855ff21a025a055b539fae05d8a8a19614e422fe8bc7f3ea9e49d9e613172f05fb7d584adb099a0124ce216d52a345a5293765065c3689d6134c92d42374604836c4cc8336cec42") @@ -282,6 +282,55 @@ class TransactionTests: QuickSpec { } } + + context("Get EthereumTransaction from EthereumSignedTransaction") { + let p = try? EthereumPrivateKey( + hexPrivateKey: "0x94eca03b4541a0eb0d173e321b6f960d08cfe4c5a75fa00ebe0a3d283c609c3a" + ) + let t = p?.address + + guard let to = t, let privateKey = p else { + return + } + + // Legacy tx + + let tx = EthereumTransaction(nonce: 0, gasPrice: EthereumQuantity(quantity: 21.gwei), gasLimit: 21000, to: to, value: EthereumQuantity(quantity: 1.eth)) + let signedTx = try! tx.sign(with: privateKey, chainId: 3) + let originalTx = try! signedTx.unsignedTransaction() + + it("should be equal for legacy tx") { + expect(tx == originalTx) == true + } + + // Modern tx + + let extendedTx = try! EthereumTransaction( + nonce: 0, + gasPrice: EthereumQuantity(quantity: 21.gwei), + maxFeePerGas: EthereumQuantity(quantity: 21.gwei), + maxPriorityFeePerGas: EthereumQuantity(quantity: 1.gwei), + gasLimit: 21000, + to: to, + value: EthereumQuantity(quantity: 1.eth), + data: EthereumData("0x02f8730180843b9aca008504e3b2920082".hexBytes()), + accessList: [ + try! EthereumAddress(hex: "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", eip55: false): [ + EthereumData(ethereumValue: "0x0000000000000000000000000000000000000000000000000000000000000003"), + EthereumData(ethereumValue: "0x0000000000000000000000000000000000000000000000000000000000000007") + ], + try! EthereumAddress(hex: "0xbb9bc244d798123fde783fcc1c72d3bb8c189413", eip55: false): [], + ], + transactionType: .eip1559 + ) + let extendedSignedTx = try! extendedTx.sign(with: privateKey, chainId: 3) + + let recoveredTx = try! extendedSignedTx.unsignedTransaction() + + it("should be equal for modern tx") { + expect(extendedTx == recoveredTx) == true + } + } } } }