diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 638d492c..78b75db0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -56,9 +56,10 @@ jobs: if: ${{ matrix.os == 'macos-latest' }} run: xcrun llvm-cov export -format="lcov" .build/debug/Web3PackageTests.xctest/Contents/MacOS/Web3PackageTests -instr-profile .build/debug/codecov/default.profdata > info.lcov - name: Upload Test Coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 if: ${{ matrix.os == 'macos-latest' }} with: files: ./info.lcov fail_ci_if_error: true verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Sources/Core/Transaction/EthereumTransaction.swift b/Sources/Core/Transaction/EthereumTransaction.swift index 37873771..5aa139dd 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 @@ -365,54 +390,30 @@ public struct EthereumSignedTransaction { recId = v.quantity } } - let rlp = RLPItem( - nonce: nonce, - gasPrice: gasPrice, - gasLimit: gasLimit, - to: to, - value: value, - data: data, - v: chainId, - r: 0, - s: 0 - ) - if let _ = try? EthereumPublicKey(message: RLPEncoder().encode(rlp), v: EthereumQuantity(quantity: recId), r: r, s: s) { - return true + do { + let messageToSign = try self.unsignedTransaction().messageToSign(chainId: self.chainId) + if let _ = try? EthereumPublicKey(message: messageToSign, v: EthereumQuantity(quantity: recId), r: r, s: s) { + return true + } + } catch { + return false } return false } private func verifyEip1559Signature() -> Bool { - let rlp = RLPItem( - nonce: nonce, - gasPrice: gasPrice, - maxFeePerGas: maxFeePerGas, - maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: gasLimit, - to: to, - value: value, - data: data, - v: 0, - r: 0, - s: 0, - chainId: chainId, - accessList: accessList, - transactionType: transactionType - ) - var messageToSign = Bytes() - messageToSign.append(0x02) do { - try messageToSign.append(contentsOf: RLPEncoder().encode(rlp)) + let messageToSign = try self.unsignedTransaction().messageToSign(chainId: self.chainId) + + if let _ = try? EthereumPublicKey(message: messageToSign, v: v, r: r, s: s) { + return true + } + + return false } catch { return false } - - if let _ = try? EthereumPublicKey(message: messageToSign, v: v, r: r, s: s) { - return true - } - - return false } // MARK: - Errors @@ -437,7 +438,7 @@ extension EthereumSignedTransaction { rawTxBytes.removeFirst() } do { - var rlp = try RLPDecoder().decode(rawTxBytes) + let rlp = try RLPDecoder().decode(rawTxBytes) try self.init(rlp: rlp) } catch { @@ -712,3 +713,41 @@ extension EthereumSignedTransaction: Hashable { hasher.combine(transactionType) } } + +extension EthereumSignedTransaction { + + public func from() throws -> EthereumAddress { + return try publicKey().address + } + + public func publicKey() throws -> EthereumPublicKey { + 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) + } else { + if v.quantity >= 27 { + recId = v.quantity - 27 + } else { + recId = v.quantity + } + } + return try EthereumPublicKey(message: messageToSign, v: EthereumQuantity(quantity: recId), r: self.r, s: self.s) + } + + 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 e5b9c4ca..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 } @@ -255,6 +255,82 @@ class TransactionTests: QuickSpec { expect(extendedSignature == signedExtTx) == true } } + context("Recover sender of Tx") { + + // Modern tx + + let rawExtTx = try? EthereumData(ethereumValue: "0x02f8b40183879f6a84773594008517bfac7c0083032918943845badade8e6dff049820680d1f14bd3903a5d080b844a9059cbb000000000000000000000000566586bba243e683256cd4bef168813e42df9d6400000000000000000000000000000000000000000000003147d6c40e7e078000c001a08753897821b034540cb14d2aa0dafa880c9017831d633ff9da498cf0369d061ca016ad20c8c2aa12edcb8ab7fd8ff0ff980d65fe42d5859a8688ee92da36b479d6") + + let signedExtTx = try! EthereumSignedTransaction(rawTx: rawExtTx!) + let extFrom = try! signedExtTx.from() + let expectedExtFrom = try! EthereumAddress(hex: "0xDFd5293D8e347dFe59E90eFd55b2956a1343963d", eip55: true) + + it("should equal the extendedTx") { + expect(extFrom == expectedExtFrom) == true + } + + // Legacy tx + + let rawTx = try? EthereumData(ethereumValue: "0xf8aa41850336f420fc830160429484018071282d4b2996272659d9c01cb08dd7327f80b844a9059cbb00000000000000000000000025b2ad0f7c48390278a39d58efeb94056fc49f1c000000000000000000000000000000000000000000000006e04233f855ff21a025a055b539fae05d8a8a19614e422fe8bc7f3ea9e49d9e613172f05fb7d584adb099a0124ce216d52a345a5293765065c3689d6134c92d42374604836c4cc8336cec42") + + let legacyTx = try! EthereumSignedTransaction(rawTx: rawTx!) + let legacyFrom = try! legacyTx.from() + let expectedLegacyFrom = try! EthereumAddress(hex: "0x096C84037baA375749479b8bE002A11d11aa9a5a", eip55: true) + + it("should be equal for legacy tx") { + expect(legacyFrom == expectedLegacyFrom) == true + } + + } + + 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 + } + } } } }