From 6be89c9b0dc46bcafeb57fc68c333148f06df9e0 Mon Sep 17 00:00:00 2001 From: b00f Date: Fri, 8 Nov 2024 01:03:34 +0800 Subject: [PATCH 1/6] [Pactus]: Support Pactus Blockchain (#4057) * feat: support Pactus * fix: fix compile_impl implementation * update Pactus address tests * encoding treasury addresss * update tests for HD wallet * define Decodable trait and implement decode * encode and decode for amount and address * decode transaction and calculate id * test(pactus): add compiler and signer tests * add C++ integration tests * extend central derivation and validation tests * revert changes on non-pactus files * fix compile error * update tests for Kotlin and Swift * use H160 for PubKeyHash * update VarInt test * fix: resolve PR review comments * fix compile error * update C++ tests * fix formatting issue * update Java and Swift tests * update swift test * fix broken swift test * fix: add successfully transfer broadcasted in mainnet * add bls validator public key * add bond payload encodable and decodable * add generic for decode_fix_slice * add bond payload for sign transaction * add bond payload as public use * add pub use validator public key * add original test case for bond and transfer * refactor transfer in test compile * add bond sign test * refactor transfer sign payload * refactor change to bond module name * fix rust tests * add transfer and bond test for swift * add test sign bond without public key * use broadcasted transaction data for tests * fix mistake on Android test --------- Co-authored-by: Javad --- .../blockchains/CoinAddressDerivationTests.kt | 1 + .../blockchains/pactus/TestPactusAddress.kt | 29 ++ .../blockchains/pactus/TestPactusSigner.kt | 149 ++++++++ docs/registry.md | 1 + include/TrustWalletCore/TWBlockchain.h | 1 + include/TrustWalletCore/TWCoinType.h | 1 + .../core/test/CoinAddressDerivationTests.kt | 1 + registry.json | 27 ++ rust/Cargo.lock | 15 + rust/Cargo.toml | 1 + rust/chains/tw_pactus/Cargo.toml | 18 + rust/chains/tw_pactus/src/compiler.rs | 84 +++++ rust/chains/tw_pactus/src/encoder/decode.rs | 138 ++++++++ rust/chains/tw_pactus/src/encoder/encode.rs | 171 +++++++++ rust/chains/tw_pactus/src/encoder/error.rs | 20 ++ rust/chains/tw_pactus/src/encoder/mod.rs | 39 ++ rust/chains/tw_pactus/src/encoder/var_int.rs | 198 +++++++++++ rust/chains/tw_pactus/src/entry.rs | 101 ++++++ rust/chains/tw_pactus/src/lib.rs | 11 + rust/chains/tw_pactus/src/modules/mod.rs | 6 + .../tw_pactus/src/modules/transaction_util.rs | 29 ++ .../tw_pactus/src/modules/tx_builder.rs | 55 +++ rust/chains/tw_pactus/src/signer.rs | 44 +++ rust/chains/tw_pactus/src/transaction/mod.rs | 274 ++++++++++++++ .../tw_pactus/src/transaction/payload/bond.rs | 100 ++++++ .../tw_pactus/src/transaction/payload/mod.rs | 60 ++++ .../src/transaction/payload/transfer.rs | 64 ++++ rust/chains/tw_pactus/src/types/address.rs | 335 ++++++++++++++++++ rust/chains/tw_pactus/src/types/amount.rs | 20 ++ rust/chains/tw_pactus/src/types/mod.rs | 7 + .../src/types/validator_public_key.rs | 105 ++++++ .../tw_utxo/src/encode/compact_integer.rs | 15 +- rust/tw_coin_registry/Cargo.toml | 1 + rust/tw_coin_registry/src/blockchain_type.rs | 1 + rust/tw_coin_registry/src/dispatcher.rs | 3 + rust/tw_tests/tests/chains/mod.rs | 1 + rust/tw_tests/tests/chains/pactus/mod.rs | 9 + .../tests/chains/pactus/pactus_address.rs | 96 +++++ .../tests/chains/pactus/pactus_compile.rs | 78 ++++ .../tests/chains/pactus/pactus_sign.rs | 39 ++ .../chains/pactus/pactus_transaction_util.rs | 14 + .../tests/chains/pactus/test_cases.rs | 206 +++++++++++ .../tests/coin_address_derivation_test.rs | 1 + src/Coin.cpp | 3 + src/Pactus/Entry.h | 17 + src/proto/Pactus.proto | 66 ++++ swift/Tests/Blockchains/PactusTests.swift | 129 +++++++ swift/Tests/CoinAddressDerivationTests.swift | 5 +- tests/chains/Pactus/AddressTests.cpp | 39 ++ tests/chains/Pactus/CoinTypeTests.cpp | 38 ++ tests/chains/Pactus/CompilerTests.cpp | 49 +++ tests/chains/Pactus/SignerTests.cpp | 33 ++ tests/chains/Pactus/TestCases.h | 150 ++++++++ tests/chains/Pactus/WalletTests.cpp | 44 +++ tests/common/CoinAddressDerivationTests.cpp | 3 + tests/interface/TWHRPTests.cpp | 3 + 56 files changed, 3142 insertions(+), 6 deletions(-) create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt create mode 100644 rust/chains/tw_pactus/Cargo.toml create mode 100644 rust/chains/tw_pactus/src/compiler.rs create mode 100644 rust/chains/tw_pactus/src/encoder/decode.rs create mode 100644 rust/chains/tw_pactus/src/encoder/encode.rs create mode 100644 rust/chains/tw_pactus/src/encoder/error.rs create mode 100644 rust/chains/tw_pactus/src/encoder/mod.rs create mode 100644 rust/chains/tw_pactus/src/encoder/var_int.rs create mode 100644 rust/chains/tw_pactus/src/entry.rs create mode 100644 rust/chains/tw_pactus/src/lib.rs create mode 100644 rust/chains/tw_pactus/src/modules/mod.rs create mode 100644 rust/chains/tw_pactus/src/modules/transaction_util.rs create mode 100644 rust/chains/tw_pactus/src/modules/tx_builder.rs create mode 100644 rust/chains/tw_pactus/src/signer.rs create mode 100644 rust/chains/tw_pactus/src/transaction/mod.rs create mode 100644 rust/chains/tw_pactus/src/transaction/payload/bond.rs create mode 100644 rust/chains/tw_pactus/src/transaction/payload/mod.rs create mode 100644 rust/chains/tw_pactus/src/transaction/payload/transfer.rs create mode 100644 rust/chains/tw_pactus/src/types/address.rs create mode 100644 rust/chains/tw_pactus/src/types/amount.rs create mode 100644 rust/chains/tw_pactus/src/types/mod.rs create mode 100644 rust/chains/tw_pactus/src/types/validator_public_key.rs create mode 100644 rust/tw_tests/tests/chains/pactus/mod.rs create mode 100644 rust/tw_tests/tests/chains/pactus/pactus_address.rs create mode 100644 rust/tw_tests/tests/chains/pactus/pactus_compile.rs create mode 100644 rust/tw_tests/tests/chains/pactus/pactus_sign.rs create mode 100644 rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs create mode 100644 rust/tw_tests/tests/chains/pactus/test_cases.rs create mode 100644 src/Pactus/Entry.h create mode 100644 src/proto/Pactus.proto create mode 100644 swift/Tests/Blockchains/PactusTests.swift create mode 100644 tests/chains/Pactus/AddressTests.cpp create mode 100644 tests/chains/Pactus/CoinTypeTests.cpp create mode 100644 tests/chains/Pactus/CompilerTests.cpp create mode 100644 tests/chains/Pactus/SignerTests.cpp create mode 100644 tests/chains/Pactus/TestCases.h create mode 100644 tests/chains/Pactus/WalletTests.cpp diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt index 9064a873e7c..f99db4e987c 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt @@ -154,5 +154,6 @@ class CoinAddressDerivationTests { TIA -> assertEquals("celestia142j9u5eaduzd7faumygud6ruhdwme98qpwmfv7", address) NATIVEZETACHAIN -> assertEquals("zeta13u6g7vqgw074mgmf2ze2cadzvkz9snlwywj304", address) DYDX -> assertEquals("dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky", address) + PACTUS -> assertEquals("pc1r7ys2g5a4xc2qtm0t4q987m4mvs57w5g0v4pvzg", address) } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt new file mode 100644 index 00000000000..b58b068b859 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.pactus + +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.* + +class TestPactusAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + val key = PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6".toHexByteArray()) + val pubkey = key.publicKeyEd25519 + val address = AnyAddress(pubkey, CoinType.PACTUS) + val expected = AnyAddress("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr", CoinType.PACTUS) + + assertEquals(pubkey.data().toHex(), "0x95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa") + assertEquals(address.description(), expected.description()) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt new file mode 100644 index 00000000000..81bbe9bf532 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.pactus + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.PrivateKey +import wallet.core.java.AnySigner +import wallet.core.jni.CoinType +import wallet.core.jni.CoinType.PACTUS +import wallet.core.jni.proto.Pactus +import wallet.core.jni.proto.Pactus.SigningOutput +import com.trustwallet.core.app.utils.Numeric +import org.junit.Assert.assertArrayEquals + +class TestPactusSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testPactusTransferSigning() { + // Successfully broadcasted transaction: + // https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f + // + val signingInput = Pactus.SigningInput.newBuilder() + signingInput.apply { + privateKey = ByteString.copyFrom( + PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6" + .toHexByteArray()).data() + ) + transaction = Pactus.TransactionMessage.newBuilder().apply { + lockTime = 2335524 + fee = 10000000 + memo = "wallet-core" + transfer = Pactus.TransferPayload.newBuilder().apply { + sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr" + receiver = "pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra" + amount = 200000000 + }.build() + }.build() + } + + val output = AnySigner.sign(signingInput.build(), PACTUS, SigningOutput.parser()) + + assertEquals( + "0x1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f", + Numeric.toHexString(output.transactionId.toByteArray()) + ) + + assertEquals( + "0x4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506", + Numeric.toHexString(output.signature.toByteArray()) + ) + + assertEquals( + "0x000124a3230080ade2040b77616c6c65742d636f726501037098338e0b6808119dfd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df218084af5f4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda850695794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", + Numeric.toHexString(output.signedTransactionData.toByteArray()) + ) + } + + @Test + fun testPactusBondWithPublicKeySigning() { + // Successfully broadcasted transaction: + // https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f + // + val signingInput = Pactus.SigningInput.newBuilder() + signingInput.apply { + privateKey = ByteString.copyFrom( + PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6" + .toHexByteArray()).data() + ) + transaction = Pactus.TransactionMessage.newBuilder().apply { + lockTime = 2339009 + fee = 10000000 + memo = "wallet-core" + bond = Pactus.BondPayload.newBuilder().apply { + sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr" + receiver = "pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl" + stake = 1000000000 + publicKey = "public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn" + }.build() + }.build() + } + + val output = AnySigner.sign(signingInput.build(), PACTUS, SigningOutput.parser()) + + assertEquals( + "0xd194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f", + Numeric.toHexString(output.transactionId.toByteArray()) + ) + + assertEquals( + "0x0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300", + Numeric.toHexString(output.signature.toByteArray()) + ) + + assertEquals( + "0x0001c1b0230080ade2040b77616c6c65742d636f726502037098338e0b6808119dfd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f451823d6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a39355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a56bbcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13833b8094ebdc030d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce700630095794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", + Numeric.toHexString(output.signedTransactionData.toByteArray()) + ) + } + + @Test + fun testPactusBondWithoutPublicKeySigning() { + // Successfully broadcasted transaction: + // https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80 + // + val signingInput = Pactus.SigningInput.newBuilder() + signingInput.apply { + privateKey = ByteString.copyFrom( + PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6" + .toHexByteArray()).data() + ) + transaction = Pactus.TransactionMessage.newBuilder().apply { + lockTime = 2335580 + fee = 10000000 + memo = "wallet-core" + bond = Pactus.BondPayload.newBuilder().apply { + sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr" + receiver = "pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd" + stake = 1000000000 + }.build() + }.build() + } + + val output = AnySigner.sign(signingInput.build(), PACTUS, SigningOutput.parser()) + + assertEquals( + "0xf83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80", + Numeric.toHexString(output.transactionId.toByteArray()) + ) + + assertEquals( + "0x9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d", + Numeric.toHexString(output.signature.toByteArray()) + ) + + assertEquals( + "0x00015ca3230080ade2040b77616c6c65742d636f726502037098338e0b6808119dfd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278be008094ebdc039e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", + Numeric.toHexString(output.signedTransactionData.toByteArray()) + ) + } +} diff --git a/docs/registry.md b/docs/registry.md index 7606e14a45b..a5045889d19 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -99,6 +99,7 @@ This list is generated from [./registry.json](../registry.json) | 14001 | WAX | WAXP | | | | 18000 | Meter | MTR | | | | 19167 | Flux | FLUX | | | +| 21888 | Pactus | PAC | | | | 52752 | Celo | CELO | | | | 59144 | Linea | ETH | | | | 81457 | Blast | ETH | | | diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index 84cac17e3b4..eeb2f744474 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -66,6 +66,7 @@ enum TWBlockchain { TWBlockchainNativeEvmos = 53, // Cosmos TWBlockchainNativeInjective = 54, // Cosmos TWBlockchainBitcoinCash = 55, + TWBlockchainPactus = 56, }; TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h index 388bd270334..9d2f10e36c9 100644 --- a/include/TrustWalletCore/TWCoinType.h +++ b/include/TrustWalletCore/TWCoinType.h @@ -186,6 +186,7 @@ enum TWCoinType { TWCoinTypeBlast = 81457, TWCoinTypeBounceBit = 6001, TWCoinTypeZkLinkNova = 810180, + TWCoinTypePactus = 21888, // end_of_tw_coin_type_marker_do_not_modify }; diff --git a/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt b/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt index c3d3dbf0527..959fd4821eb 100644 --- a/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt +++ b/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt @@ -147,5 +147,6 @@ class CoinAddressDerivationTests { Tia -> "celestia142j9u5eaduzd7faumygud6ruhdwme98qpwmfv7" NativeZetaChain -> "zeta13u6g7vqgw074mgmf2ze2cadzvkz9snlwywj304" Dydx -> "dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky" + Pactus -> "pc1r7ys2g5a4xc2qtm0t4q987m4mvs57w5g0v4pvzg" } } diff --git a/registry.json b/registry.json index abaf52b05bd..d88798aff9c 100644 --- a/registry.json +++ b/registry.json @@ -4787,5 +4787,32 @@ "rpc": "https://rpc.zklink.io", "documentation": "https://docs.zklink.io" } + }, + { + "id": "pactus", + "name": "Pactus", + "coinId": 21888, + "symbol": "PAC", + "decimals": 9, + "blockchain": "Pactus", + "derivation": [ + { + "path": "m/44'/21888'/3'/0'" + } + ], + "curve": "ed25519", + "publicKeyType": "ed25519", + "hrp": "pc", + "explorer": { + "url": "https://pacviewer.com", + "txPath": "/transaction/", + "accountPath": "/address/" + }, + "info": { + "url": "https://pactus.org", + "source": "https://github.com/pactus-project/pactus", + "rpc": "https://docs.pactus.org/api/http", + "documentation": "https://docs.pactus.org" + } } ] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index dcb26f709ab..e12cf3ea375 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1857,6 +1857,7 @@ dependencies = [ "tw_misc", "tw_native_evmos", "tw_native_injective", + "tw_pactus", "tw_ronin", "tw_solana", "tw_sui", @@ -2078,6 +2079,20 @@ dependencies = [ "tw_memory", ] +[[package]] +name = "tw_pactus" +version = "0.1.0" +dependencies = [ + "bech32", + "byteorder", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + [[package]] name = "tw_proto" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f6bd5522ea7..692beec2692 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,6 +10,7 @@ members = [ "chains/tw_internet_computer", "chains/tw_native_evmos", "chains/tw_native_injective", + "chains/tw_pactus", "chains/tw_ronin", "chains/tw_solana", "chains/tw_sui", diff --git a/rust/chains/tw_pactus/Cargo.toml b/rust/chains/tw_pactus/Cargo.toml new file mode 100644 index 00000000000..105b0aa7679 --- /dev/null +++ b/rust/chains/tw_pactus/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tw_pactus" +version = "0.1.0" +edition = "2021" + +[dependencies] +bech32 = "0.9.1" +byteorder = "1.4" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } +tw_hash = { path = "../../tw_hash" } +tw_encoding = { path = "../../tw_encoding" } + +[dev-dependencies] +tw_encoding = { path = "../../tw_encoding" } + diff --git a/rust/chains/tw_pactus/src/compiler.rs b/rust/chains/tw_pactus/src/compiler.rs new file mode 100644 index 00000000000..3433b77d6e2 --- /dev/null +++ b/rust/chains/tw_pactus/src/compiler.rs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ed25519; +use tw_proto::Pactus::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +use crate::modules::tx_builder::TxBuilder; + +pub struct PactusCompiler; + +impl PactusCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let trx = TxBuilder::from_proto(&input)?; + let sign_bytes = trx.sign_bytes()?; + + let output = CompilerProto::PreSigningOutput { + data_hash: trx.id().into(), + data: sign_bytes.into(), + ..CompilerProto::PreSigningOutput::default() + }; + + Ok(output) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult> { + let signature_bytes = signatures + .first() + .or_tw_err(SigningErrorType::Error_signatures_count)?; + let public_key_bytes = public_keys + .first() + .or_tw_err(SigningErrorType::Error_signatures_count)?; + + let public_key = ed25519::sha512::PublicKey::try_from(public_key_bytes.as_slice())?; + let signature = ed25519::Signature::try_from(signature_bytes.as_slice())?; + + let mut trx = TxBuilder::from_proto(&input)?; + trx.set_signatory(public_key.to_owned(), signature.to_owned()); + + let data = trx.to_bytes()?; + + let output = Proto::SigningOutput { + transaction_id: trx.id().into(), + signed_transaction_data: data.into(), + signature: signature.to_bytes().to_vec().into(), + ..Proto::SigningOutput::default() + }; + + Ok(output) + } +} diff --git a/rust/chains/tw_pactus/src/encoder/decode.rs b/rust/chains/tw_pactus/src/encoder/decode.rs new file mode 100644 index 00000000000..f9e2ef80edf --- /dev/null +++ b/rust/chains/tw_pactus/src/encoder/decode.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::Hash; +use tw_keypair::ed25519::{sha512::PublicKey, Signature}; + +use super::error::Error; +use crate::encoder::var_int::VarInt; +use crate::encoder::Decodable; + +pub(crate) fn decode_var_slice(r: &mut dyn std::io::Read) -> Result, Error> { + let len = *VarInt::decode(r)?; + let mut buf = vec![0; len as usize]; + r.read_exact(&mut buf)?; + + Ok(buf) +} + +pub(crate) fn decode_fix_slice( + r: &mut dyn std::io::Read, +) -> Result<[u8; N], Error> { + let mut buf: [u8; N] = [0; N]; + r.read_exact(&mut buf)?; + + Ok(buf) +} + +impl Decodable for Vec { + fn decode(r: &mut dyn std::io::Read) -> Result { + decode_var_slice(r) + } +} + +impl Decodable for String { + fn decode(r: &mut dyn std::io::Read) -> Result { + let data = decode_var_slice(r)?; + String::from_utf8(data).map_err(|_| self::Error::ParseFailed("Invalid String")) + } +} + +impl Decodable for PublicKey { + fn decode(r: &mut dyn std::io::Read) -> Result { + let data = decode_fix_slice::<{ PublicKey::LEN }>(r)?; + PublicKey::try_from(data.as_slice()) + .map_err(|_| self::Error::ParseFailed("Invalid Public Key")) + } +} + +impl Decodable for Signature { + fn decode(r: &mut dyn std::io::Read) -> Result { + let data = decode_fix_slice::<{ Signature::LEN }>(r)?; + Signature::try_from(data.as_slice()) + .map_err(|_| self::Error::ParseFailed("Invalid Signature")) + } +} + +impl Decodable for Hash { + fn decode(r: &mut dyn std::io::Read) -> Result { + let data = decode_fix_slice::(r)?; + Hash::try_from(data.as_slice()).map_err(|_| self::Error::ParseFailed("Invalid Hash")) + } +} + +macro_rules! impl_decodable_for_int { + ($int:ty, $size:literal) => { + impl Decodable for $int { + #[inline] + fn decode(r: &mut dyn std::io::Read) -> Result { + let mut buf = [0; $size]; + r.read_exact(&mut buf[..])?; + Ok(<$int>::from_le_bytes(buf)) + } + } + }; +} + +impl_decodable_for_int!(u8, 1); +impl_decodable_for_int!(i32, 4); +impl_decodable_for_int!(i64, 8); +impl_decodable_for_int!(u16, 2); +impl_decodable_for_int!(u32, 4); +impl_decodable_for_int!(u64, 8); + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use tw_encoding::hex::DecodeHex; + + use super::*; + use crate::encoder::deserialize; + + #[test] + fn test_decode_var_slice() { + let expected = vec![1, 2, 3, 4]; + let mut cursor = Cursor::new("0401020304".decode_hex().unwrap()); + let slice = decode_var_slice(&mut cursor).unwrap(); + + assert_eq!(expected, slice); + } + + #[test] + fn test_decode_fix_slice() { + let expected = vec![1, 2, 3, 4]; + let mut cursor = Cursor::new("01020304".decode_hex().unwrap()); + let slice = decode_fix_slice::<4>(&mut cursor).unwrap(); + + assert_eq!(expected, slice); + } + + #[test] + fn test_encode_numbers() { + let data = vec![1_u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0]; + let mut cursor = Cursor::new(data); + + assert_eq!(1u8, u8::decode(&mut cursor).unwrap()); + assert_eq!(2u16, u16::decode(&mut cursor).unwrap()); + assert_eq!(3u32, u32::decode(&mut cursor).unwrap()); + assert_eq!(4u64, u64::decode(&mut cursor).unwrap()); + } + + #[test] + fn test_decode_bytes() { + let expected = "0145".decode_hex().unwrap(); + let bytes = "020145".decode_hex().unwrap(); + + assert_eq!(expected, deserialize::>(&bytes).unwrap()); + } + + #[test] + fn test_encode_string() { + let expected = "hello".to_string(); + let bytes = "0568656c6c6f056844656c6c6e".decode_hex().unwrap(); + + assert_eq!(expected, deserialize::(&bytes).unwrap()); + } +} diff --git a/rust/chains/tw_pactus/src/encoder/encode.rs b/rust/chains/tw_pactus/src/encoder/encode.rs new file mode 100644 index 00000000000..9840c1873a8 --- /dev/null +++ b/rust/chains/tw_pactus/src/encoder/encode.rs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use byteorder::{LittleEndian, WriteBytesExt}; +use tw_hash::Hash; +use tw_keypair::ed25519::{sha512::PublicKey, Signature}; + +use super::error::Error; +use crate::encoder::var_int::VarInt; +use crate::encoder::Encodable; + +pub(crate) fn encode_var_slice(data: &[u8], w: &mut dyn std::io::Write) -> Result<(), Error> { + VarInt::from(data.len()).encode(w)?; + w.write_all(data)?; + + Ok(()) +} + +pub(crate) fn encode_fix_slice(data: &[u8], w: &mut dyn std::io::Write) -> Result<(), Error> { + w.write_all(data)?; + + Ok(()) +} + +impl Encodable for Vec { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + encode_var_slice(self, w) + } + + fn encoded_size(&self) -> usize { + VarInt::from(self.len()).encoded_size() + self.len() + } +} + +impl Encodable for String { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + encode_var_slice(self.as_bytes(), w) + } + + fn encoded_size(&self) -> usize { + VarInt::from(self.len()).encoded_size() + self.len() + } +} + +impl Encodable for PublicKey { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + encode_fix_slice(self.as_slice(), w) + } + + fn encoded_size(&self) -> usize { + PublicKey::LEN + } +} + +impl Encodable for Signature { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + encode_fix_slice(self.to_bytes().as_slice(), w) + } + + fn encoded_size(&self) -> usize { + Signature::LEN + } +} + +impl Encodable for Hash { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + encode_fix_slice(self.as_slice(), w) + } + + fn encoded_size(&self) -> usize { + N + } +} + +impl Encodable for u8 { + #[inline] + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + w.write_u8(*self)?; + + Ok(()) + } + + #[inline] + fn encoded_size(&self) -> usize { + 1 + } +} + +macro_rules! impl_encodable_for_int { + ($int:ty, $size:literal, $write_fn:tt) => { + impl Encodable for $int { + #[inline] + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + w.$write_fn::(*self)?; + + Ok(()) + } + + #[inline] + fn encoded_size(&self) -> usize { + $size + } + } + }; +} + +impl_encodable_for_int!(i32, 4, write_i32); +impl_encodable_for_int!(i64, 8, write_i64); +impl_encodable_for_int!(u16, 2, write_u16); +impl_encodable_for_int!(u32, 4, write_u32); +impl_encodable_for_int!(u64, 8, write_u64); + +#[cfg(test)] +mod tests { + use tw_encoding::hex::DecodeHex; + + use super::*; + use crate::encoder::serialize; + + #[test] + fn test_encode_var_slice() { + let expected = "0401020304".decode_hex().unwrap(); + let slice = vec![1, 2, 3, 4]; + let mut w = Vec::new(); + encode_var_slice(&slice, &mut w).unwrap(); + + assert_eq!(expected, w.to_vec()); + } + + #[test] + fn test_encode_fix_slice() { + let expected = "01020304".decode_hex().unwrap(); + let slice = vec![1, 2, 3, 4]; + let mut w = Vec::new(); + encode_fix_slice(&slice, &mut w).unwrap(); + + assert_eq!(expected, w.to_vec()); + } + + #[test] + fn test_encode_numbers() { + let mut w = Vec::new(); + + 1u8.encode(&mut w).unwrap(); + 2u16.encode(&mut w).unwrap(); + 3u32.encode(&mut w).unwrap(); + 4u64.encode(&mut w).unwrap(); + + let expected = vec![1_u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(w.to_vec(), expected); + } + + #[test] + fn test_encode_bytes() { + let expected = "020145".decode_hex().unwrap(); + let bytes = "0145".decode_hex().unwrap(); + + assert_eq!(expected, serialize(&bytes).unwrap()); + assert_eq!(expected.len(), bytes.encoded_size()); + } + + #[test] + fn test_encode_string() { + let expected = "0568656c6c6f".decode_hex().unwrap(); + let msg = "hello".to_string(); + + assert_eq!(expected, serialize(&msg).unwrap()); + assert_eq!(expected.len(), msg.encoded_size()); + } +} diff --git a/rust/chains/tw_pactus/src/encoder/error.rs b/rust/chains/tw_pactus/src/encoder/error.rs new file mode 100644 index 00000000000..b3643854a90 --- /dev/null +++ b/rust/chains/tw_pactus/src/encoder/error.rs @@ -0,0 +1,20 @@ +use tw_coin_entry::error::prelude::{SigningError, SigningErrorType}; + +/// Errors encountered when encoding or decoding data. +#[derive(Debug)] +pub enum Error { + IoError(std::io::Error), + ParseFailed(&'static str), +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::IoError(err) + } +} + +impl From for SigningError { + fn from(_: Error) -> Self { + SigningError::new(SigningErrorType::Error_input_parse) + } +} diff --git a/rust/chains/tw_pactus/src/encoder/mod.rs b/rust/chains/tw_pactus/src/encoder/mod.rs new file mode 100644 index 00000000000..506c3baec67 --- /dev/null +++ b/rust/chains/tw_pactus/src/encoder/mod.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::io::Cursor; + +use error::Error; + +pub mod decode; +pub mod encode; +pub mod error; +pub mod var_int; + +pub fn serialize(t: &T) -> Result, Error> { + let mut writer = Vec::with_capacity(t.encoded_size()); + t.encode(&mut writer)?; + + Ok(writer.to_vec()) +} + +pub fn deserialize(data: &[u8]) -> Result { + let mut cursor = Cursor::new(data); + T::decode(&mut cursor) +} + +/// Trait for encoding an object into a consistent byte sequence. +pub trait Encodable { + /// Encode the object in consistent and deterministic way. + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error>; + + /// Determine the size of serialized object. + fn encoded_size(&self) -> usize; +} + +/// Trait for decoding an object from a byte sequence. +pub trait Decodable: Sized { + /// Decode the object in consistent and deterministic way. + fn decode(r: &mut dyn std::io::Read) -> Result; +} diff --git a/rust/chains/tw_pactus/src/encoder/var_int.rs b/rust/chains/tw_pactus/src/encoder/var_int.rs new file mode 100644 index 00000000000..aba98f95f4f --- /dev/null +++ b/rust/chains/tw_pactus/src/encoder/var_int.rs @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::ops::Deref; + +use byteorder::ReadBytesExt; + +use super::{error::Error, Decodable}; +use crate::encoder::Encodable; + +/// A type of variable-length integer used in the Pactus blockchain to serialize a variable-length integer. +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct VarInt(u64); + +impl From for VarInt { + fn from(value: usize) -> Self { + VarInt(value as u64) + } +} + +impl Deref for VarInt { + type Target = u64; + + fn deref(&self) -> &u64 { + &self.0 + } +} + +impl Encodable for VarInt { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + let mut val = self.0; + // Make sure that there is one after this + while val >= 0x80 { + let n = (val as u8 & 0x7f) | 0x80; + w.write_all(&[n])?; + val >>= 7; // It should be in multiples of 7, this should just get the next part + } + + w.write_all(&[val as u8])?; + + Ok(()) + } + + fn encoded_size(&self) -> usize { + match self.0 { + val if val >= 0x8000000000000000 => 10, + val if val >= 0x100000000000000 => 9, + val if val >= 0x2000000000000 => 8, + val if val >= 0x40000000000 => 7, + val if val >= 0x800000000 => 6, + val if val >= 0x10000000 => 5, + val if val >= 0x200000 => 4, + val if val >= 0x4000 => 3, + val if val >= 0x80 => 2, + _ => 1, + } + } +} + +impl Decodable for VarInt { + fn decode(r: &mut dyn std::io::Read) -> Result { + let mut res: Vec = vec![]; + loop { + let n = r.read_u8()?; + // Zero in any position other than the first is invalid + // since it is not the shortest encoding. + if n == 0 && !res.is_empty() { + return Err(Error::ParseFailed("VarInt has a zero in a position other than the first. This is not the shortest encoding.")); + } + res.push(n & 0b0111_1111); + if n & 0b1000_0000 == 0 { + break; + } + } + let mut int = 0u64; + res.reverse(); + let (last, arr) = res.split_last().unwrap(); + for bits in arr { + int |= *bits as u64; + int = if int.leading_zeros() >= 7 { + int << 7 + } else { + return Err(Error::ParseFailed("VarInt overflows u64")); + }; + } + int |= *last as u64; + Ok(VarInt(int)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::encoder::deserialize; + + #[test] + fn test_var_int_encode_data() { + let mut w = Vec::new(); + + VarInt::from(0x00_usize).encode(&mut w).unwrap(); + VarInt::from(0xfc_usize).encode(&mut w).unwrap(); + VarInt::from(0xfd_usize).encode(&mut w).unwrap(); + VarInt::from(0xffff_usize).encode(&mut w).unwrap(); + VarInt::from(0x01_0000_usize).encode(&mut w).unwrap(); + VarInt::from(0xffff_ffff_usize).encode(&mut w).unwrap(); + VarInt(0x01_0000_0000_u64).encode(&mut w).unwrap(); + + let expected = vec![ + 0x00, // 0x00 + 0xfc, 0x01, // 0xfc + 0xfd, 0x01, // 0xfd + 0xff, 0xff, 0x03, // 0xffff + 0x80, 0x80, 0x04, // 0x01_0000 + 0xff, 0xff, 0xff, 0xff, 0x0f, // 0xffff_ffff + 0x80, 0x80, 0x80, 0x80, 0x10, // 0x01_0000_0000 + ]; + + assert_eq!(w.to_vec(), expected); + } + + // Define the common test cases as a constant + const VARINT_TEST_CASES: &[(u64, &[u8])] = &[ + (0x0u64, &[0x00u8]), + (0xffu64, &[0xff, 0x01]), + (0x7fffu64, &[0xff, 0xff, 0x01]), + (0x3fffffu64, &[0xff, 0xff, 0xff, 0x01]), + (0x1fffffffu64, &[0xff, 0xff, 0xff, 0xff, 0x01]), + (0xfffffffffu64, &[0xff, 0xff, 0xff, 0xff, 0xff, 0x01]), + ( + 0x7ffffffffffu64, + &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], + ), + ( + 0x3ffffffffffffu64, + &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], + ), + ( + 0x1ffffffffffffffu64, + &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], + ), + ( + 0xffffffffffffffffu64, + &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], + ), + (0x200u64, &[0x80, 0x04]), + (0x027fu64, &[0xff, 0x04]), + (0xff00000000u64, &[0x80, 0x80, 0x80, 0x80, 0xf0, 0x1f]), + (0xffffffffu64, &[0xff, 0xff, 0xff, 0xff, 0x0f]), + (0x100000000u64, &[0x80, 0x80, 0x80, 0x80, 0x10]), + (0x7ffffffffu64, &[0xff, 0xff, 0xff, 0xff, 0x7f]), + (0x800000000u64, &[0x80, 0x80, 0x80, 0x80, 0x80, 0x01]), + ]; + + #[test] + fn test_var_int_encode() { + for (i, (value, encoded)) in VARINT_TEST_CASES.iter().enumerate() { + let mut w = Vec::new(); + let var_int = VarInt(*value); + let encoded_size = var_int.encoded_size(); + var_int.encode(&mut w).unwrap(); + let out = w.as_slice(); + + assert_eq!(out, *encoded, "Test {i} failed: data mismatch"); + assert_eq!( + encoded_size, + out.len(), + "Test {i} failed: encoded size mismatch" + ); + } + } + + #[test] + fn test_var_int_decode() { + for (i, (value, data)) in VARINT_TEST_CASES.iter().enumerate() { + let var_int = deserialize::(data).unwrap(); + + assert_eq!(*value, *var_int, "Test {i} failed: value mismatch"); + } + } + + #[test] + fn test_var_int_parse_error() { + // varint must be shortest encoding + let res = deserialize::(&[0x98, 0]); + assert!(matches!(res.unwrap_err(), Error::ParseFailed(_))); + + // If the last number is not a 0, it will error with an IO error (UnexpectedEof) + let res = deserialize::(&[0xff; 1]); + assert!(matches!(res.unwrap_err(), Error::IoError(_))); + + let res = deserialize::(&[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 1u8, + ]); + assert!(matches!(res.unwrap_err(), Error::ParseFailed(_))); + } +} diff --git a/rust/chains/tw_pactus/src/entry.rs b/rust/chains/tw_pactus/src/entry.rs new file mode 100644 index 00000000000..e9d12f3ac8c --- /dev/null +++ b/rust/chains/tw_pactus/src/entry.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Pactus::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +use crate::compiler::PactusCompiler; +use crate::modules::transaction_util::PactusTransactionUtil; +use crate::signer::PactusSigner; +use crate::types::Address; + +pub struct PactusEntry; + +impl CoinEntry for PactusEntry { + type AddressPrefix = NoPrefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + type TransactionUtil = PactusTransactionUtil; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn parse_address_unchecked(&self, address: &str) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let public_key = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + Address::from_public_key(public_key) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + PactusSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + PactusCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + PactusCompiler::compile(coin, input, signatures, public_keys) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(PactusTransactionUtil) + } +} diff --git a/rust/chains/tw_pactus/src/lib.rs b/rust/chains/tw_pactus/src/lib.rs new file mode 100644 index 00000000000..938d9bba25a --- /dev/null +++ b/rust/chains/tw_pactus/src/lib.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod compiler; +pub mod encoder; +pub mod entry; +pub mod modules; +pub mod signer; +pub mod transaction; +pub mod types; diff --git a/rust/chains/tw_pactus/src/modules/mod.rs b/rust/chains/tw_pactus/src/modules/mod.rs new file mode 100644 index 00000000000..b09e92443b1 --- /dev/null +++ b/rust/chains/tw_pactus/src/modules/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod transaction_util; +pub mod tx_builder; diff --git a/rust/chains/tw_pactus/src/modules/transaction_util.rs b/rust/chains/tw_pactus/src/modules/transaction_util.rs new file mode 100644 index 00000000000..a28e72b8f35 --- /dev/null +++ b/rust/chains/tw_pactus/src/modules/transaction_util.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_util::TransactionUtil; +use tw_encoding::hex; + +use crate::encoder::deserialize; +use crate::transaction::Transaction; + +pub struct PactusTransactionUtil; + +impl TransactionUtil for PactusTransactionUtil { + fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + Self::calc_tx_hash_impl(coin, encoded_tx) + } +} + +impl PactusTransactionUtil { + fn calc_tx_hash_impl(_coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + let trx_bytes = hex::decode(encoded_tx).map_err(|_| SigningErrorType::Error_input_parse)?; + + let trx = deserialize::(&trx_bytes)?; + + Ok(hex::encode(trx.id(), false)) + } +} diff --git a/rust/chains/tw_pactus/src/modules/tx_builder.rs b/rust/chains/tw_pactus/src/modules/tx_builder.rs new file mode 100644 index 00000000000..e9e2449be96 --- /dev/null +++ b/rust/chains/tw_pactus/src/modules/tx_builder.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::payload::{BondPayload, Payload, TransferPayload}; +use crate::transaction::Transaction; +use crate::types::{Address, Amount, ValidatorPublicKey}; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_proto::Pactus; + +pub struct TxBuilder; + +impl TxBuilder { + pub fn from_proto(input: &Pactus::Proto::SigningInput) -> SigningResult { + match &input.transaction { + None => SigningError::err(SigningErrorType::Error_invalid_params), + Some(trx) => { + let payload: Box = match &trx.payload { + Pactus::Proto::mod_TransactionMessage::OneOfpayload::transfer(pld) => { + let sender = Address::from_str(&pld.sender)?; + let receiver = Address::from_str(&pld.receiver)?; + Box::new(TransferPayload::new(sender, receiver, Amount(pld.amount))) + }, + Pactus::Proto::mod_TransactionMessage::OneOfpayload::bond(pld) => { + let sender = Address::from_str(&pld.sender)?; + let receiver = Address::from_str(&pld.receiver)?; + let public_key = if !pld.public_key.is_empty() { + Some(ValidatorPublicKey::from_str(&pld.public_key)?) + } else { + None + }; + + Box::new(BondPayload::new( + sender, + receiver, + Amount(pld.stake), + public_key, + )) + }, + Pactus::Proto::mod_TransactionMessage::OneOfpayload::None => { + return SigningError::err(SigningErrorType::Error_invalid_params) + }, + }; + + Ok(Transaction::new( + trx.lock_time, + Amount(trx.fee), + trx.memo.to_string(), + payload, + )) + }, + } + } +} diff --git a/rust/chains/tw_pactus/src/signer.rs b/rust/chains/tw_pactus/src/signer.rs new file mode 100644 index 00000000000..baf4f0a068b --- /dev/null +++ b/rust/chains/tw_pactus/src/signer.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ed25519; +use tw_keypair::traits::KeyPairTrait; +use tw_proto::Pactus::Proto; + +use crate::modules::tx_builder::TxBuilder; + +pub struct PactusSigner; + +impl PactusSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let mut trx = TxBuilder::from_proto(&input)?; + let key_pair = ed25519::sha512::KeyPair::try_from(input.private_key.as_ref())?; + let signature = trx.sign(key_pair.private())?; + + let data = trx.to_bytes()?; + + let output = Proto::SigningOutput { + transaction_id: trx.id().into(), + signed_transaction_data: data.into(), + signature: signature.to_bytes().to_vec().into(), + ..Proto::SigningOutput::default() + }; + + Ok(output) + } +} diff --git a/rust/chains/tw_pactus/src/transaction/mod.rs b/rust/chains/tw_pactus/src/transaction/mod.rs new file mode 100644 index 00000000000..6e00ff37699 --- /dev/null +++ b/rust/chains/tw_pactus/src/transaction/mod.rs @@ -0,0 +1,274 @@ +pub mod payload; + +use std::fmt::Debug; + +use payload::{BondPayload, Payload, PayloadType, TransferPayload}; +use tw_coin_entry::error::prelude::SigningResult; +use tw_hash::blake2::blake2_b; +use tw_keypair::ed25519::sha512::{PrivateKey, PublicKey}; +use tw_keypair::ed25519::Signature; +use tw_keypair::traits::SigningKeyTrait; + +use crate::encoder::error::Error as EncoderError; +use crate::encoder::{deserialize, Decodable, Encodable}; +use crate::types::Amount; + +const VERSION_LATEST: u8 = 1; +const FLAG_NOT_SIGNED: u8 = 0x02; + +#[derive(Debug)] +pub struct Transaction { + flags: u8, + version: u8, + lock_time: u32, + fee: Amount, + memo: String, + payload: Box, + signature: Option, + public_key: Option, +} + +impl Transaction { + pub fn new(lock_time: u32, fee: Amount, memo: String, payload: Box) -> Self { + Transaction { + flags: FLAG_NOT_SIGNED, + version: VERSION_LATEST, + lock_time, + fee, + memo, + payload, + public_key: None, + signature: None, + } + } + + pub fn from_bytes(input: &[u8]) -> SigningResult { + Ok(deserialize::(input)?) + } + + pub fn sign(&mut self, private_key: &PrivateKey) -> SigningResult { + let sign_bytes = self.sign_bytes()?; + let signature = private_key.sign(sign_bytes)?; + + self.set_signatory(private_key.public(), signature.clone()); + + Ok(signature) + } + + pub fn set_signatory(&mut self, public_key: PublicKey, signature: Signature) { + // Unset "Not Signed" flag + self.flags &= !FLAG_NOT_SIGNED; + + self.public_key = Some(public_key); + self.signature = Some(signature); + } + + pub fn id(&self) -> Vec { + blake2_b(&self.sign_bytes().unwrap_or_default(), 32).unwrap_or_default() + } + + pub fn to_bytes(&self) -> SigningResult> { + let mut w = Vec::with_capacity(self.encoded_size()); + + self.encode(&mut w)?; + + Ok(w.to_vec()) + } + + pub fn sign_bytes(&self) -> SigningResult> { + let mut w = Vec::new(); + self.encode_with_no_signatory(&mut w)?; + let mut sign_bytes = w.to_vec(); + sign_bytes.remove(0); // Remove flags + + Ok(sign_bytes) + } + + fn encode_with_no_signatory(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> { + self.flags.encode(w)?; + self.version.encode(w)?; + self.lock_time.encode(w)?; + self.fee.encode(w)?; + self.memo.encode(w)?; + self.payload.payload_type().encode(w)?; + self.payload.encode(w)?; + + Ok(()) + } + + fn is_signed(&self) -> bool { + self.flags & FLAG_NOT_SIGNED == 0 + } +} + +impl Encodable for Transaction { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> { + self.encode_with_no_signatory(w)?; + + if let Some(sig) = &self.signature { + sig.encode(w)?; + } + + if let Some(pub_key) = &self.public_key { + pub_key.encode(w)?; + } + + Ok(()) + } + + fn encoded_size(&self) -> usize { + let mut len = self.flags.encoded_size() + + self.version.encoded_size() + + self.lock_time.encoded_size() + + self.payload.payload_type().encoded_size() + + self.fee.encoded_size() + + self.memo.encoded_size() + + self.payload.encoded_size(); + + if let Some(sig) = &self.signature { + len += sig.encoded_size(); + } + + if let Some(pub_key) = &self.public_key { + len += pub_key.encoded_size(); + } + + len + } +} + +impl Decodable for Transaction { + fn decode(r: &mut dyn std::io::Read) -> Result { + let flags = u8::decode(r)?; + let version = u8::decode(r)?; + let lock_time = u32::decode(r)?; + let fee = Amount::decode(r)?; + let memo = String::decode(r)?; + let payload_type = PayloadType::decode(r)?; + let payload: Box = match payload_type { + PayloadType::Transfer => Box::new(TransferPayload::decode(r)?), + PayloadType::Bond => Box::new(BondPayload::decode(r)?), + _ => return Err(EncoderError::ParseFailed("Unsupported payload")), + }; + + let mut trx = Transaction { + flags, + version, + lock_time, + fee, + memo, + payload, + public_key: None, + signature: None, + }; + + if !trx.is_signed() { + return Ok(trx); + } + + let signature = Signature::decode(r)?; + let public_key = PublicKey::decode(r)?; + + trx.signature = Some(signature); + trx.public_key = Some(public_key); + + Ok(trx) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use tw_encoding::hex::DecodeHex; + + use crate::types::Address; + + use super::*; + + #[test] + fn test_payload_type_encoding() { + let mut stream = Vec::new(); + + let payload = PayloadType::Unbond; + payload.encode(&mut stream).unwrap(); + assert_eq!(stream.to_vec(), &[4]); + } + + const TRANSACTION_NOT_SIGNED: &str = concat!( + "02", // Flags + "01", // Version + "01020300", // LockTime + "e807", // Fee + "0474657374", // Memo + "01", // PayloadType + "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender + "037a14ae24533816e7faaa6ed28fcdde8e55a7df21", // Receiver + "a09c01" // Amount + ); + + const TRANSACTION_SIGNED: &str = concat!( + "00", // Flags + "01", // Version + "01020300", // LockTime + "e807", // Fee + "0474657374", // Memo + "01", // PayloadType + "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender + "037a14ae24533816e7faaa6ed28fcdde8e55a7df21", // Receiver + "a09c01", // Amount + "50ac25c7125271489b0cd230549257c93fb8c6265f2914a988ba7b81c1bc47ff", // Signature + "f027412dd59447867911035ff69742d171060a1f132ac38b95acc6e39ec0bd09", + "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa" // PublicKey + ); + + const TX_ID: &str = "34cd4656a98f7eb996e83efdc384cefbe3a9c52dca79a99245b4eacc0b0b4311"; + + #[test] + fn test_sign_signature() { + let expected_data = TRANSACTION_SIGNED.decode_hex().unwrap(); + let expected_id = TX_ID.decode_hex().unwrap(); + + let sender = Address::from_str("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr").unwrap(); + let receiver = Address::from_str("pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra").unwrap(); + let payload = Box::new(TransferPayload::new(sender, receiver, Amount(20000))); + let mut trx = Transaction::new(0x00030201, Amount(1000), "test".to_string(), payload); + + let private_key_data = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6" + .decode_hex() + .unwrap(); + let private_key = PrivateKey::try_from(private_key_data.as_slice()).unwrap(); + trx.sign(&private_key).unwrap(); + + assert_eq!(expected_data, trx.to_bytes().unwrap()); + assert_eq!(expected_id, trx.id()); + } + + #[test] + fn test_encoding_not_signed() { + let data = TRANSACTION_NOT_SIGNED.decode_hex().unwrap(); + let trx = Transaction::from_bytes(&data).unwrap(); + let expected_id = TX_ID.decode_hex().unwrap(); + + let encoded_data = trx.to_bytes().unwrap(); + + assert_eq!(encoded_data, data); + assert_eq!(expected_id, trx.id()); + assert_eq!(trx.encoded_size(), data.len()); + assert!(!trx.is_signed()); + } + + #[test] + fn test_encoding_signed() { + let data = TRANSACTION_SIGNED.decode_hex().unwrap(); + let trx = Transaction::from_bytes(&data).unwrap(); + let expected_id = TX_ID.decode_hex().unwrap(); + + let encoded_data = trx.to_bytes().unwrap(); + + assert_eq!(encoded_data, data); + assert_eq!(expected_id, trx.id()); + assert_eq!(trx.encoded_size(), data.len()); + assert!(trx.is_signed()); + } +} diff --git a/rust/chains/tw_pactus/src/transaction/payload/bond.rs b/rust/chains/tw_pactus/src/transaction/payload/bond.rs new file mode 100644 index 00000000000..152fa5a6a7b --- /dev/null +++ b/rust/chains/tw_pactus/src/transaction/payload/bond.rs @@ -0,0 +1,100 @@ +use crate::encoder::error::Error as EncoderError; +use crate::{ + encoder::{Decodable, Encodable}, + types::{Address, Amount, ValidatorPublicKey}, +}; + +use super::{Payload, PayloadType}; + +pub const BLS_PUBLIC_KEY_SIZE: usize = 96; + +#[derive(Debug)] +pub struct BondPayload { + sender: Address, + receiver: Address, + stake: Amount, + public_key: Option, +} + +impl BondPayload { + pub fn new( + sender: Address, + receiver: Address, + stake: Amount, + public_key: Option, + ) -> Self { + BondPayload { + sender, + receiver, + stake, + public_key, + } + } +} + +impl Encodable for BondPayload { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> { + self.sender.encode(w)?; + self.receiver.encode(w)?; + + match self.public_key { + Some(ref public_key) => { + (BLS_PUBLIC_KEY_SIZE as u8).encode(w)?; + public_key.encode(w)?; + }, + None => { + 0u8.encode(w)?; + }, + } + + self.stake.encode(w)?; + Ok(()) + } + + fn encoded_size(&self) -> usize { + self.sender.encoded_size() + + self.receiver.encoded_size() + + self.stake.encoded_size() + + match self.public_key { + Some(ref public_key) => 1 + public_key.encoded_size(), + None => 1, + } + } +} + +impl Decodable for BondPayload { + fn decode(r: &mut dyn std::io::Read) -> Result { + let sender = Address::decode(r)?; + let receiver = Address::decode(r)?; + + let mut public_key = None; + let public_key_size: u8 = u8::decode(r)?; + + if public_key_size == BLS_PUBLIC_KEY_SIZE as u8 { + public_key = Some(ValidatorPublicKey::decode(r)?); + } else if public_key_size != 0 { + return Err(EncoderError::ParseFailed("invalid public key size")); + } + + let stake = Amount::decode(r)?; + + Ok(BondPayload { + sender, + receiver, + stake, + public_key, + }) + } +} + +impl Payload for BondPayload { + fn signer(&self) -> &Address { + &self.sender + } + fn value(&self) -> Amount { + self.stake.clone() + } + fn payload_type(&self) -> PayloadType { + PayloadType::Bond + } +} diff --git a/rust/chains/tw_pactus/src/transaction/payload/mod.rs b/rust/chains/tw_pactus/src/transaction/payload/mod.rs new file mode 100644 index 00000000000..110155033dd --- /dev/null +++ b/rust/chains/tw_pactus/src/transaction/payload/mod.rs @@ -0,0 +1,60 @@ +mod bond; +mod transfer; + +pub use bond::BondPayload; +pub use transfer::TransferPayload; + +use std::fmt::Debug; + +use crate::encoder::error::Error as EncoderError; +use crate::{ + encoder::{Decodable, Encodable}, + types::{Address, Amount}, +}; + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum PayloadType { + Transfer = 1, + Bond = 2, + Sortition = 3, + Unbond = 4, + Withdraw = 5, +} + +impl TryFrom for PayloadType { + type Error = EncoderError; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(PayloadType::Transfer), + 2 => Ok(PayloadType::Bond), + 3 => Ok(PayloadType::Sortition), + 4 => Ok(PayloadType::Unbond), + 5 => Ok(PayloadType::Withdraw), + _ => Err(EncoderError::ParseFailed("Invalid PayloadType value")), + } + } +} + +impl Encodable for PayloadType { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> { + (*self as u8).encode(w) + } + + fn encoded_size(&self) -> usize { + 1 + } +} + +impl Decodable for PayloadType { + fn decode(r: &mut dyn std::io::Read) -> Result { + PayloadType::try_from(u8::decode(r)?) + } +} + +pub trait Payload: Debug + Encodable { + fn signer(&self) -> &Address; + fn value(&self) -> Amount; + fn payload_type(&self) -> PayloadType; +} diff --git a/rust/chains/tw_pactus/src/transaction/payload/transfer.rs b/rust/chains/tw_pactus/src/transaction/payload/transfer.rs new file mode 100644 index 00000000000..a39854c0429 --- /dev/null +++ b/rust/chains/tw_pactus/src/transaction/payload/transfer.rs @@ -0,0 +1,64 @@ +use crate::encoder::error::Error as EncoderError; +use crate::{ + encoder::{Decodable, Encodable}, + types::{Address, Amount}, +}; + +use super::{Payload, PayloadType}; + +#[derive(Debug)] +pub struct TransferPayload { + sender: Address, + receiver: Address, + amount: Amount, +} + +impl TransferPayload { + pub fn new(sender: Address, receiver: Address, amount: Amount) -> Self { + TransferPayload { + sender, + receiver, + amount, + } + } +} + +impl Encodable for TransferPayload { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> { + self.sender.encode(w)?; + self.receiver.encode(w)?; + self.amount.encode(w)?; + + Ok(()) + } + + fn encoded_size(&self) -> usize { + self.sender.encoded_size() + self.receiver.encoded_size() + self.amount.encoded_size() + } +} + +impl Decodable for TransferPayload { + fn decode(r: &mut dyn std::io::Read) -> Result { + let sender = Address::decode(r)?; + let receiver = Address::decode(r)?; + let amount = Amount::decode(r)?; + + Ok(TransferPayload { + sender, + receiver, + amount, + }) + } +} + +impl Payload for TransferPayload { + fn signer(&self) -> &Address { + &self.sender + } + fn value(&self) -> Amount { + self.amount.clone() + } + fn payload_type(&self) -> PayloadType { + PayloadType::Transfer + } +} diff --git a/rust/chains/tw_pactus/src/types/address.rs b/rust/chains/tw_pactus/src/types/address.rs new file mode 100644 index 00000000000..785e82c6639 --- /dev/null +++ b/rust/chains/tw_pactus/src/types/address.rs @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use std::str::FromStr; + +use bech32::{FromBase32, ToBase32}; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_hash::blake2::blake2_b; +use tw_hash::ripemd::ripemd_160; +use tw_hash::H160; +use tw_keypair::ed25519::sha512::PublicKey; +use tw_memory::Data; + +use crate::encoder::error::Error; +use crate::encoder::{Decodable, Encodable}; + +const ADDRESS_HRP: &str = "pc"; +const TREASURY_ADDRESS_STRING: &str = "000000000000000000000000000000000000000000"; + +/// Enum for Pactus address types. +#[derive(Debug, Clone, PartialEq)] +pub enum AddressType { + Treasury = 0, + Validator = 1, + BlsAccount = 2, + Ed25519Account = 3, +} + +impl TryFrom for AddressType { + type Error = AddressError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(AddressType::Treasury), + 1 => Ok(AddressType::Validator), + 2 => Ok(AddressType::BlsAccount), + 3 => Ok(AddressType::Ed25519Account), + _ => Err(AddressError::Unsupported), + } + } +} + +impl Encodable for AddressType { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + (self.clone() as u8).encode(w) + } + + fn encoded_size(&self) -> usize { + 1 + } +} + +impl Decodable for AddressType { + fn decode(r: &mut dyn std::io::Read) -> Result { + AddressType::try_from(u8::decode(r)?) + .map_err(|_| Error::ParseFailed("Invalid address type")) + } +} + +/// Pactus addresses are 21 bytes long. +/// The first byte indicates the address type, and the remaining 20 bytes +/// represent the hash of the public key. +/// The hash is computed as RIPEMD160(Blake2b(public key)). +#[derive(Debug, Clone, PartialEq)] +pub struct Address { + addr_type: AddressType, + pub_hash: H160, +} + +impl Address { + pub fn from_public_key(public_key: &PublicKey) -> Result { + let pud_data = public_key.to_bytes(); + let pub_hash_data = + ripemd_160(&blake2_b(pud_data.as_ref(), 32).map_err(|_| AddressError::Internal)?); + let pub_hash = Address::vec_to_pub_hash(pub_hash_data)?; + + Ok(Address { + addr_type: AddressType::Ed25519Account, + pub_hash, + }) + } + + pub fn is_treasury(&self) -> bool { + self.addr_type == AddressType::Treasury && self.pub_hash.is_zero() + } + + pub fn vec_to_pub_hash(vec: Vec) -> Result { + H160::try_from(vec.as_slice()).map_err(|_| AddressError::Internal) + } +} + +impl CoinAddress for Address { + #[inline] + fn data(&self) -> Data { + let mut data = Vec::with_capacity(21); + data.push(self.addr_type.clone() as u8); + data.extend_from_slice(self.pub_hash.as_ref()); + + data + } +} + +// Pactus addresses are encoded into a string format using the Bech32m encoding scheme. +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_treasury() { + return f.write_str(TREASURY_ADDRESS_STRING); + } + + let mut b32 = Vec::with_capacity(33); + + b32.push(bech32::u5::try_from_u8(self.addr_type.clone() as u8).map_err(|_| fmt::Error)?); + b32.extend_from_slice(&self.pub_hash.to_vec().to_base32()); + bech32::encode_to_fmt(f, ADDRESS_HRP, &b32, bech32::Variant::Bech32m) + .map_err(|_| fmt::Error)? + } +} + +impl Encodable for Address { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + self.addr_type.encode(w)?; + + if self.is_treasury() { + return Ok(()); + } + + self.pub_hash.encode(w)?; + + Ok(()) + } + + fn encoded_size(&self) -> usize { + if self.is_treasury() { + return 1; + } + + 21 + } +} + +impl Decodable for Address { + fn decode(r: &mut dyn std::io::Read) -> Result { + let addr_type = AddressType::decode(r)?; + if addr_type == AddressType::Treasury { + return Ok(Address { + addr_type, + pub_hash: H160::new(), + }); + } + + let pub_hash = H160::decode(r)?; + Ok(Address { + addr_type, + pub_hash, + }) + } +} + +impl FromStr for Address { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + if s == TREASURY_ADDRESS_STRING { + return Ok(Address { + addr_type: AddressType::Treasury, + pub_hash: H160::new(), + }); + } + + let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?; + + if hrp != ADDRESS_HRP { + return Err(AddressError::InvalidHrp); + } + + if b32.len() != 33 { + return Err(AddressError::InvalidInput); + } + + let addr_type = AddressType::try_from(b32[0].to_u8())?; + let b8 = Vec::::from_base32(&b32[1..]).map_err(|_| AddressError::InvalidInput)?; + let pub_hash = Address::vec_to_pub_hash(b8)?; + + Ok(Address { + addr_type, + pub_hash, + }) + } +} + +#[cfg(test)] +mod test { + use tw_encoding::hex::{DecodeHex, ToHex}; + use tw_keypair::ed25519::sha512::PrivateKey; + + use super::*; + use crate::encoder::{deserialize, Encodable}; + + #[test] + fn test_treasury_address_encoding() { + let addr = Address::from_str(TREASURY_ADDRESS_STRING).unwrap(); + assert!(addr.is_treasury()); + + let mut w = Vec::new(); + addr.encode(&mut w).unwrap(); + assert_eq!(w.to_vec(), [0x00]); + assert_eq!(addr.encoded_size(), 1); + } + + #[test] + fn test_treasury_address_decoding() { + let data = vec![0u8]; + + let addr = deserialize::
(&data).unwrap(); + assert!(addr.is_treasury()); + assert_eq!(addr.to_string(), TREASURY_ADDRESS_STRING); + } + + #[test] + fn test_address_encoding() { + let addr = Address::from_str("pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra").unwrap(); + assert!(!addr.is_treasury()); + + let mut w = Vec::new(); + addr.encode(&mut w).unwrap(); + assert_eq!( + w.to_vec(), + "03000102030405060708090a0b0c0d0e0f00010203" + .decode_hex() + .unwrap() + ); + assert_eq!(addr.encoded_size(), 21); + } + + #[test] + fn test_address_decoding() { + let data = "03000102030405060708090a0b0c0d0e0f00010203" + .decode_hex() + .unwrap(); + + let addr = deserialize::
(&data).unwrap(); + assert!(!addr.is_treasury()); + assert_eq!( + addr.to_string(), + "pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra" + ); + } + + #[test] + fn test_address_string() { + struct TestCase<'a> { + name: &'a str, + addr_type: AddressType, + pub_hash: &'a str, + expected_addr: &'a str, + } + + // Define a list of test cases for encoding and decoding + let test_cases = vec![ + TestCase { + name: "Type Treasury (0)", + addr_type: AddressType::Treasury, + pub_hash: "0000000000000000000000000000000000000000", + expected_addr: TREASURY_ADDRESS_STRING, + }, + TestCase { + name: "Type Validator (1)", + addr_type: AddressType::Validator, + pub_hash: "000102030405060708090a0b0c0d0e0f00010203", + expected_addr: "pc1pqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr803qet", + }, + TestCase { + name: "Type BLS-Account (2)", + addr_type: AddressType::BlsAccount, + pub_hash: "000102030405060708090a0b0c0d0e0f00010203", + expected_addr: "pc1zqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr6ypawk", + }, + TestCase { + name: "Type Secp256k1-Account (3)", + addr_type: AddressType::Ed25519Account, + pub_hash: "000102030405060708090a0b0c0d0e0f00010203", + expected_addr: "pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra", + }, + ]; + + for case in test_cases { + let pub_hash_data = case.pub_hash.decode_hex().unwrap(); + let addr = Address { + addr_type: case.addr_type, + pub_hash: Address::vec_to_pub_hash(pub_hash_data).unwrap(), + }; + + let addr_str = addr.to_string(); + assert_eq!(addr_str, case.expected_addr, "test {} failed", case.name); + } + } + + #[test] + fn test_encodable() { + let expected_data = "03b281dee7850ca2272d9ba95b16d48030821aaf27" + .decode_hex() + .unwrap(); + let private_key = PrivateKey::try_from( + "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5", + ) + .unwrap(); + let address = Address::from_public_key(&private_key.public()).unwrap(); + let mut w = Vec::new(); + + address.encode(&mut w).unwrap(); + + assert_eq!(expected_data, w.to_vec(),); + assert_eq!(expected_data.len(), address.encoded_size()); + } + + #[test] + fn test_address_from_private_key() { + let private_key_data = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6" + .decode_hex() + .unwrap(); + let private_key = PrivateKey::try_from(private_key_data.as_slice()).unwrap(); + let public_key = private_key.public(); + let address = Address::from_public_key(&public_key).unwrap(); + + let expected_public_key = + "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa"; + let expected_address = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"; + + assert_eq!(public_key.to_bytes().to_hex(), expected_public_key); + assert_eq!(address.to_string(), expected_address); + } +} diff --git a/rust/chains/tw_pactus/src/types/amount.rs b/rust/chains/tw_pactus/src/types/amount.rs new file mode 100644 index 00000000000..84c9e156e6a --- /dev/null +++ b/rust/chains/tw_pactus/src/types/amount.rs @@ -0,0 +1,20 @@ +use crate::encoder::{error::Error, var_int::VarInt, Decodable, Encodable}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Amount(pub i64); + +impl Encodable for Amount { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + VarInt::from(self.0 as usize).encode(w) + } + + fn encoded_size(&self) -> usize { + VarInt::from(self.0 as usize).encoded_size() + } +} + +impl Decodable for Amount { + fn decode(r: &mut dyn std::io::Read) -> Result { + Ok(Amount(*VarInt::decode(r)? as i64)) + } +} diff --git a/rust/chains/tw_pactus/src/types/mod.rs b/rust/chains/tw_pactus/src/types/mod.rs new file mode 100644 index 00000000000..ff66b91c4f4 --- /dev/null +++ b/rust/chains/tw_pactus/src/types/mod.rs @@ -0,0 +1,7 @@ +pub mod address; +pub mod amount; +pub mod validator_public_key; + +pub use address::Address; +pub use amount::Amount; +pub use validator_public_key::ValidatorPublicKey; diff --git a/rust/chains/tw_pactus/src/types/validator_public_key.rs b/rust/chains/tw_pactus/src/types/validator_public_key.rs new file mode 100644 index 00000000000..84425774143 --- /dev/null +++ b/rust/chains/tw_pactus/src/types/validator_public_key.rs @@ -0,0 +1,105 @@ +use crate::encoder::error::Error; +use crate::encoder::{decode::decode_fix_slice, encode::encode_fix_slice}; +use crate::encoder::{Decodable, Encodable}; +use bech32::FromBase32; +use std::str::FromStr; +use tw_keypair::KeyPairError; + +pub const BLS_PUBLIC_KEY_SIZE: usize = 96; +pub const PUBLIC_KEY_HRP: &str = "public"; + +#[derive(Debug)] +pub struct ValidatorPublicKey(pub [u8; BLS_PUBLIC_KEY_SIZE]); + +impl Encodable for ValidatorPublicKey { + fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { + encode_fix_slice(&self.0, w) + } + + fn encoded_size(&self) -> usize { + BLS_PUBLIC_KEY_SIZE + } +} + +impl Decodable for ValidatorPublicKey { + fn decode(r: &mut dyn std::io::Read) -> Result { + Ok(ValidatorPublicKey(decode_fix_slice::( + r, + )?)) + } +} + +impl FromStr for ValidatorPublicKey { + type Err = KeyPairError; + + fn from_str(s: &str) -> Result { + let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| KeyPairError::InvalidPublicKey)?; + if hrp != PUBLIC_KEY_HRP { + return Err(KeyPairError::InvalidPublicKey); + } + + let b8 = Vec::::from_base32(&b32[1..]).map_err(|_| KeyPairError::InvalidPublicKey)?; + let pub_data = b8.try_into().map_err(|_| KeyPairError::InvalidPublicKey)?; + + Ok(ValidatorPublicKey(pub_data)) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use tw_encoding::hex::DecodeHex; + + use crate::types::ValidatorPublicKey; + + #[test] + fn test_public_key_string() { + struct TestCase<'a> { + name: &'a str, + pub_key_str: &'a str, + pub_key_data: &'a str, + } + + // Define a list of test cases for encoding and decoding + let test_cases = vec![ + TestCase { + name: "invalid checksum", + pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx470", + pub_key_data: "", + }, + TestCase { + name: "invalid length: 95", + pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg73y98kl", + pub_key_data: "", + }, + TestCase { + name: "invalid HRP", + pub_key_str: "xxx1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5evslaq", + pub_key_data: "", + }, + TestCase { + name: "OK", + pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx47a", + pub_key_data: "af0f74917f5065af94727ae9541b0ddcfb5b828a9e016b02498f477ed37fb44d5d882495afb6fd4f9773e4ea9deee436030c4d61c6e3a1151585e1d838cae1444a438d089ce77e10c492a55f6908125c5be9b236a246e4082d08de564e111e65", + }, + ]; + + for case in test_cases { + let pub_key_data = case.pub_key_data.decode_hex().unwrap().to_vec(); + let test_result = ValidatorPublicKey::from_str(case.pub_key_str); + + if pub_key_data.is_empty() { + assert!(test_result.is_err()); + } else { + assert!(test_result.is_ok()); + assert_eq!( + test_result.unwrap().0.to_vec(), + pub_key_data, + "test {} failed", + case.name + ); + } + } + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/compact_integer.rs b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs index 4a95a378699..ec4e3ae9d2e 100644 --- a/rust/frameworks/tw_utxo/src/encode/compact_integer.rs +++ b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs @@ -64,17 +64,22 @@ mod tests { let mut stream = Stream::default(); stream - .append(&CompactInteger::from(0_usize)) + .append(&CompactInteger::from(0x00_usize)) .append(&CompactInteger::from(0xfc_usize)) .append(&CompactInteger::from(0xfd_usize)) .append(&CompactInteger::from(0xffff_usize)) - .append(&CompactInteger::from(0x10000_usize)) + .append(&CompactInteger::from(0x01_0000_usize)) .append(&CompactInteger::from(0xffff_ffff_usize)) - .append(&CompactInteger(0x1_0000_0000_u64)); + .append(&CompactInteger(0x01_0000_0000_u64)); let expected = vec![ - 0_u8, 0xfc, 0xfd, 0xfd, 0x00, 0xfd, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x01, 0x00, 0xfe, - 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, // 0x00 + 0xfc, // 0xfc + 0xfd, 0xfd, 0x00, // 0xfd + 0xfd, 0xff, 0xff, // 0xffff + 0xfe, 0x00, 0x00, 0x01, 0x00, // 0x01_0000 + 0xfe, 0xff, 0xff, 0xff, 0xff, // 0xffff_ffff + 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // 0x01_0000_0000 ]; assert_eq!(stream.out(), expected); diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index 40e2a51b0ef..7bff39d1ea0 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -25,6 +25,7 @@ tw_memory = { path = "../tw_memory" } tw_misc = { path = "../tw_misc" } tw_native_evmos = { path = "../chains/tw_native_evmos" } tw_native_injective = { path = "../chains/tw_native_injective" } +tw_pactus = { path = "../chains/tw_pactus" } tw_ronin = { path = "../chains/tw_ronin" } tw_solana = { path = "../chains/tw_solana" } tw_sui = { path = "../chains/tw_sui" } diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index 93630d0d66d..cde37c58cde 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -19,6 +19,7 @@ pub enum BlockchainType { InternetComputer, NativeEvmos, NativeInjective, + Pactus, Ronin, Solana, Sui, diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index b7c3f7751b7..d08784ccd56 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -19,6 +19,7 @@ use tw_greenfield::entry::GreenfieldEntry; use tw_internet_computer::entry::InternetComputerEntry; use tw_native_evmos::entry::NativeEvmosEntry; use tw_native_injective::entry::NativeInjectiveEntry; +use tw_pactus::entry::PactusEntry; use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; use tw_sui::entry::SuiEntry; @@ -39,6 +40,7 @@ const GREENFIELD: GreenfieldEntry = GreenfieldEntry; const INTERNET_COMPUTER: InternetComputerEntry = InternetComputerEntry; const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry; const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; +const PACTUS: PactusEntry = PactusEntry; const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; const SUI: SuiEntry = SuiEntry; @@ -59,6 +61,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&INTERNET_COMPUTER), BlockchainType::NativeEvmos => Ok(&NATIVE_EVMOS), BlockchainType::NativeInjective => Ok(&NATIVE_INJECTIVE), + BlockchainType::Pactus => Ok(&PACTUS), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), BlockchainType::Sui => Ok(&SUI), diff --git a/rust/tw_tests/tests/chains/mod.rs b/rust/tw_tests/tests/chains/mod.rs index b147a7b83e5..d048ec74a00 100644 --- a/rust/tw_tests/tests/chains/mod.rs +++ b/rust/tw_tests/tests/chains/mod.rs @@ -15,6 +15,7 @@ mod greenfield; mod internet_computer; mod native_evmos; mod native_injective; +mod pactus; mod solana; mod sui; mod tbinance; diff --git a/rust/tw_tests/tests/chains/pactus/mod.rs b/rust/tw_tests/tests/chains/pactus/mod.rs new file mode 100644 index 00000000000..88e8edb4017 --- /dev/null +++ b/rust/tw_tests/tests/chains/pactus/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod pactus_address; +mod pactus_compile; +mod pactus_sign; +mod pactus_transaction_util; +mod test_cases; diff --git a/rust/tw_tests/tests/chains/pactus/pactus_address.rs b/rust/tw_tests/tests/chains/pactus/pactus_address.rs new file mode 100644 index 00000000000..9d64d1590a7 --- /dev/null +++ b/rust/tw_tests/tests/chains/pactus/pactus_address.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_pactus_address_derive() { + test_address_derive( + CoinType::Pactus, + "2134ae97465505dfd5a1fd05a8a0f146209c601eb3f1b0363b4cfe4b47ba1ab4", + "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl", + ); +} + +#[test] +fn test_pactus_address_normalization() { + test_address_normalization( + CoinType::Pactus, + "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl", + "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl", + ); +} + +#[test] +fn test_pactus_address_is_valid() { + test_address_valid( + CoinType::Pactus, + "000000000000000000000000000000000000000000", + ); + test_address_valid( + CoinType::Pactus, + "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr", + ); + test_address_valid( + CoinType::Pactus, + "pc1zzqkzzu4vyddss052as6c37qrdcfptegquw826x", + ); + test_address_valid( + CoinType::Pactus, + "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl", + ); +} + +#[test] +fn test_pactus_address_invalid() { + test_address_invalid(CoinType::Pactus, ""); + test_address_invalid(CoinType::Pactus, "00"); + test_address_invalid(CoinType::Pactus, "not_proper_encoded"); + test_address_invalid(CoinType::Pactus, "pc1ioiooi"); + test_address_invalid(CoinType::Pactus, "pc19p72rf"); + test_address_invalid( + CoinType::Pactus, + "qc1z0hrct7eflrpw4ccrttxzs4qud2axex4dh8zz75", + ); + test_address_invalid( + CoinType::Pactus, + "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dg8xaf5", + ); // invalid checksum + test_address_invalid( + CoinType::Pactus, + "pc1p0hrct7eflrpw4ccrttxzs4qud2axexs2dhdk8", + ); // invalid length + test_address_invalid( + CoinType::Pactus, + "pc1y0hrct7eflrpw4ccrttxzs4qud2axex4dksmred", + ); // invalid type +} + +#[test] +fn test_pactus_address_get_data() { + test_address_get_data( + CoinType::Pactus, + "000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000", + ); + test_address_get_data( + CoinType::Pactus, + "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr", + "017dc785fb29f8c2eae3035acc28541c6aba6c9aad", + ); + test_address_get_data( + CoinType::Pactus, + "pc1zzqkzzu4vyddss052as6c37qrdcfptegquw826x", + "02102c2172ac235b083e8aec3588f8036e1215e500", + ); + test_address_get_data( + CoinType::Pactus, + "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl", + "03f4acc4cf284bc69a73afa245c2d872bc8d818c88", + ); +} diff --git a/rust/tw_tests/tests/chains/pactus/pactus_compile.rs b/rust/tw_tests/tests/chains/pactus/pactus_compile.rs new file mode 100644 index 00000000000..27a0c274c76 --- /dev/null +++ b/rust/tw_tests/tests/chains/pactus/pactus_compile.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::pactus::test_cases::PRIVATE_KEY; +use crate::chains::pactus::test_cases::TEST_CASES; +use tw_any_coin::ffi::tw_transaction_compiler::{ + tw_transaction_compiler_compile, tw_transaction_compiler_pre_image_hashes, +}; +use tw_coin_entry::error::prelude::*; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::ToHex; +use tw_keypair::ed25519; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper; +use tw_misc::traits::ToBytesVec; +use tw_proto::Pactus::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_proto::{deserialize, serialize}; + +#[test] +fn test_pactus_transaction_compile() { + for case in TEST_CASES.iter() { + // Step 1: Create signing input. + let input = (case.sign_input_fn)(); + + // Step 2: Obtain preimage hash + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let preimage_data = TWDataHelper::wrap(unsafe { + tw_transaction_compiler_pre_image_hashes(CoinType::Pactus as u32, input_data.ptr()) + }) + .to_vec() + .expect("!tw_transaction_compiler_pre_image_hashes returned nullptr"); + + let preimage: CompilerProto::PreSigningOutput = + deserialize(&preimage_data).expect("Coin entry returned an invalid output"); + + assert_eq!(preimage.error, SigningErrorType::OK); + assert!(preimage.error_message.is_empty()); + assert_eq!(preimage.data.to_hex(), case.data_to_sign); + + // Step 3: Sign the data "externally" + let private_key = ed25519::sha512::KeyPair::try_from(PRIVATE_KEY).unwrap(); + let public_key = private_key.public().to_vec(); + + let signature = private_key + .sign(preimage.data.to_vec()) + .expect("Error signing data") + .to_vec(); + assert_eq!(signature.to_hex(), case.signature); + + // Step 4: Compile transaction info + let signatures = TWDataVectorHelper::create([signature]); + let public_keys = TWDataVectorHelper::create([public_key]); + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let output_data = TWDataHelper::wrap(unsafe { + tw_transaction_compiler_compile( + CoinType::Pactus as u32, + input_data.ptr(), + signatures.ptr(), + public_keys.ptr(), + ) + }) + .to_vec() + .expect("!tw_transaction_compiler_compile returned nullptr"); + + let output: Proto::SigningOutput = + deserialize(&output_data).expect("Coin entry returned an invalid output"); + + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + assert_eq!(output.transaction_id.to_hex(), case.transaction_id); + assert_eq!(output.signature.to_hex(), case.signature); + assert_eq!(output.signed_transaction_data.to_hex(), case.signed_data); + } +} diff --git a/rust/tw_tests/tests/chains/pactus/pactus_sign.rs b/rust/tw_tests/tests/chains/pactus/pactus_sign.rs new file mode 100644 index 00000000000..f6547b1918d --- /dev/null +++ b/rust/tw_tests/tests/chains/pactus/pactus_sign.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::pactus::test_cases::PRIVATE_KEY; +use crate::chains::pactus::test_cases::TEST_CASES; +use tw_any_coin::ffi::tw_any_signer::tw_any_signer_sign; +use tw_coin_entry::error::prelude::*; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_proto::Pactus::Proto; +use tw_proto::{deserialize, serialize}; + +#[test] +fn test_pactus_sign_transactions() { + for case in TEST_CASES.iter() { + let input = Proto::SigningInput { + private_key: PRIVATE_KEY.decode_hex().unwrap().into(), + ..(case.sign_input_fn)() + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { + tw_any_signer_sign(input_data.ptr(), CoinType::Pactus as u32) + }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + assert_eq!(output.transaction_id.to_hex(), case.transaction_id); + assert_eq!(output.signature.to_hex(), case.signature); + assert_eq!(output.signed_transaction_data.to_hex(), case.signed_data); + } +} diff --git a/rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs b/rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs new file mode 100644 index 00000000000..989448e3210 --- /dev/null +++ b/rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs @@ -0,0 +1,14 @@ +use super::test_cases::TEST_CASES; +use tw_any_coin::test_utils::transaction_calc_tx_hash_utils::TransactionUtilHelper; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_pactus_transaction_util_calc_tx_hash() { + for case in TEST_CASES { + let tx_hash = TransactionUtilHelper::calc_tx_hash(CoinType::Pactus, case.signed_data); + assert_eq!(tx_hash, case.transaction_id); + + let tx_hash = TransactionUtilHelper::calc_tx_hash(CoinType::Pactus, case.signed_data); + assert_eq!(tx_hash, case.transaction_id); + } +} diff --git a/rust/tw_tests/tests/chains/pactus/test_cases.rs b/rust/tw_tests/tests/chains/pactus/test_cases.rs new file mode 100644 index 00000000000..0a79e33bb2f --- /dev/null +++ b/rust/tw_tests/tests/chains/pactus/test_cases.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_proto::Pactus::Proto; + +/// A macro to define test data. +/// Transaction format explained here: https://docs.pactus.org/protocol/transaction/format/ +macro_rules! define_test_data { + ( $tx_id:expr, + $signature:expr, + $public_key:expr + $(, $param:expr)*, + ) => { + pub const DATA_TO_SIGN: &str = concat!( + $( $param, )* + ); + + pub const SIGNED_DATA: &str = concat!( + "00", // Signed Flag + $($param, )* + $signature, + $public_key + ); + + pub const TX_ID: &str = $tx_id; + pub const SIGNATURE: &str = $signature; + }; +} + +// Private key for all the test cases +pub const PRIVATE_KEY: &str = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"; + +// Successfully broadcasted transaction: +// https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f +pub mod transfer_test_case { + use super::*; + use tw_encoding::hex::DecodeHex; + + define_test_data!( + "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f", // transaction ID + "4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc\ + 8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506", // Signature + "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", // PublicKey + "01", // Version + "24a32300", // LockTime + "80ade204", // Fee + "0b77616c6c65742d636f7265", // Memo + "01", // PayloadType + "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender + "037a14ae24533816e7faaa6ed28fcdde8e55a7df21", // Receiver + "8084af5f", // Amount + ); + + pub fn sign_input() -> Proto::SigningInput<'static> { + let transfer_payload = Proto::TransferPayload { + sender: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr".into(), + receiver: "pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra".into(), + amount: 200000000, + }; + + let transaction = Proto::TransactionMessage { + lock_time: 2335524, + fee: 10000000, + memo: "wallet-core".into(), + payload: Proto::mod_TransactionMessage::OneOfpayload::transfer(transfer_payload), + }; + + let private_key_bytes = PRIVATE_KEY.decode_hex().unwrap(); + + Proto::SigningInput { + transaction: Some(transaction), + private_key: private_key_bytes.into(), + } + } +} + +// Successfully broadcasted transaction: +// https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f +pub mod bond_with_public_key_test_case { + use super::*; + use tw_encoding::hex::DecodeHex; + + define_test_data!( + "d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f", // transaction ID + "0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac7\ + 9d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300", // Signature + "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", // PublicKey + "01", // Version + "c1b02300", // LockTime + "80ade204", // Fee + "0b77616c6c65742d636f7265", // Memo + "02", // PayloadType + "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender + "0129288df0bf7bd4b5e9eeed8b932d0c76f451823d", // Receiver + "60\ + 98bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a3935\ + 5cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a56bbc\ + dde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13833b", // Validator Public key + "8094ebdc03", // Stake + ); + + pub fn sign_input() -> Proto::SigningInput<'static> { + let bond_payload = Proto::BondPayload { + sender: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr".into(), + receiver: "pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl".into(), + stake: 1000000000, + public_key: "public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn".into() + }; + + let transaction = Proto::TransactionMessage { + lock_time: 2339009, + fee: 10000000, + memo: "wallet-core".into(), + payload: Proto::mod_TransactionMessage::OneOfpayload::bond(bond_payload), + }; + + let private_key_bytes = PRIVATE_KEY.decode_hex().unwrap(); + + Proto::SigningInput { + transaction: Some(transaction), + private_key: private_key_bytes.into(), + } + } +} + +// Successfully broadcasted transaction: +// https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80 +// +// If the validator exists and has already been staked, there’s no need to send the public key. +// If the validator does not exist, the public key is required, as it is not indexed on the chain. +pub mod bond_without_public_key_test_case { + use super::*; + use tw_encoding::hex::DecodeHex; + + define_test_data!( + "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80", // transaction ID + "9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0\ + 715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d", // Signature + "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", // PublicKey + "01", // Version + "5ca32300", // LockTime + "80ade204", // Fee + "0b77616c6c65742d636f7265", // Memo + "02", // PayloadType + "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender + "01d2fa2a7d560502199995ea260954f064d90278be", // Receiver + "00", // Public key zero + "8094ebdc03", // Stake + ); + + pub fn sign_input() -> Proto::SigningInput<'static> { + let bond_payload = Proto::BondPayload { + sender: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr".into(), + receiver: "pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd".into(), + stake: 1000000000, + public_key: Default::default(), + }; + + let transaction = Proto::TransactionMessage { + lock_time: 2335580, + fee: 10000000, + memo: "wallet-core".into(), + payload: Proto::mod_TransactionMessage::OneOfpayload::bond(bond_payload), + }; + + let private_key_bytes = PRIVATE_KEY.decode_hex().unwrap(); + + Proto::SigningInput { + transaction: Some(transaction), + private_key: private_key_bytes.into(), + } + } +} + +pub(crate) struct TestCase { + pub sign_input_fn: fn() -> Proto::SigningInput<'static>, + pub transaction_id: &'static str, + pub signature: &'static str, + pub data_to_sign: &'static str, + pub signed_data: &'static str, +} + +pub(crate) const TEST_CASES: &[TestCase; 3] = &[ + TestCase { + sign_input_fn: transfer_test_case::sign_input, + transaction_id: transfer_test_case::TX_ID, + signature: transfer_test_case::SIGNATURE, + data_to_sign: transfer_test_case::DATA_TO_SIGN, + signed_data: transfer_test_case::SIGNED_DATA, + }, + TestCase { + sign_input_fn: bond_with_public_key_test_case::sign_input, + transaction_id: bond_with_public_key_test_case::TX_ID, + signature: bond_with_public_key_test_case::SIGNATURE, + data_to_sign: bond_with_public_key_test_case::DATA_TO_SIGN, + signed_data: bond_with_public_key_test_case::SIGNED_DATA, + }, + TestCase { + sign_input_fn: bond_without_public_key_test_case::sign_input, + transaction_id: bond_without_public_key_test_case::TX_ID, + signature: bond_without_public_key_test_case::SIGNATURE, + data_to_sign: bond_without_public_key_test_case::DATA_TO_SIGN, + signed_data: bond_without_public_key_test_case::SIGNED_DATA, + }, +]; diff --git a/rust/tw_tests/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs index bb6c14842e3..4f7e27b9375 100644 --- a/rust/tw_tests/tests/coin_address_derivation_test.rs +++ b/rust/tw_tests/tests/coin_address_derivation_test.rs @@ -153,6 +153,7 @@ fn test_coin_address_derivation() { CoinType::Solana => "5sn9QYhDaq61jLXJ8Li5BKqGL4DDMJQvU1rdN8XgVuwC", CoinType::Sui => "0x01a5c6c1b74cec4fbd12b3e17252b83448136065afcdf24954dc3a9c26df4905", CoinType::TON => "UQCj3jAU_Ec2kXdAqweKt4rYjiwTNwiCfaUnIDHGh7wTwx_G", + CoinType::Pactus => "pc1rk2qaaeu9pj3zwtvm49d3d4yqxzpp4te87cx0am", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/src/Coin.cpp b/src/Coin.cpp index bb3d3717d54..447c5c2f5a0 100644 --- a/src/Coin.cpp +++ b/src/Coin.cpp @@ -67,6 +67,7 @@ #include "NativeEvmos/Entry.h" #include "NativeInjective/Entry.h" #include "BitcoinCash/Entry.h" +#include "Pactus/Entry.h" // end_of_coin_includes_marker_do_not_modify using namespace TW; @@ -127,6 +128,7 @@ InternetComputer::Entry InternetComputerDP; NativeEvmos::Entry NativeEvmosDP; NativeInjective::Entry NativeInjectiveDP; BitcoinCash::Entry BitcoinCashDP; +Pactus::Entry PactusDP; // end_of_coin_dipatcher_declarations_marker_do_not_modify CoinEntry* coinDispatcher(TWCoinType coinType) { @@ -189,6 +191,7 @@ CoinEntry* coinDispatcher(TWCoinType coinType) { case TWBlockchainNativeEvmos: entry = &NativeEvmosDP; break; case TWBlockchainNativeInjective: entry = &NativeInjectiveDP; break; case TWBlockchainBitcoinCash: entry = &BitcoinCashDP; break; + case TWBlockchainPactus: entry = &PactusDP; break; // end_of_coin_dipatcher_switch_marker_do_not_modify default: entry = nullptr; break; diff --git a/src/Pactus/Entry.h b/src/Pactus/Entry.h new file mode 100644 index 00000000000..2f41bcde419 --- /dev/null +++ b/src/Pactus/Entry.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "rust/RustCoinEntry.h" + +namespace TW::Pactus { + +/// Entry point for Pactus coin. +/// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file +class Entry : public Rust::RustCoinEntry { +}; + +} // namespace TW::Pactus + diff --git a/src/proto/Pactus.proto b/src/proto/Pactus.proto new file mode 100644 index 00000000000..ce3f9d3a02e --- /dev/null +++ b/src/proto/Pactus.proto @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +syntax = "proto3"; + +package TW.Pactus.Proto; +option java_package = "wallet.core.jni.proto"; + +import "Common.proto"; + +message TransactionMessage { + // The lock time for the transaction. + uint32 lock_time = 1; + // The transaction fee in NanoPAC. + int64 fee = 2; + // A memo string for the transaction (optional). + string memo = 3; + + oneof payload { + TransferPayload transfer = 10; + BondPayload bond = 11; + } +} + +// Transfer payload for creating a Transfer transaction between two accounts. +message TransferPayload { + // The sender's account address. + string sender = 1; + // The receiver's account address. + string receiver = 2; + // The amount to be transferred, specified in NanoPAC. + int64 amount = 3; +} + +// Bond payload for creating a Bond transaction from an account to a validator. +message BondPayload { + // The sender's account address. + string sender = 1; + // The receiver's validator address. + string receiver = 2; + // The stake amount in NanoPAC. + int64 stake = 3; + // The public key of the validator (only set when creating a new validator). + string public_key = 4; +} + +// Input data necessary to create a signed transaction. +message SigningInput { + bytes private_key = 1; + TransactionMessage transaction = 2; +} + +// Transaction signing output. +message SigningOutput { + // Transaction ID (Hash). + bytes transaction_id = 1; + // Signed and encoded transaction bytes. + bytes signed_transaction_data = 2; + // Signature the signed transaction. + bytes signature = 3; + // A possible error, `OK` if none. + Common.Proto.SigningError error = 4; + // Detailed error message, if any. + string error_message = 5; +} diff --git a/swift/Tests/Blockchains/PactusTests.swift b/swift/Tests/Blockchains/PactusTests.swift new file mode 100644 index 00000000000..933bdeaebea --- /dev/null +++ b/swift/Tests/Blockchains/PactusTests.swift @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import XCTest +import WalletCore + +class PactusTests: XCTestCase { + var privateKey: PrivateKey! + + override func setUp() { + super.setUp() + privateKey = PrivateKey(data: Data(hexString: "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6")!)! + } + + func testAddress() { + let pubkey = privateKey.getPublicKeyEd25519() + let address = AnyAddress(publicKey: pubkey, coin: .pactus) + let addressFromString = AnyAddress(string: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr", coin: .pactus)! + + XCTAssertEqual(pubkey.data.hexString, "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa") + XCTAssertEqual(address.description, addressFromString.description) + } + + func testTransferSign() { + // Successfully broadcasted transaction: + // https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f + let input = PactusSigningInput.with { + $0.privateKey = privateKey.data + $0.transaction = PactusTransactionMessage.with { + $0.lockTime = 2335524 + $0.fee = 10000000 + $0.memo = "wallet-core" + $0.transfer = PactusTransferPayload.with { + $0.sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr" + $0.receiver = "pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra" + $0.amount = 200000000 + } + } + } + + let output: PactusSigningOutput = AnySigner.sign(input: input, coin: .pactus) + + let expectedTransactionID = "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f" + let expectedSignature = "4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc" + + "8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506" + let expectedSignedData = "000124a3230080ade2040b77616c6c65742d636f726501037098338e0b680811" + + "9dfd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df" + + "218084af5f4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9" + + "b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736" + + "693eda850695794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560b" + + "b72145f4fa"; + XCTAssertEqual(output.transactionID.hexString, expectedTransactionID) + XCTAssertEqual(output.signature.hexString, expectedSignature) + XCTAssertEqual(output.signedTransactionData.hexString, expectedSignedData) + } + + func testBondWithPublicKeySign() { + // Successfully broadcasted transaction: + // https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f + let input = PactusSigningInput.with { + $0.privateKey = privateKey.data + $0.transaction = PactusTransactionMessage.with { + $0.lockTime = 2339009 + $0.fee = 10000000 + $0.memo = "wallet-core" + $0.bond = PactusBondPayload.with { + $0.sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr" + $0.receiver = "pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl" + $0.stake = 1000000000 + $0.publicKey = "public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn" + } + } + } + + let output: PactusSigningOutput = AnySigner.sign(input: input, coin: .pactus) + + let expectedTransactionID = "d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f" + let expectedSignature = "0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac7" + + "9d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300" + let expectedSignedData = "0001c1b0230080ade2040b77616c6c65742d636f726502037098338e0b680811" + + "9dfd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f45182" + + "3d6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a" + + "39355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a5" + + "6bbcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13" + + "833b8094ebdc030d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda5" + + "5b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff" + + "65e39ce700630095794161374b22c696dabb98e93f6ca9300b22f3b904921fbf" + + "560bb72145f4fa" + + XCTAssertEqual(output.transactionID.hexString, expectedTransactionID) + XCTAssertEqual(output.signature.hexString, expectedSignature) + XCTAssertEqual(output.signedTransactionData.hexString, expectedSignedData) + } + + func testBondWithoutPublicKeySign() { + // Successfully broadcasted transaction: + // https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80 + let input = PactusSigningInput.with { + $0.privateKey = privateKey.data + $0.transaction = PactusTransactionMessage.with { + $0.lockTime = 2335580 + $0.fee = 10000000 + $0.memo = "wallet-core" + $0.bond = PactusBondPayload.with { + $0.sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr" + $0.receiver = "pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd" + $0.stake = 1000000000 + } + } + } + + let output: PactusSigningOutput = AnySigner.sign(input: input, coin: .pactus) + + let expectedTransactionID = "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80" + let expectedSignature = "9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0" + + "715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d" + let expectedSignedData = "00015ca3230080ade2040b77616c6c65742d636f726502037098338e0b680811" + + "9dfd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278" + + "be008094ebdc039e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d" + + "85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc4" + + "36aa58f9a8f00d95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf" + + "560bb72145f4fa"; + + XCTAssertEqual(output.transactionID.hexString, expectedTransactionID) + XCTAssertEqual(output.signature.hexString, expectedSignature) + XCTAssertEqual(output.signedTransactionData.hexString, expectedSignedData) + } +} diff --git a/swift/Tests/CoinAddressDerivationTests.swift b/swift/Tests/CoinAddressDerivationTests.swift index 6a572ab6048..72c611f4083 100644 --- a/swift/Tests/CoinAddressDerivationTests.swift +++ b/swift/Tests/CoinAddressDerivationTests.swift @@ -130,7 +130,7 @@ class CoinAddressDerivationTests: XCTestCase { assertCoinDerivation(coin, expectedResult, derivedAddress, address) case .rootstock: let expectedResult = "0xA2D7065F94F838a3aB9C04D67B312056846424Df" - assertCoinDerivation(coin, expectedResult, derivedAddress, address) + assertCoinDerivation(coin, expectedResult, derivedAddress, address) case .filecoin: let expectedResult = "f1zzykebxldfcakj5wdb5n3n7priul522fnmjzori" assertCoinDerivation(coin, expectedResult, derivedAddress, address) @@ -400,6 +400,9 @@ class CoinAddressDerivationTests: XCTestCase { case .dydx: let expectedResult = "dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky" assertCoinDerivation(coin, expectedResult, derivedAddress, address) + case .pactus: + let expectedResult = "pc1r7ys2g5a4xc2qtm0t4q987m4mvs57w5g0v4pvzg" + assertCoinDerivation(coin, expectedResult, derivedAddress, address) @unknown default: fatalError() } diff --git a/tests/chains/Pactus/AddressTests.cpp b/tests/chains/Pactus/AddressTests.cpp new file mode 100644 index 00000000000..c3bcb0a260d --- /dev/null +++ b/tests/chains/Pactus/AddressTests.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "HexCoding.h" +#include "Pactus/Entry.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include "TestUtilities.h" +#include +#include + +namespace TW::Pactus::tests { + +TEST(PactusAddress, AddressData) { + auto string = STRING("pc1rspm7ps49gar9ft5g0tkl6lhxs8ygeakq87quh3"); + auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinTypePactus)); + auto string2 = WRAPS(TWAnyAddressDescription(addr.get())); + EXPECT_TRUE(TWStringEqual(string.get(), string2.get())); + auto keyHash = WRAPD(TWAnyAddressData(addr.get())); + assertHexEqual(keyHash, "038077e0c2a5474654ae887aedfd7ee681c88cf6c0"); +} + +TEST(PactusAddress, FromPrivateKey) { + auto privateKey = PrivateKey(parse_hex("2134ae97465505dfd5a1fd05a8a0f146209c601eb3f1b0363b4cfe4b47ba1ab4")); + auto pubkey = privateKey.getPublicKey(TWPublicKeyTypeED25519); + Entry entry; + auto address = entry.deriveAddress(TWCoinTypePactus, pubkey, TWDerivationDefault, std::monostate{}); + ASSERT_EQ(address, "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl"); +} + +TEST(PactusAddress, FromPublicKey) { + auto publicKey = PublicKey(parse_hex("600d30a4373ae788e2d4a08f4728f45d259593fbdd9632bbe283c4c37ac6a3df"), TWPublicKeyTypeED25519); + Entry entry; + auto address = entry.deriveAddress(TWCoinTypePactus, publicKey, TWDerivationDefault, std::monostate{}); + ASSERT_EQ(address, "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl"); +} + +} // namespace TW::Pactus::tests \ No newline at end of file diff --git a/tests/chains/Pactus/CoinTypeTests.cpp b/tests/chains/Pactus/CoinTypeTests.cpp new file mode 100644 index 00000000000..cd28ee9de14 --- /dev/null +++ b/tests/chains/Pactus/CoinTypeTests.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" +#include "Pactus/Entry.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include +#include +#include +#include + +namespace TW::Pactus::tests { + +TEST(PactusCoinType, TWCoinType) { + const auto coin = TWCoinTypePactus; + const auto symbol = WRAPS(TWCoinTypeConfigurationGetSymbol(coin)); + const auto id = WRAPS(TWCoinTypeConfigurationGetID(coin)); + const auto name = WRAPS(TWCoinTypeConfigurationGetName(coin)); + const auto txId = WRAPS(TWStringCreateWithUTF8Bytes("")); + const auto txUrl = WRAPS(TWCoinTypeConfigurationGetTransactionURL(coin, txId.get())); + const auto accId = WRAPS(TWStringCreateWithUTF8Bytes("")); + const auto accUrl = WRAPS(TWCoinTypeConfigurationGetAccountURL(coin, accId.get())); + + assertStringsEqual(id, "pactus"); + assertStringsEqual(name, "Pactus"); + assertStringsEqual(symbol, "PAC"); + ASSERT_EQ(TWCoinTypeConfigurationGetDecimals(coin), 9); + ASSERT_EQ(TWCoinTypeBlockchain(coin), TWBlockchainPactus); + ASSERT_EQ(TWCoinTypeP2pkhPrefix(coin), 0); + ASSERT_EQ(TWCoinTypeP2shPrefix(coin), 0); + ASSERT_EQ(TWCoinTypeStaticPrefix(coin), 0); + assertStringsEqual(txUrl, "https://pacviewer.com/transaction/"); + assertStringsEqual(accUrl, "https://pacviewer.com/address/"); +} + +} \ No newline at end of file diff --git a/tests/chains/Pactus/CompilerTests.cpp b/tests/chains/Pactus/CompilerTests.cpp new file mode 100644 index 00000000000..0d4a902e2d9 --- /dev/null +++ b/tests/chains/Pactus/CompilerTests.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include "TestCases.h" +#include "TestUtilities.h" +#include "TransactionCompiler.h" +#include "proto/Pactus.pb.h" +#include "proto/TransactionCompiler.pb.h" + +#include + +using namespace TW; + +TEST(PactusCompiler, CompileAndSign) { + for (const auto& testCase : TEST_CASES) { + auto input = testCase.createSigningInput(); + auto inputString = input.SerializeAsString(); + auto inputStrData = TW::Data(inputString.begin(), inputString.end()); + + // Pre-hash the transaction. + auto preImageHashesData = TransactionCompiler::preImageHashes(TWCoinTypePactus, inputStrData); + TxCompiler::Proto::PreSigningOutput preSigningOutput; + preSigningOutput.ParseFromArray(preImageHashesData.data(), static_cast(preImageHashesData.size())); + auto actualDataToSign = data(preSigningOutput.data()); + + EXPECT_EQ(preSigningOutput.error(), Common::Proto::OK); + EXPECT_EQ(hex(actualDataToSign), testCase.dataToSign); + + // Sign the pre-hash data. + auto privateKey = PrivateKey(parse_hex(PRIVATE_KEY_HEX)); + auto publicKey = privateKey.getPublicKey(TWPublicKeyTypeED25519).bytes; + auto signature = privateKey.sign(actualDataToSign, TWCurveED25519); + EXPECT_EQ(hex(signature), testCase.signature); + + // Compile the transaction. + auto outputData = TransactionCompiler::compileWithSignatures(TWCoinTypePactus, inputStrData, {signature}, {publicKey}); + TW::Pactus::Proto::SigningOutput output; + output.ParseFromArray(outputData.data(), static_cast(outputData.size())); + + EXPECT_EQ(output.error(), Common::Proto::OK); + ASSERT_EQ(hex(output.signed_transaction_data()), testCase.signedData); + ASSERT_EQ(hex(output.signature()), testCase.signature); + ASSERT_EQ(hex(output.transaction_id()), testCase.transactionID); + } +} diff --git a/tests/chains/Pactus/SignerTests.cpp b/tests/chains/Pactus/SignerTests.cpp new file mode 100644 index 00000000000..cbb469310c0 --- /dev/null +++ b/tests/chains/Pactus/SignerTests.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include "TestCases.h" +#include "TestUtilities.h" +#include "TransactionCompiler.h" +#include "proto/Pactus.pb.h" +#include "proto/TransactionCompiler.pb.h" + +#include + +using namespace TW; + +TEST(PactusSigner, Sign) { + for (const auto& testCase : TEST_CASES) { + auto input = testCase.createSigningInput(); + + auto privateKey = PrivateKey(parse_hex(PRIVATE_KEY_HEX)); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + + TW::Pactus::Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypePactus); + + EXPECT_EQ(output.error(), Common::Proto::OK); + ASSERT_EQ(hex(output.signed_transaction_data()), testCase.signedData); + ASSERT_EQ(hex(output.signature()), testCase.signature); + ASSERT_EQ(hex(output.transaction_id()), testCase.transactionID); + } +} diff --git a/tests/chains/Pactus/TestCases.h b/tests/chains/Pactus/TestCases.h new file mode 100644 index 00000000000..1fc953b54e4 --- /dev/null +++ b/tests/chains/Pactus/TestCases.h @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "proto/Pactus.pb.h" +#include "proto/TransactionCompiler.pb.h" + +#include + +const std::string PRIVATE_KEY_HEX = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"; + +namespace TransferTransaction { +// Successfully broadcasted transaction: +// https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f +static TW::Pactus::Proto::SigningInput createSigningInput() { + TW::Pactus::Proto::SigningInput input; + TW::Pactus::Proto::TransactionMessage* trx = input.mutable_transaction(); + trx->set_lock_time(2335524); + trx->set_fee(10000000); + trx->set_memo("wallet-core"); + + TW::Pactus::Proto::TransferPayload* pld = trx->mutable_transfer(); + pld->set_sender("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"); + pld->set_receiver("pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra"); + pld->set_amount(200000000); + + return input; +} + +const std::string transactionID = "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f"; +const std::string signature = "4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc" + "8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506"; +const std::string dataToSign = "0124a3230080ade2040b77616c6c65742d636f726501037098338e0b6808119d" + "fd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df21" + "8084af5f"; +const std::string signedData = "000124a3230080ade2040b77616c6c65742d636f726501037098338e0b680811" + "9dfd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df" + "218084af5f4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9" + "b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736" + "693eda850695794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560b" + "b72145f4fa"; +} // namespace TransferTransaction + +namespace BondWithPublicKeyTransaction { +// Successfully broadcasted transaction: +// https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f + +static TW::Pactus::Proto::SigningInput createSigningInput() { + TW::Pactus::Proto::SigningInput input; + TW::Pactus::Proto::TransactionMessage* trx = input.mutable_transaction(); + trx->set_lock_time(2339009); + trx->set_fee(10000000); + trx->set_memo("wallet-core"); + + TW::Pactus::Proto::BondPayload* pld = trx->mutable_bond(); + pld->set_sender("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"); + pld->set_receiver("pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl"); + pld->set_stake(1000000000); + pld->set_public_key("public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn"); + + return input; +} + +const std::string transactionID = "d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f"; +const std::string signature = "0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac7" + "9d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300"; +const std::string dataToSign = "01c1b0230080ade2040b77616c6c65742d636f726502037098338e0b6808119d" + "fd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f451823d" + "6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a39" + "355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a56b" + "bcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb1383" + "3b8094ebdc03"; +const std::string signedData = "0001c1b0230080ade2040b77616c6c65742d636f726502037098338e0b680811" + "9dfd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f45182" + "3d6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a" + "39355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a5" + "6bbcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13" + "833b8094ebdc030d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda5" + "5b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff" + "65e39ce700630095794161374b22c696dabb98e93f6ca9300b22f3b904921fbf" + "560bb72145f4fa"; +} // namespace BondWithPublicKeyTransaction + +namespace BondWithoutPublicKeyTransaction { +// Successfully broadcasted transaction: +// https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80 + +static TW::Pactus::Proto::SigningInput createSigningInput() { + TW::Pactus::Proto::SigningInput input; + TW::Pactus::Proto::TransactionMessage* trx = input.mutable_transaction(); + trx->set_lock_time(2335580); + trx->set_fee(10000000); + trx->set_memo("wallet-core"); + + TW::Pactus::Proto::BondPayload* pld = trx->mutable_bond(); + pld->set_sender("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"); + pld->set_receiver("pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd"); + pld->set_stake(1000000000); + + return input; +} + +const std::string transactionID = "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80"; +const std::string signature = "9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0" + "715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d"; +const std::string dataToSign = "015ca3230080ade2040b77616c6c65742d636f726502037098338e0b6808119d" + "fd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278be" + "008094ebdc03"; +const std::string signedData = "00015ca3230080ade2040b77616c6c65742d636f726502037098338e0b680811" + "9dfd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278" + "be008094ebdc039e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d" + "85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc4" + "36aa58f9a8f00d95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf" + "560bb72145f4fa"; + +} // namespace BondWithoutPublicKeyTransaction + +struct TestCase { + std::function createSigningInput; + std::string transactionID; + std::string signature; + std::string dataToSign; + std::string signedData; +}; + +const TestCase TEST_CASES[] = { + { + TransferTransaction::createSigningInput, + TransferTransaction::transactionID, + TransferTransaction::signature, + TransferTransaction::dataToSign, + TransferTransaction::signedData, + }, + { + BondWithPublicKeyTransaction::createSigningInput, + BondWithPublicKeyTransaction::transactionID, + BondWithPublicKeyTransaction::signature, + BondWithPublicKeyTransaction::dataToSign, + BondWithPublicKeyTransaction::signedData, + }, + { + BondWithoutPublicKeyTransaction::createSigningInput, + BondWithoutPublicKeyTransaction::transactionID, + BondWithoutPublicKeyTransaction::signature, + BondWithoutPublicKeyTransaction::dataToSign, + BondWithoutPublicKeyTransaction::signedData, + }, +}; \ No newline at end of file diff --git a/tests/chains/Pactus/WalletTests.cpp b/tests/chains/Pactus/WalletTests.cpp new file mode 100644 index 00000000000..bcd3076f398 --- /dev/null +++ b/tests/chains/Pactus/WalletTests.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" +#include "Pactus/Entry.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include +#include +#include +#include + +namespace TW::Pactus::tests { + +TEST(PactusWallet, DerivationPath) { + auto derivationPath = TWCoinTypeDerivationPath(TWCoinTypePactus); + assertStringsEqual(WRAPS(derivationPath), "m/44'/21888'/3'/0'"); +} + +TEST(PactusWallet, HDWallet) { + auto mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon cactus"; + auto passphrase = ""; + auto wallet = WRAP(TWHDWallet, TWHDWalletCreateWithMnemonic(STRING(mnemonic).get(), STRING(passphrase).get())); + + auto derivationPath1 = TWStringCreateWithUTF8Bytes("m/44'/21888'/3'/0'"); + auto privateKey1 = WRAP(TWPrivateKey, TWHDWalletGetKey(wallet.get(), TWCoinTypePactus, derivationPath1)); + auto publicKey1 = WRAP(TWPublicKey, TWPrivateKeyGetPublicKeyEd25519(privateKey1.get())); + auto address1 = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey1.get(), TWCoinTypePactus)); + auto addressStr1 = WRAPS(TWAnyAddressDescription(address1.get())); + + auto derivationPath2 = TWStringCreateWithUTF8Bytes("m/44'/21888'/3'/1'"); + auto privateKey2 = WRAP(TWPrivateKey, TWHDWalletGetKey(wallet.get(), TWCoinTypePactus, derivationPath2)); + auto publicKey2 = WRAP(TWPublicKey, TWPrivateKeyGetPublicKeyEd25519(privateKey2.get())); + auto address2 = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey2.get(), TWCoinTypePactus)); + auto addressStr2 = WRAPS(TWAnyAddressDescription(address2.get())); + + assertStringsEqual(addressStr1, "pc1rcx9x55nfme5juwdgxd2ksjdcmhvmvkrygmxpa3"); + assertStringsEqual(addressStr2, "pc1r7aynw9urvh66ktr3fte2gskjjnxzruflkgde94"); + TWStringDelete(derivationPath1); + TWStringDelete(derivationPath2); +} + +} \ No newline at end of file diff --git a/tests/common/CoinAddressDerivationTests.cpp b/tests/common/CoinAddressDerivationTests.cpp index 1273ea427eb..6df5336c5c0 100644 --- a/tests/common/CoinAddressDerivationTests.cpp +++ b/tests/common/CoinAddressDerivationTests.cpp @@ -396,6 +396,9 @@ TEST(Coin, DeriveAddress) { case TWCoinTypeDydx: EXPECT_EQ(address, "dydx1hkfq3zahaqkkzx5mjnamwjsfpq2jk7z0sz38vk"); break; + case TWCoinTypePactus: + EXPECT_EQ(address, "pc1rehvlc6tfn79z0zjqqaj8zas5j5h9c2fe59a4ff"); + break; // end_of_coin_address_derivation_tests_marker_do_not_modify // no default branch here, intentionally, to better notice any missing coins } diff --git a/tests/interface/TWHRPTests.cpp b/tests/interface/TWHRPTests.cpp index 14ddb39b657..d99e81a6bb8 100644 --- a/tests/interface/TWHRPTests.cpp +++ b/tests/interface/TWHRPTests.cpp @@ -34,6 +34,7 @@ TEST(TWHRP, StringForHRP) { ASSERT_STREQ(stringForHRP(TWHRPCryptoOrg), "cro"); ASSERT_STREQ(stringForHRP(TWHRPOsmosis), "osmo"); ASSERT_STREQ(stringForHRP(TWHRPSecret), "secret"); + ASSERT_STREQ(stringForHRP(TWHRPPactus), "pc"); } TEST(TWHRP, HRPForString) { @@ -62,6 +63,7 @@ TEST(TWHRP, HRPForString) { ASSERT_EQ(hrpForString("osmo"), TWHRPOsmosis); ASSERT_EQ(hrpForString("ecash"), TWHRPECash); ASSERT_EQ(hrpForString("secret"), TWHRPSecret); + ASSERT_EQ(hrpForString("pc"), TWHRPPactus); } TEST(TWHPR, HPRByCoinType) { @@ -89,6 +91,7 @@ TEST(TWHPR, HPRByCoinType) { ASSERT_EQ(TWHRPOsmosis, TWCoinTypeHRP(TWCoinTypeOsmosis)); ASSERT_EQ(TWHRPECash, TWCoinTypeHRP(TWCoinTypeECash)); ASSERT_EQ(TWHRPSecret, TWCoinTypeHRP(TWCoinTypeSecret)); + ASSERT_EQ(TWHRPPactus, TWCoinTypeHRP(TWCoinTypePactus)); ASSERT_EQ(TWHRPUnknown, TWCoinTypeHRP(TWCoinTypeAion)); ASSERT_EQ(TWHRPUnknown, TWCoinTypeHRP(TWCoinTypeCallisto)); From 3772536bd3de5cf3a8b412f598823fc6ea5285fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20=22Doom=22=20Doumergue?= Date: Wed, 13 Nov 2024 20:18:38 +0100 Subject: [PATCH 2/6] Update Polkadot extrinsic encoding to support new spec for Acala (#4107) * Update Polkadot extrinsic encoding to support new spec for Acala * Make Test Fail * Fix Acala test --------- Co-authored-by: jaimeToca --- src/Polkadot/Extrinsic.cpp | 2 +- tests/chains/Acala/TWAnySignerTests.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Polkadot/Extrinsic.cpp b/src/Polkadot/Extrinsic.cpp index b88e5e9a843..dcd0b31a308 100644 --- a/src/Polkadot/Extrinsic.cpp +++ b/src/Polkadot/Extrinsic.cpp @@ -390,7 +390,7 @@ Data Extrinsic::encodeIdentityAddAuthorization(const Proto::Identity::AddAuthori static bool requires_new_spec_compatbility(uint32_t network, uint32_t specVersion) noexcept { // version 1002005 introduces a breaking change for Polkadot and Kusama - return ((network == 0 || network == 2) && specVersion >= 1002005); + return ((network == 0 || network == 2) && specVersion >= 1002005) || (network == 10 && specVersion >= 2270); } Data Extrinsic::encodePayload() const { diff --git a/tests/chains/Acala/TWAnySignerTests.cpp b/tests/chains/Acala/TWAnySignerTests.cpp index c69ada618f0..cf8cb5f1641 100644 --- a/tests/chains/Acala/TWAnySignerTests.cpp +++ b/tests/chains/Acala/TWAnySignerTests.cpp @@ -26,7 +26,7 @@ TEST(TWAnySignerAcala, Sign) { input.set_block_hash(blockHash.data(), blockHash.size()); input.set_nonce(0); - input.set_spec_version(2170); + input.set_spec_version(2270); input.set_private_key(secret.data(), secret.size()); input.set_network(10); // Acala input.set_transaction_version(2); @@ -48,12 +48,12 @@ TEST(TWAnySignerAcala, Sign) { auto extrinsic = Extrinsic(input); auto preimage = extrinsic.encodePayload(); - EXPECT_EQ(hex(preimage), "0a0000c8c602ded977c56076ae38d98026fa669ca10d6a2b5a0bfc4086ae7668ed1c60070010a5d4e8d50200007a08000002000000fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c707ffa05b7dc6cdb6356bd8bd51ff20b2757c3214a76277516080a10f1bc7537"); + EXPECT_EQ(hex(preimage), "0a0000c8c602ded977c56076ae38d98026fa669ca10d6a2b5a0bfc4086ae7668ed1c60070010a5d4e8d502000000de08000002000000fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c707ffa05b7dc6cdb6356bd8bd51ff20b2757c3214a76277516080a10f1bc753700"); Proto::SigningOutput output; ANY_SIGN(input, TWCoinTypePolkadot); - EXPECT_EQ(hex(output.encoded()), "41028400e9590e4d99264a14a85e21e69537e4a64f66a875d38cb8f76b305f41fabe24a900dd54466dffd1e3c80b76013e9459fbdcd17805bd5fdbca0961a643bad1cbd2b7fe005c62c51c18b67f31eb9e61b187a911952fee172ef18402d07c703eec3100d50200000a0000c8c602ded977c56076ae38d98026fa669ca10d6a2b5a0bfc4086ae7668ed1c60070010a5d4e8"); + EXPECT_EQ(hex(output.encoded()), "45028400e9590e4d99264a14a85e21e69537e4a64f66a875d38cb8f76b305f41fabe24a900a9c3111fb98507f929e4da9aea30f996c69d2790e5a1e789f91634dc5d4f6afb155e0f1ea623498c04778f06dbc698109c3490c3e6b4c33d8e58ebab82a0f40bd5020000000a0000c8c602ded977c56076ae38d98026fa669ca10d6a2b5a0bfc4086ae7668ed1c60070010a5d4e8"); } } // namespace TW::Polkadot::tests From 45124dd5881d9bf8ccb36dfbeb8f41a2b0d46139 Mon Sep 17 00:00:00 2001 From: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> Date: Tue, 19 Nov 2024 01:57:35 +0700 Subject: [PATCH 3/6] [Nimiq]: Add support for Nimiq PoS Albatross Mainnet (#4115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for Nimiq Albatross PoS transaction serialization (#4110) * Update Nimiq Transaction encoding for Albatross PoS * Update tests * [Nimiq]: Add networkId parameter to SigningInput * [Nimiq]: Fix iOS test * [Nimiq]: Fix invalid integer type cast * [Nimiq]: Fix iOS test --------- Co-authored-by: Sören Schwert --- src/Nimiq/Signer.cpp | 3 +- src/Nimiq/Transaction.cpp | 36 ++++++++++++++++++++++-- src/Nimiq/Transaction.h | 11 ++++++-- src/proto/Nimiq.proto | 11 ++++++++ swift/Tests/Blockchains/NimiqTests.swift | 3 +- tests/chains/Nimiq/SignerTests.cpp | 3 +- tests/chains/Nimiq/TWAnySignerTests.cpp | 18 ++++++++++++ tests/chains/Nimiq/TransactionTests.cpp | 6 ++-- 8 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/Nimiq/Signer.cpp b/src/Nimiq/Signer.cpp index ed08cefdff3..e8de7375c62 100644 --- a/src/Nimiq/Signer.cpp +++ b/src/Nimiq/Signer.cpp @@ -19,7 +19,8 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput& input) noexcept { /* destination */Address(input.destination()), /* amount */input.value(), /* fee */input.fee(), - /* vsh */input.validity_start_height() + /* vsh */input.validity_start_height(), + /* networkId */input.network_id() ); auto signer = Signer(); diff --git a/src/Nimiq/Transaction.cpp b/src/Nimiq/Transaction.cpp index e5ba42006fe..9617cd2fcbb 100644 --- a/src/Nimiq/Transaction.cpp +++ b/src/Nimiq/Transaction.cpp @@ -9,25 +9,33 @@ namespace TW::Nimiq { -const uint8_t NETWORK_ID = 42; const uint8_t EMPTY_FLAGS = 0; std::vector Transaction::serialize() const { + // Source code: + // https://github.com/nimiq/core-rs-albatross/blame/b8ed402c9096ffb54afea52347b91ab7831e75de/primitives/transaction/src/lib.rs#L699 + std::vector data; data.push_back(0x00); // Basic TX type + if (isAlbatross()) { + data.push_back(0x00); // Signature Proof type and flags (Ed25519 type and no flags) + } data.insert(data.end(), sender_pub_key.begin(), sender_pub_key.end()); data.insert(data.end(), destination.bytes.begin(), destination.bytes.end()); encode64BE(amount, data); encode64BE(fee, data); encode32BE(vsh, data); - data.push_back(NETWORK_ID); + data.push_back(consensusNetworkId()); data.insert(data.end(), signature.begin(), signature.end()); return data; } std::vector Transaction::getPreImage() const { + // Source code: + // https://github.com/nimiq/core-rs-albatross/blame/b8ed402c9096ffb54afea52347b91ab7831e75de/primitives/transaction/src/lib.rs#L582 + std::vector data; // Build pre-image @@ -40,10 +48,32 @@ std::vector Transaction::getPreImage() const { encode64BE(amount, data); encode64BE(fee, data); encode32BE(vsh, data); - data.push_back(NETWORK_ID); + data.push_back(consensusNetworkId()); data.push_back(EMPTY_FLAGS); + if (isAlbatross()) { + data.push_back(0x00); // Sender Data size (+ 0 bytes of data) + } return data; } +bool Transaction::isAlbatross() const { + if (networkId == Proto::NetworkId::MainnetAlbatross) { + return true; + } + return false; +} + +uint8_t Transaction::consensusNetworkId() const { + switch (networkId) { + case Proto::NetworkId::UseDefault: + case Proto::NetworkId::Mainnet: + return static_cast(Proto::NetworkId::Mainnet); + case Proto::NetworkId::MainnetAlbatross: + return static_cast(Proto::NetworkId::MainnetAlbatross); + default: + throw std::invalid_argument("Invalid network ID"); + } +} + } // namespace TW::Nimiq diff --git a/src/Nimiq/Transaction.h b/src/Nimiq/Transaction.h index 4615d440537..98ea799387d 100644 --- a/src/Nimiq/Transaction.h +++ b/src/Nimiq/Transaction.h @@ -5,6 +5,7 @@ #pragma once #include "Address.h" +#include "proto/Nimiq.pb.h" namespace TW::Nimiq { @@ -20,16 +21,22 @@ class Transaction { uint64_t fee; // Validity start (block) height uint32_t vsh; + // Network ID + Proto::NetworkId networkId; // Sender signature std::array signature; Transaction(const std::array& sender, const Address& dest, uint64_t amount, - uint64_t fee, uint32_t vsh) - : sender_pub_key(sender), destination(dest), amount(amount), fee(fee), vsh(vsh) {} + uint64_t fee, uint32_t vsh, Proto::NetworkId networkId) + : sender_pub_key(sender), destination(dest), amount(amount), fee(fee), vsh(vsh), networkId(networkId) {} public: std::vector serialize() const; std::vector getPreImage() const; + + private: + bool isAlbatross() const; + uint8_t consensusNetworkId() const; }; } // namespace TW::Nimiq diff --git a/src/proto/Nimiq.proto b/src/proto/Nimiq.proto index 76d00dd903e..13abbd9c734 100644 --- a/src/proto/Nimiq.proto +++ b/src/proto/Nimiq.proto @@ -3,6 +3,14 @@ syntax = "proto3"; package TW.Nimiq.Proto; option java_package = "wallet.core.jni.proto"; +enum NetworkId { + UseDefault = 0; + // Default PoW Mainnet. + Mainnet = 42; + // PoS Mainnet starting at the PoW block height 3’456’000. + MainnetAlbatross = 24; +} + // Input data necessary to create a signed transaction. message SigningInput { // The secret private key used for signing (32 bytes). @@ -19,6 +27,9 @@ message SigningInput { // Validity start, in block height uint32 validity_start_height = 5; + + // Network ID. + NetworkId network_id = 6; } // Result containing the signed and encoded transaction. diff --git a/swift/Tests/Blockchains/NimiqTests.swift b/swift/Tests/Blockchains/NimiqTests.swift index 8ee8dac51ed..d96ad33e5ce 100644 --- a/swift/Tests/Blockchains/NimiqTests.swift +++ b/swift/Tests/Blockchains/NimiqTests.swift @@ -14,10 +14,11 @@ class NimiqTests: XCTestCase { $0.value = 42042042 $0.fee = 1000 $0.validityStartHeight = 314159 + $0.networkID = .mainnetAlbatross } let output: NimiqSigningOutput = AnySigner.sign(input: input, coin: .nimiq) - XCTAssertEqual(output.encoded.hexString, "0070c7492aaa9c9ac7a05bc0d9c5db2dae9372029654f71f0c7f95deed5099b7021450ffc385cd4e7c6ac9a7e91614ca67ff90568a00000000028182ba00000000000003e80004cb2f2a74dc7f6e0ab58a0bf52cc6e8801b0cca132dd4229d9a3e3a3d2f90e4d8f045d981b771bf5fc3851a98f3c617b1a943228f963e910e061808a721cfa0e3cad50b") + XCTAssertEqual(output.encoded.hexString, "000070c7492aaa9c9ac7a05bc0d9c5db2dae9372029654f71f0c7f95deed5099b7021450ffc385cd4e7c6ac9a7e91614ca67ff90568a00000000028182ba00000000000003e80004cb2f180ba678744be3bf9cd44fbcdabfb5be209f21739934836e26055610ab02720fa99489219d9f3581664473a1b40b30ad1f6e13150d59f8234a42c3f0de3d505405") } } diff --git a/tests/chains/Nimiq/SignerTests.cpp b/tests/chains/Nimiq/SignerTests.cpp index c22441f2f39..d8a56bdb9cd 100644 --- a/tests/chains/Nimiq/SignerTests.cpp +++ b/tests/chains/Nimiq/SignerTests.cpp @@ -30,7 +30,8 @@ TEST(NimiqSigner, Sign) { Address("NQ86 2H8F YGU5 RM77 QSN9 LYLH C56A CYYR 0MLA"), 42042042, 1000, - 314159 + 314159, + Proto::NetworkId::Mainnet ); Signer signer; diff --git a/tests/chains/Nimiq/TWAnySignerTests.cpp b/tests/chains/Nimiq/TWAnySignerTests.cpp index a618b1ac04f..f8879960cd1 100644 --- a/tests/chains/Nimiq/TWAnySignerTests.cpp +++ b/tests/chains/Nimiq/TWAnySignerTests.cpp @@ -28,4 +28,22 @@ TEST(TWAnySignerNimiq, Sign) { EXPECT_EQ(hex(output.encoded()), "0070c7492aaa9c9ac7a05bc0d9c5db2dae9372029654f71f0c7f95deed5099b7021450ffc385cd4e7c6ac9a7e91614ca67ff90568a00000000028182ba00000000000003e80004cb2f2a74dc7f6e0ab58a0bf52cc6e8801b0cca132dd4229d9a3e3a3d2f90e4d8f045d981b771bf5fc3851a98f3c617b1a943228f963e910e061808a721cfa0e3cad50b"); } +TEST(TWAnySignerNimiq, SignPoS) { + auto privateKey = parse_hex("e3cc33575834add098f8487123cd4bca543ee859b3e8cfe624e7e6a97202b756"); + + Proto::SigningInput input; + + input.set_destination("NQ86 2H8F YGU5 RM77 QSN9 LYLH C56A CYYR 0MLA"); + input.set_fee(1000); + input.set_value(42042042); + input.set_validity_start_height(314159); + input.set_private_key(privateKey.data(), privateKey.size()); + input.set_network_id(Proto::NetworkId::MainnetAlbatross); + + Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypeNimiq); + + EXPECT_EQ(hex(output.encoded()), "000070c7492aaa9c9ac7a05bc0d9c5db2dae9372029654f71f0c7f95deed5099b7021450ffc385cd4e7c6ac9a7e91614ca67ff90568a00000000028182ba00000000000003e80004cb2f180ba678744be3bf9cd44fbcdabfb5be209f21739934836e26055610ab02720fa99489219d9f3581664473a1b40b30ad1f6e13150d59f8234a42c3f0de3d505405"); +} + } // namespace TW::Nimiq::tests diff --git a/tests/chains/Nimiq/TransactionTests.cpp b/tests/chains/Nimiq/TransactionTests.cpp index 679f955f4ca..cd6eaeb2b22 100644 --- a/tests/chains/Nimiq/TransactionTests.cpp +++ b/tests/chains/Nimiq/TransactionTests.cpp @@ -22,7 +22,8 @@ TEST(NimiqTransaction, PreImage) { Address("NQ86 2H8F YGU5 RM77 QSN9 LYLH C56A CYYR 0MLA"), 42042042, 1000, - 314159 + 314159, + Proto::NetworkId::UseDefault ); ASSERT_EQ(hex(tx.getPreImage()), "000082d5f776378ccbe34a3d941f22d4715bc9f81e0d001450ffc385cd4e7c6ac9a7e91614ca67ff90568a0000000000028182ba00000000000003e80004cb2f2a00"); @@ -39,7 +40,8 @@ TEST(NimiqTransaction, Serialize) { Address("NQ86 2H8F YGU5 RM77 QSN9 LYLH C56A CYYR 0MLA"), 42042042, 1000, - 314159 + 314159, + Proto::NetworkId::Mainnet ); const auto signature = parse_hex("74dc7f6e0ab58a0bf52cc6e8801b0cca132dd4229d9a3e3a3d2f90e4d8f045d981b771bf5fc3851a98f3c617b1a943228f963e910e061808a721cfa0e3cad50b"); From e34e9ba2ed6e571e2b0531a5cfe1495cea5258f7 Mon Sep 17 00:00:00 2001 From: Sergei Boiko <127754187+satoshiotomakan@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:09:52 +0700 Subject: [PATCH 4/6] [BCH]: Fix Transaction Planning (#4118) * [BCH]: Fix Transaction Planning * [BCH]: Add BitcoinCash transaction signing V2 test in C++ --- rust/chains/tw_bitcoincash/src/context.rs | 1 + rust/chains/tw_bitcoincash/src/entry.rs | 10 +++ .../chains/bitcoincash/bitcoincash_sign.rs | 13 +++- .../chains/solana/solana_transaction_ffi.rs | 7 +- .../chains/BitcoinCash/TWBitcoinCashTests.cpp | 71 +++++++++++++++++++ 5 files changed, 96 insertions(+), 6 deletions(-) diff --git a/rust/chains/tw_bitcoincash/src/context.rs b/rust/chains/tw_bitcoincash/src/context.rs index e4eb7b2036d..9190ff463e6 100644 --- a/rust/chains/tw_bitcoincash/src/context.rs +++ b/rust/chains/tw_bitcoincash/src/context.rs @@ -7,6 +7,7 @@ use tw_coin_entry::error::prelude::*; use tw_utxo::context::{AddressPrefixes, UtxoContext}; use tw_utxo::script::Script; +#[derive(Default)] pub struct BitcoinCashContext; impl UtxoContext for BitcoinCashContext { diff --git a/rust/chains/tw_bitcoincash/src/entry.rs b/rust/chains/tw_bitcoincash/src/entry.rs index bbe4625aa7d..92dee76a81c 100644 --- a/rust/chains/tw_bitcoincash/src/entry.rs +++ b/rust/chains/tw_bitcoincash/src/entry.rs @@ -92,4 +92,14 @@ impl CoinEntry for BitcoinCashEntry { ) -> Self::SigningOutput { BitcoinCompiler::::compile(coin, input, signatures, public_keys) } + + #[inline] + fn plan_builder(&self) -> Option { + Some(BitcoinPlanner::::default()) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(BitcoinTransactionUtil) + } } diff --git a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs index 8802ca9963d..0282966d3c6 100644 --- a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs +++ b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::chains::bitcoincash::test_cases::transfer_96ee20; -use crate::chains::common::bitcoin::{btc_info, sign, TransactionOneof}; +use crate::chains::common::bitcoin::{btc_info, plan, sign, TransactionOneof}; use tw_coin_registry::coin_type::CoinType; use tw_encoding::hex::DecodeHex; use tw_proto::BitcoinV2::Proto; @@ -20,6 +20,16 @@ fn test_bitcoincash_sign_input_p2pkh_from_to_address() { ..Default::default() }; + plan::BitcoinPlanHelper::new(&signing) + .coin(CoinType::BitcoinCash) + .plan(plan::Expected { + inputs: vec![5151], + outputs: vec![600, 4325], + vsize_estimate: 227, + fee_estimate: 226, + change: 0, + }); + // Successfully broadcasted: // https://blockchair.com/bitcoin-cash/transaction/96ee20002b34e468f9d3c5ee54f6a8ddaa61c118889c4f35395c2cd93ba5bbb4 sign::BitcoinSignHelper::new(&signing) @@ -29,7 +39,6 @@ fn test_bitcoincash_sign_input_p2pkh_from_to_address() { txid: transfer_96ee20::TX_ID, inputs: vec![5151], outputs: vec![600, 4325], - // `vsize` is different from the estimated value due to the signatures der serialization. vsize: 226, weight: 904, fee: 226, diff --git a/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs b/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs index c9bdb0a5e4b..a9a31f80da0 100644 --- a/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs +++ b/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs @@ -2,18 +2,17 @@ // // Copyright © 2017 Trust Wallet. -use tw_any_coin::test_utils::address_utils::test_address_derive; -use tw_any_coin::test_utils::sign_utils::{AnySignerHelper, PreImageHelper}; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; use tw_any_coin::test_utils::transaction_decode_utils::TransactionDecoderHelper; use tw_coin_registry::coin_type::CoinType; use tw_encoding::base64::STANDARD; -use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_encoding::hex::DecodeHex; use tw_encoding::{base58, base64}; use tw_memory::test_utils::tw_data_helper::TWDataHelper; use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper; use tw_memory::test_utils::tw_string_helper::TWStringHelper; use tw_proto::Common::Proto::SigningError; -use tw_proto::Solana::Proto::{self, mod_SigningInput::OneOftransaction_type as TransactionType}; +use tw_proto::Solana::Proto::{self}; use tw_solana::SOLANA_ALPHABET; use wallet_core_rs::ffi::solana::transaction::{ tw_solana_transaction_get_compute_unit_limit, tw_solana_transaction_get_compute_unit_price, diff --git a/tests/chains/BitcoinCash/TWBitcoinCashTests.cpp b/tests/chains/BitcoinCash/TWBitcoinCashTests.cpp index 734aa7d541c..dc77f5b5642 100644 --- a/tests/chains/BitcoinCash/TWBitcoinCashTests.cpp +++ b/tests/chains/BitcoinCash/TWBitcoinCashTests.cpp @@ -6,6 +6,7 @@ #include "Bitcoin/SigHashType.h" #include "HexCoding.h" #include "proto/Bitcoin.pb.h" +#include "proto/BitcoinV2.pb.h" #include "TestUtilities.h" #include @@ -161,6 +162,76 @@ TEST(BitcoinCash, SignTransaction) { "e510000000000000" "1976a9149e089b6889e032d46e3b915a3392edfd616fb1c488ac" "00000000"); } + +TEST(BitcoinCash, SignTransactionV2) { + auto privateKey = parse_hex("7fdafb9db5bc501f2096e7d13d331dc7a75d9594af3d251313ba8b6200f4e384"); + auto txId = parse_hex("050d00e2e18ef13969606f1ceee290d3f49bd940684ce39898159352952b8ce2"); + std::reverse(txId.begin(), txId.end()); + + BitcoinV2::Proto::SigningInput signing; + signing.add_private_keys(privateKey.data(), privateKey.size()); + signing.mutable_chain_info()->set_p2pkh_prefix(0); + signing.mutable_chain_info()->set_p2sh_prefix(5); + signing.mutable_chain_info()->set_hrp("bitcoincash"); + + auto& builder = *signing.mutable_builder(); + builder.set_version(BitcoinV2::Proto::TransactionVersion::V1); + builder.set_input_selector(BitcoinV2::Proto::InputSelector::UseAll); + builder.set_fixed_dust_threshold(546); + + auto& in = *builder.add_inputs(); + auto& inOutPoint = *in.mutable_out_point(); + inOutPoint.set_hash(txId.data(), txId.size()); + inOutPoint.set_vout(2); + in.set_value(5151); + // Cash address without prefix. + in.set_receiver_address("qzhlrcrcne07x94h99thved2pgzdtv8ccujjy73xya"); + in.set_sighash_type(TWBitcoinSigHashTypeAll | TWBitcoinSigHashTypeFork); + + auto& out0 = *builder.add_outputs(); + out0.set_value(600); + // Legacy address. + out0.set_to_address("1Bp9U1ogV3A14FMvKbRJms7ctyso4Z4Tcx"); + + auto& explicitChangeOutput = *builder.add_outputs(); + explicitChangeOutput.set_value(4325); + // Cash address with an explicit prefix. + explicitChangeOutput.set_to_address("bitcoincash:qz0q3xmg38sr94rw8wg45vujah7kzma3cskxymnw06"); + + Proto::SigningInput legacy; + *legacy.mutable_signing_v2() = signing; + legacy.set_coin_type(TWCoinTypeBitcoinCash); + + Proto::TransactionPlan plan; + ANY_PLAN(legacy, plan, TWCoinTypeBitcoin); + + ASSERT_EQ(plan.error(), Common::Proto::SigningError::OK); + const auto planV2 = plan.planning_result_v2(); + EXPECT_EQ(planV2.error(), Common::Proto::SigningError::OK) << planV2.error_message(); + + EXPECT_EQ(planV2.inputs_size(), 1); + EXPECT_EQ(planV2.outputs_size(), 2); + EXPECT_EQ(planV2.vsize_estimate(), 227); + EXPECT_EQ(planV2.fee_estimate(), 226); + EXPECT_EQ(planV2.change(), 0); + + Proto::SigningOutput output; + ANY_SIGN(legacy, TWCoinTypeBitcoin); + + EXPECT_EQ(output.error(), Common::Proto::OK); + ASSERT_TRUE(output.has_signing_result_v2()); + const auto outputV2 = output.signing_result_v2(); + EXPECT_EQ(outputV2.error(), Common::Proto::SigningError::OK) << outputV2.error_message(); + ASSERT_EQ(hex(outputV2.encoded()), + "01000000" + "01" + "e28c2b955293159898e34c6840d99bf4d390e2ee1c6f606939f18ee1e2000d05" "02000000" "6b483045022100b70d158b43cbcded60e6977e93f9a84966bc0cec6f2dfd1463d1223a90563f0d02207548d081069de570a494d0967ba388ff02641d91cadb060587ead95a98d4e3534121038eab72ec78e639d02758e7860cdec018b49498c307791f785aa3019622f4ea5b" "ffffffff" + "02" + "5802000000000000" "1976a914769bdff96a02f9135a1d19b749db6a78fe07dc9088ac" + "e510000000000000" "1976a9149e089b6889e032d46e3b915a3392edfd616fb1c488ac" + "00000000"); +} + // clang-format on } // namespace TW::Bitcoin::tests From 6dc2c62b7340b186cdf1fc3abb7d8d1e096b101f Mon Sep 17 00:00:00 2001 From: 10gic Date: Fri, 22 Nov 2024 17:52:49 +0800 Subject: [PATCH 5/6] Export bech32 and bech32m functions (#4101) * Export bech32 and bech32m functions * Fix test cases * Merge bech32m functions into TWBech32 struct * Fix failed test cases * Fix failed swift test case --- .../trustwallet/core/app/utils/TestBech32.kt | 49 ++++++++++++ codegen-v2/manifest/TWBech32.yaml | 78 +++++++++++++++++++ include/TrustWalletCore/TWBase58.h | 4 +- include/TrustWalletCore/TWBech32.h | 47 +++++++++++ src/Bech32.cpp | 4 +- src/interface/TWBech32.cpp | 60 ++++++++++++++ swift/Tests/Bech32Tests.swift | 42 ++++++++++ tests/interface/TWBech32Tests.cpp | 49 ++++++++++++ wasm/tests/Bech32.test.ts | 47 +++++++++++ 9 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt create mode 100644 codegen-v2/manifest/TWBech32.yaml create mode 100644 include/TrustWalletCore/TWBech32.h create mode 100644 src/interface/TWBech32.cpp create mode 100644 swift/Tests/Bech32Tests.swift create mode 100644 tests/interface/TWBech32Tests.cpp create mode 100644 wasm/tests/Bech32.test.ts diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt new file mode 100644 index 00000000000..64b32728c5c --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt @@ -0,0 +1,49 @@ +package com.trustwallet.core.app.utils + +import org.junit.Assert.* +import org.junit.Test +import wallet.core.jni.Bech32 + +class TestBech32 { + init { + System.loadLibrary("TrustWalletCore"); + } + + @Test + fun testEncode() { + val data = Numeric.hexStringToByteArray("00443214c74254b635cf84653a56d7c675be77df") + assertEquals(Bech32.encode("abcdef", data), "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + } + + @Test + fun testDecode() { + val decoded = Bech32.decode("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + assertEquals(Numeric.toHexString(decoded), "0x00443214c74254b635cf84653a56d7c675be77df") + } + + @Test + fun testDecodeWrongChecksumVariant() { + // This is a Bech32m variant, not Bech32 variant. So it should fail using Bech32 decoder. + val decoded = Bech32.decode("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + assertNull(decoded) + } + + @Test + fun testEncodeM() { + val data = Numeric.hexStringToByteArray("ffbbcdeb38bdab49ca307b9ac5a928398a418820") + assertEquals(Bech32.encodeM("abcdef", data), "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + } + + @Test + fun testDecodeM() { + val decoded = Bech32.decodeM("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + assertEquals(Numeric.toHexString(decoded), "0xffbbcdeb38bdab49ca307b9ac5a928398a418820") + } + + @Test + fun testDecodeMWrongChecksumVariant() { + // This is a Bech32 variant, not Bech32m variant. So it should fail using Bech32M decoder. + val decoded = Bech32.decodeM("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + assertNull(decoded) + } +} diff --git a/codegen-v2/manifest/TWBech32.yaml b/codegen-v2/manifest/TWBech32.yaml new file mode 100644 index 00000000000..1d192712adb --- /dev/null +++ b/codegen-v2/manifest/TWBech32.yaml @@ -0,0 +1,78 @@ +name: TWBech32 +structs: +- name: TWBech32 + is_public: true + is_class: false +functions: +- name: TWBech32Encode + is_public: true + is_static: true + params: + - name: hrp + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + - name: data + type: + variant: data + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true +- name: TWBech32Decode + is_public: true + is_static: true + params: + - name: string + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: data + is_constant: true + is_nullable: true + is_pointer: true +- name: TWBech32EncodeM + is_public: true + is_static: true + params: + - name: hrp + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + - name: data + type: + variant: data + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true +- name: TWBech32DecodeM + is_public: true + is_static: true + params: + - name: string + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: data + is_constant: true + is_nullable: true + is_pointer: true diff --git a/include/TrustWalletCore/TWBase58.h b/include/TrustWalletCore/TWBase58.h index c59ddd2a2a0..a35ae178865 100644 --- a/include/TrustWalletCore/TWBase58.h +++ b/include/TrustWalletCore/TWBase58.h @@ -31,14 +31,14 @@ TWString *_Nonnull TWBase58EncodeNoCheck(TWData *_Nonnull data); /// Decodes a Base58 string, checking the checksum. Returns null if the string is not a valid Base58 string. /// /// \param string The Base58 string to decode. -/// \return the decoded data, empty if the string is not a valid Base58 string with checksum. +/// \return the decoded data, null if the string is not a valid Base58 string with checksum. TW_EXPORT_STATIC_METHOD TWData *_Nullable TWBase58Decode(TWString *_Nonnull string); /// Decodes a Base58 string, w/o checking the checksum. Returns null if the string is not a valid Base58 string. /// /// \param string The Base58 string to decode. -/// \return the decoded data, empty if the string is not a valid Base58 string without checksum. +/// \return the decoded data, null if the string is not a valid Base58 string without checksum. TW_EXPORT_STATIC_METHOD TWData *_Nullable TWBase58DecodeNoCheck(TWString *_Nonnull string); diff --git a/include/TrustWalletCore/TWBech32.h b/include/TrustWalletCore/TWBech32.h new file mode 100644 index 00000000000..53c0ab8973b --- /dev/null +++ b/include/TrustWalletCore/TWBech32.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Bech32 encode / decode functions +TW_EXPORT_STRUCT +struct TWBech32; + +/// Encodes data as a Bech32 string. +/// +/// \param hrp The human-readable part. +/// \param data The data part. +/// \return the encoded Bech32 string. +TW_EXPORT_STATIC_METHOD +TWString *_Nonnull TWBech32Encode(TWString* _Nonnull hrp, TWData *_Nonnull data); + +/// Decodes a Bech32 string. Returns null if the string is not a valid Bech32 string. +/// +/// \param string The Bech32 string to decode. +/// \return the decoded data, null if the string is not a valid Bech32 string. Note that the human-readable part is not returned. +TW_EXPORT_STATIC_METHOD +TWData *_Nullable TWBech32Decode(TWString *_Nonnull string); + +/// Encodes data as a Bech32m string. +/// +/// \param hrp The human-readable part. +/// \param data The data part. +/// \return the encoded Bech32m string. +TW_EXPORT_STATIC_METHOD +TWString *_Nonnull TWBech32EncodeM(TWString* _Nonnull hrp, TWData *_Nonnull data); + +/// Decodes a Bech32m string. Returns null if the string is not a valid Bech32m string. +/// +/// \param string The Bech32m string to decode. +/// \return the decoded data, null if the string is not a valid Bech32m string. Note that the human-readable part is not returned. +TW_EXPORT_STATIC_METHOD +TWData *_Nullable TWBech32DecodeM(TWString *_Nonnull string); + +TW_EXTERN_C_END diff --git a/src/Bech32.cpp b/src/Bech32.cpp index d6bd309a1de..273c1f3f16c 100644 --- a/src/Bech32.cpp +++ b/src/Bech32.cpp @@ -103,7 +103,7 @@ Data create_checksum(const std::string& hrp, const Data& values, ChecksumVariant } // namespace -/** Encode a Bech32 string. */ +/** Encode a Bech32 string. Note that the values must each encode 5 bits, normally get from convertBits<8, 5, true> */ std::string encode(const std::string& hrp, const Data& values, ChecksumVariant variant) { Data checksum = create_checksum(hrp, values, variant); Data combined = values; @@ -116,7 +116,7 @@ std::string encode(const std::string& hrp, const Data& values, ChecksumVariant v return ret; } -/** Decode a Bech32 string. */ +/** Decode a Bech32 string. Note that the returned values are 5 bits each, you may want to use convertBits<5, 8, false> */ std::tuple decode(const std::string& str) { if (str.length() > 120 || str.length() < 2) { // too long or too short diff --git a/src/interface/TWBech32.cpp b/src/interface/TWBech32.cpp new file mode 100644 index 00000000000..fe184ead846 --- /dev/null +++ b/src/interface/TWBech32.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include + +#include "../Bech32.h" + +#include + +using namespace TW; + +static TWString *_Nonnull encodeGeneric(TWString* _Nonnull hrp, TWData *_Nonnull data, const Bech32::ChecksumVariant variant) { + const auto cppHrp = *static_cast(hrp); + const auto cppData = *static_cast(data); + Data enc; + if (!Bech32::convertBits<8, 5, true>(enc, cppData)) { + return TWStringCreateWithUTF8Bytes(""); + } + const auto result = Bech32::encode(cppHrp, enc, variant); + return TWStringCreateWithUTF8Bytes(result.c_str()); +} + +static TWData *_Nullable decodeGeneric(TWString *_Nonnull string, const Bech32::ChecksumVariant variant) { + const auto cppString = *static_cast(string); + const auto decoded = Bech32::decode(cppString); + + const auto data = std::get<1>(decoded); + if (data.empty()) { // Failed to decode + return nullptr; + } + + if (std::get<2>(decoded) != variant) { // Wrong ChecksumVariant + return nullptr; + } + + // Bech bits conversion + Data conv; + if (!Bech32::convertBits<5, 8, false>(conv, data)) { + return nullptr; + } + + return TWDataCreateWithBytes(conv.data(), conv.size()); +} + +TWString *_Nonnull TWBech32Encode(TWString* _Nonnull hrp, TWData *_Nonnull data) { + return encodeGeneric(hrp, data, Bech32::ChecksumVariant::Bech32); +} + +TWString *_Nonnull TWBech32EncodeM(TWString* _Nonnull hrp, TWData *_Nonnull data) { + return encodeGeneric(hrp, data, Bech32::ChecksumVariant::Bech32M); +} + +TWData *_Nullable TWBech32Decode(TWString *_Nonnull string) { + return decodeGeneric(string, Bech32::ChecksumVariant::Bech32); +} + +TWData *_Nullable TWBech32DecodeM(TWString *_Nonnull string) { + return decodeGeneric(string, Bech32::ChecksumVariant::Bech32M); +} diff --git a/swift/Tests/Bech32Tests.swift b/swift/Tests/Bech32Tests.swift new file mode 100644 index 00000000000..f09bd9507fa --- /dev/null +++ b/swift/Tests/Bech32Tests.swift @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import XCTest +import WalletCore + +class Bech32Tests: XCTestCase { + func testEncode() { + let data = Data(hexString: "00443214c74254b635cf84653a56d7c675be77df")! + let encoded = Bech32.encode(hrp: "abcdef", data: data) + XCTAssertEqual(encoded, "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + } + + func testDecode() { + let decoded = Bech32.decode(string: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw")! + XCTAssertEqual(decoded.hexString, "00443214c74254b635cf84653a56d7c675be77df") + } + + func testDecodeWrongChecksumVariant() { + // This is a Bech32m variant, not Bech32 variant. So it should fail using Bech32 decoder. + let decoded = Bech32.decode(string: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + XCTAssertNil(decoded) + } + + func testEncodeM() { + let data = Data(hexString: "ffbbcdeb38bdab49ca307b9ac5a928398a418820")! + let encoded = Bech32.encodeM(hrp: "abcdef", data: data) + XCTAssertEqual(encoded, "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + } + + func testDecodeM() { + let decoded = Bech32.decodeM(string: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx")! + XCTAssertEqual(decoded.hexString, "ffbbcdeb38bdab49ca307b9ac5a928398a418820") + } + + func testDecodeMWrongChecksumVariant() { + // This is a Bech32 variant, not Bech32m variant. So it should fail using Bech32M decoder. + let decoded = Bech32.decodeM(string: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + XCTAssertNil(decoded) + } +} diff --git a/tests/interface/TWBech32Tests.cpp b/tests/interface/TWBech32Tests.cpp new file mode 100644 index 00000000000..00c1b3b6d98 --- /dev/null +++ b/tests/interface/TWBech32Tests.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" + +#include + +#include + +TEST(TWBech32, Encode) { + const auto hrp = STRING("abcdef"); + const auto data = DATA("00443214c74254b635cf84653a56d7c675be77df"); + const auto result = WRAPS(TWBech32Encode(hrp.get(), data.get())); + assertStringsEqual(result, "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); +} + +TEST(TWBech32, Decode) { + const auto input = STRING("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + const auto result = WRAPD(TWBech32Decode(input.get())); + assertHexEqual(result, "00443214c74254b635cf84653a56d7c675be77df"); +} + +TEST(TWBech32, Decode_WrongChecksumVariant) { + // This is a Bech32m variant, not Bech32 variant. So it should fail using Bech32 decoder. + const auto input = STRING("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + const auto result = WRAPD(TWBech32Decode(input.get())); + ASSERT_EQ(result.get(), nullptr); +} + +TEST(TWBech32, EncodeM) { + const auto hrp = STRING("abcdef"); + const auto data = DATA("ffbbcdeb38bdab49ca307b9ac5a928398a418820"); + const auto result = WRAPS(TWBech32EncodeM(hrp.get(), data.get())); + assertStringsEqual(result, "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); +} + +TEST(TWBech32, DecodeM) { + const auto input = STRING("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + auto result = WRAPD(TWBech32DecodeM(input.get())); + assertHexEqual(result, "ffbbcdeb38bdab49ca307b9ac5a928398a418820"); +} + +TEST(TWBech32, DecodeM_WrongChecksumVariant) { + // This is a Bech32 variant, not Bech32m variant. So it should fail using Bech32M decoder. + const auto input = STRING("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + const auto result = WRAPD(TWBech32DecodeM(input.get())); + ASSERT_EQ(result.get(), nullptr); +} diff --git a/wasm/tests/Bech32.test.ts b/wasm/tests/Bech32.test.ts new file mode 100644 index 00000000000..f9ac76a0c9f --- /dev/null +++ b/wasm/tests/Bech32.test.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import { assert } from "chai"; +import { Buffer } from "buffer"; + +describe("Bech32", () => { + + it("test encode", () => { + const { Bech32 } = globalThis.core; + + const encoded = Bech32.encode("abcdef", Buffer.from("00443214c74254b635cf84653a56d7c675be77df", "hex")); + + assert.equal(encoded, "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + }); + + it("test decode", () => { + const { Bech32, HexCoding } = globalThis.core; + + const decoded = Bech32.decode("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + + assert.equal( + HexCoding.encode(decoded), + "0x00443214c74254b635cf84653a56d7c675be77df" + ); + }); + + it("test encodeM", () => { + const { Bech32 } = globalThis.core; + + const encoded = Bech32.encodeM("abcdef", Buffer.from("ffbbcdeb38bdab49ca307b9ac5a928398a418820", "hex")); + + assert.equal(encoded, "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + }); + + it("test decodeM", () => { + const { Bech32, HexCoding } = globalThis.core; + + const decoded = Bech32.decodeM("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + + assert.equal( + HexCoding.encode(decoded), + "0xffbbcdeb38bdab49ca307b9ac5a928398a418820" + ); + }); +}); From 9dae7e1b6447c8fdafa75555c19476ea8b431128 Mon Sep 17 00:00:00 2001 From: Andropulus Date: Fri, 22 Nov 2024 19:24:15 +0100 Subject: [PATCH 6/6] fixed duplicate symbol with another lib (#4093) * fixed duplicate symbol with another lib * fixed review --------- Co-authored-by: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> --- src/Nimiq/Address.cpp | 2 +- src/interface/TWHash.cpp | 2 +- trezor-crypto/crypto/blake2b.c | 38 +++++++++---------- trezor-crypto/crypto/hasher.c | 8 ++-- trezor-crypto/crypto/nano.c | 12 +++--- trezor-crypto/crypto/tests/test_check.c | 22 +++++------ trezor-crypto/include/TrezorCrypto/blake2b.h | 16 ++++---- .../ed25519-hash-custom-blake2b.h | 8 ++-- 8 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/Nimiq/Address.cpp b/src/Nimiq/Address.cpp index ae58500cfc0..46ab1bb1f5e 100644 --- a/src/Nimiq/Address.cpp +++ b/src/Nimiq/Address.cpp @@ -88,7 +88,7 @@ Address::Address(const std::vector& data) { Address::Address(const PublicKey& publicKey) { auto hash = std::array(); - blake2b(publicKey.bytes.data(), 32, hash.data(), hash.size()); + tc_blake2b(publicKey.bytes.data(), 32, hash.data(), hash.size()); std::copy(hash.begin(), hash.begin() + Address::size, bytes.begin()); } diff --git a/src/interface/TWHash.cpp b/src/interface/TWHash.cpp index b66adc07fb3..a6a5528627d 100644 --- a/src/interface/TWHash.cpp +++ b/src/interface/TWHash.cpp @@ -86,7 +86,7 @@ TWData *_Nonnull TWHashBlake2bPersonal(TWData *_Nonnull data, TWData * _Nonnull auto dataBytes = TWDataBytes(data); auto personalBytes = TWDataBytes(personal); auto personalSize = TWDataSize(personal); - blake2b_Personal(dataBytes, static_cast(TWDataSize(data)), personalBytes, personalSize, resultBytes.data(), outlen); + tc_blake2b_Personal(dataBytes, static_cast(TWDataSize(data)), personalBytes, personalSize, resultBytes.data(), outlen); auto result = TWDataCreateWithBytes(resultBytes.data(), outlen); return result; } diff --git a/trezor-crypto/crypto/blake2b.c b/trezor-crypto/crypto/blake2b.c index 5ec488bfe00..5b71c58de65 100644 --- a/trezor-crypto/crypto/blake2b.c +++ b/trezor-crypto/crypto/blake2b.c @@ -93,7 +93,7 @@ static void blake2b_init0( blake2b_state *S ) } /* init xors IV with input parameter block */ -int blake2b_init_param( blake2b_state *S, const blake2b_param *P ) +static int blake2b_init_param( blake2b_state *S, const blake2b_param *P ) { const uint8_t *p = ( const uint8_t * )( P ); size_t i = 0; @@ -110,7 +110,7 @@ int blake2b_init_param( blake2b_state *S, const blake2b_param *P ) /* Sequential blake2b initialization */ -int blake2b_Init( blake2b_state *S, size_t outlen ) +int tc_blake2b_Init( blake2b_state *S, size_t outlen ) { blake2b_param P[1] = {0}; @@ -131,7 +131,7 @@ int blake2b_Init( blake2b_state *S, size_t outlen ) return blake2b_init_param( S, P ); } -int blake2b_InitPersonal( blake2b_state *S, size_t outlen, const void *personal, size_t personal_len) +int tc_blake2b_InitPersonal( blake2b_state *S, size_t outlen, const void *personal, size_t personal_len) { blake2b_param P[1] = {0}; @@ -153,7 +153,7 @@ int blake2b_InitPersonal( blake2b_state *S, size_t outlen, const void *personal, return blake2b_init_param( S, P ); } -int blake2b_InitKey( blake2b_state *S, size_t outlen, const void *key, size_t keylen ) +int tc_blake2b_InitKey( blake2b_state *S, size_t outlen, const void *key, size_t keylen ) { blake2b_param P[1] = {0}; @@ -180,7 +180,7 @@ int blake2b_InitKey( blake2b_state *S, size_t outlen, const void *key, size_t ke uint8_t block[BLAKE2B_BLOCKBYTES] = {0}; memzero( block, BLAKE2B_BLOCKBYTES ); memcpy( block, key, keylen ); - blake2b_Update( S, block, BLAKE2B_BLOCKBYTES ); + tc_blake2b_Update( S, block, BLAKE2B_BLOCKBYTES ); memzero( block, BLAKE2B_BLOCKBYTES ); /* Burn the key from stack */ } return 0; @@ -254,7 +254,7 @@ static void blake2b_compress( blake2b_state *S, const uint8_t block[BLAKE2B_BLOC #undef G #undef ROUND -int blake2b_Update( blake2b_state *S, const void *pin, size_t inlen ) +int tc_blake2b_Update( blake2b_state *S, const void *pin, size_t inlen ) { const unsigned char * in = (const unsigned char *)pin; if( inlen > 0 ) @@ -281,7 +281,7 @@ int blake2b_Update( blake2b_state *S, const void *pin, size_t inlen ) return 0; } -int blake2b_Final( blake2b_state *S, void *out, size_t outlen ) +int tc_blake2b_Final( blake2b_state *S, void *out, size_t outlen ) { uint8_t buffer[BLAKE2B_OUTBYTES] = {0}; size_t i = 0; @@ -305,30 +305,30 @@ int blake2b_Final( blake2b_state *S, void *out, size_t outlen ) return 0; } -int blake2b(const uint8_t *msg, uint32_t msg_len, void *out, size_t outlen) +int tc_blake2b(const uint8_t *msg, uint32_t msg_len, void *out, size_t outlen) { BLAKE2B_CTX ctx; - if (0 != blake2b_Init(&ctx, outlen)) return -1; - if (0 != blake2b_Update(&ctx, msg, msg_len)) return -1; - if (0 != blake2b_Final(&ctx, out, outlen)) return -1; + if (0 != tc_blake2b_Init(&ctx, outlen)) return -1; + if (0 != tc_blake2b_Update(&ctx, msg, msg_len)) return -1; + if (0 != tc_blake2b_Final(&ctx, out, outlen)) return -1; return 0; } // [wallet-core] -int blake2b_Personal(const uint8_t *msg, uint32_t msg_len, const void *personal, size_t personal_len, void *out, size_t outlen) +int tc_blake2b_Personal(const uint8_t *msg, uint32_t msg_len, const void *personal, size_t personal_len, void *out, size_t outlen) { BLAKE2B_CTX ctx; - if (0 != blake2b_InitPersonal(&ctx, outlen, personal, personal_len)) return -1; - if (0 != blake2b_Update(&ctx, msg, msg_len)) return -1; - if (0 != blake2b_Final(&ctx, out, outlen)) return -1; + if (0 != tc_blake2b_InitPersonal(&ctx, outlen, personal, personal_len)) return -1; + if (0 != tc_blake2b_Update(&ctx, msg, msg_len)) return -1; + if (0 != tc_blake2b_Final(&ctx, out, outlen)) return -1; return 0; } -int blake2b_Key(const uint8_t *msg, uint32_t msg_len, const void *key, size_t keylen, void *out, size_t outlen) +int tc_blake2b_Key(const uint8_t *msg, uint32_t msg_len, const void *key, size_t keylen, void *out, size_t outlen) { BLAKE2B_CTX ctx; - if (0 != blake2b_InitKey(&ctx, outlen, key, keylen)) return -1; - if (0 != blake2b_Update(&ctx, msg, msg_len)) return -1; - if (0 != blake2b_Final(&ctx, out, outlen)) return -1; + if (0 != tc_blake2b_InitKey(&ctx, outlen, key, keylen)) return -1; + if (0 != tc_blake2b_Update(&ctx, msg, msg_len)) return -1; + if (0 != tc_blake2b_Final(&ctx, out, outlen)) return -1; return 0; } diff --git a/trezor-crypto/crypto/hasher.c b/trezor-crypto/crypto/hasher.c index d6dd613e905..59c10181497 100644 --- a/trezor-crypto/crypto/hasher.c +++ b/trezor-crypto/crypto/hasher.c @@ -50,10 +50,10 @@ void hasher_InitParam(Hasher *hasher, HasherType type, const void *param, groestl512_Init(&hasher->ctx.groestl); break; case HASHER_BLAKE2B: - blake2b_Init(&hasher->ctx.blake2b, 32); + tc_blake2b_Init(&hasher->ctx.blake2b, 32); break; case HASHER_BLAKE2B_PERSONAL: - blake2b_InitPersonal(&hasher->ctx.blake2b, 32, hasher->param, + tc_blake2b_InitPersonal(&hasher->ctx.blake2b, 32, hasher->param, hasher->param_size); break; } @@ -90,7 +90,7 @@ void hasher_Update(Hasher *hasher, const uint8_t *data, size_t length) { break; case HASHER_BLAKE2B: case HASHER_BLAKE2B_PERSONAL: - blake2b_Update(&hasher->ctx.blake2b, data, length); + tc_blake2b_Update(&hasher->ctx.blake2b, data, length); break; } } @@ -132,7 +132,7 @@ void hasher_Final(Hasher *hasher, uint8_t hash[HASHER_DIGEST_LENGTH]) { break; case HASHER_BLAKE2B: case HASHER_BLAKE2B_PERSONAL: - blake2b_Final(&hasher->ctx.blake2b, hash, 32); + tc_blake2b_Final(&hasher->ctx.blake2b, hash, 32); break; } } diff --git a/trezor-crypto/crypto/nano.c b/trezor-crypto/crypto/nano.c index 15dc643fa13..54f4968d1ef 100644 --- a/trezor-crypto/crypto/nano.c +++ b/trezor-crypto/crypto/nano.c @@ -66,9 +66,9 @@ size_t nano_get_address( uint8_t checksum[NANO_CHECKSUM_LEN]; blake2b_state hash; - blake2b_Init(&hash, NANO_CHECKSUM_LEN); - blake2b_Update(&hash, public_key, sizeof(ed25519_public_key)); - blake2b_Final(&hash, checksum, NANO_CHECKSUM_LEN); + tc_blake2b_Init(&hash, NANO_CHECKSUM_LEN); + tc_blake2b_Update(&hash, public_key, sizeof(ed25519_public_key)); + tc_blake2b_Final(&hash, checksum, NANO_CHECKSUM_LEN); for (int i = 0; i < NANO_CHECKSUM_LEN; i++) { raw.data.checksum[NANO_CHECKSUM_LEN - (i + 1)] = checksum[i]; @@ -132,9 +132,9 @@ bool nano_validate_address( // Validate the checksum uint8_t checksum[NANO_CHECKSUM_LEN]; blake2b_state hash; - blake2b_Init(&hash, NANO_CHECKSUM_LEN); - blake2b_Update(&hash, raw.data.public_key, sizeof(ed25519_public_key)); - blake2b_Final(&hash, checksum, NANO_CHECKSUM_LEN); + tc_blake2b_Init(&hash, NANO_CHECKSUM_LEN); + tc_blake2b_Update(&hash, raw.data.public_key, sizeof(ed25519_public_key)); + tc_blake2b_Final(&hash, checksum, NANO_CHECKSUM_LEN); for (int i = 0; i < NANO_CHECKSUM_LEN; i++) { if (raw.data.checksum[NANO_CHECKSUM_LEN - (i + 1)] != checksum[i]) { diff --git a/trezor-crypto/crypto/tests/test_check.c b/trezor-crypto/crypto/tests/test_check.c index 0227f0146a8..138dd253fa0 100644 --- a/trezor-crypto/crypto/tests/test_check.c +++ b/trezor-crypto/crypto/tests/test_check.c @@ -4837,21 +4837,21 @@ START_TEST(test_blake2b) { uint8_t digest[BLAKE2B_DIGEST_LENGTH]; for (size_t i = 0; i < (sizeof(tests) / sizeof(*tests)); i++) { size_t msg_len = strlen(tests[i].msg) / 2; - blake2b_Key(fromhex(tests[i].msg), msg_len, key, sizeof(key), digest, + tc_blake2b_Key(fromhex(tests[i].msg), msg_len, key, sizeof(key), digest, sizeof(digest)); ck_assert_mem_eq(digest, fromhex(tests[i].hash), sizeof(digest)); // Test progressive hashing. size_t part_len = msg_len / 2; BLAKE2B_CTX ctx; - ck_assert_int_eq(blake2b_InitKey(&ctx, sizeof(digest), key, sizeof(key)), + ck_assert_int_eq(tc_blake2b_InitKey(&ctx, sizeof(digest), key, sizeof(key)), 0); - ck_assert_int_eq(blake2b_Update(&ctx, fromhex(tests[i].msg), part_len), 0); - ck_assert_int_eq(blake2b_Update(&ctx, NULL, 0), 0); - ck_assert_int_eq(blake2b_Update(&ctx, fromhex(tests[i].msg) + part_len, + ck_assert_int_eq(tc_blake2b_Update(&ctx, fromhex(tests[i].msg), part_len), 0); + ck_assert_int_eq(tc_blake2b_Update(&ctx, NULL, 0), 0); + ck_assert_int_eq(tc_blake2b_Update(&ctx, fromhex(tests[i].msg) + part_len, msg_len - part_len), 0); - ck_assert_int_eq(blake2b_Final(&ctx, digest, sizeof(digest)), 0); + ck_assert_int_eq(tc_blake2b_Final(&ctx, digest, sizeof(digest)), 0); ck_assert_mem_eq(digest, fromhex(tests[i].hash), BLAKE2B_DIGEST_LENGTH); } } @@ -4897,15 +4897,15 @@ START_TEST(test_blake2bp) { size_t part_len = msg_len / 2; BLAKE2B_CTX ctx; ck_assert_int_eq( - blake2b_InitPersonal(&ctx, sizeof(digest), tests[i].personal, + tc_blake2b_InitPersonal(&ctx, sizeof(digest), tests[i].personal, strlen(tests[i].personal)), 0); - ck_assert_int_eq(blake2b_Update(&ctx, fromhex(tests[i].msg), part_len), 0); - ck_assert_int_eq(blake2b_Update(&ctx, NULL, 0), 0); - ck_assert_int_eq(blake2b_Update(&ctx, fromhex(tests[i].msg) + part_len, + ck_assert_int_eq(tc_blake2b_Update(&ctx, fromhex(tests[i].msg), part_len), 0); + ck_assert_int_eq(tc_blake2b_Update(&ctx, NULL, 0), 0); + ck_assert_int_eq(tc_blake2b_Update(&ctx, fromhex(tests[i].msg) + part_len, msg_len - part_len), 0); - ck_assert_int_eq(blake2b_Final(&ctx, digest, sizeof(digest)), 0); + ck_assert_int_eq(tc_blake2b_Final(&ctx, digest, sizeof(digest)), 0); ck_assert_mem_eq(digest, fromhex(tests[i].hash), sizeof(digest)); } } diff --git a/trezor-crypto/include/TrezorCrypto/blake2b.h b/trezor-crypto/include/TrezorCrypto/blake2b.h index c002dd7afc5..c4e20bb7c34 100644 --- a/trezor-crypto/include/TrezorCrypto/blake2b.h +++ b/trezor-crypto/include/TrezorCrypto/blake2b.h @@ -33,16 +33,16 @@ typedef struct __blake2b_state #define BLAKE2B_DIGEST_LENGTH BLAKE2B_OUTBYTES #define BLAKE2B_KEY_LENGTH BLAKE2B_KEYBYTES -int blake2b_Init(blake2b_state *S, size_t outlen); -int blake2b_InitKey(blake2b_state *S, size_t outlen, const void *key, size_t keylen); -int blake2b_InitPersonal(blake2b_state *S, size_t outlen, const void *personal, size_t personal_len); -int blake2b_Update(blake2b_state *S, const void *pin, size_t inlen); -int blake2b_Final(blake2b_state *S, void *out, size_t outlen); +int tc_blake2b_Init(blake2b_state *S, size_t outlen); +int tc_blake2b_InitKey(blake2b_state *S, size_t outlen, const void *key, size_t keylen); +int tc_blake2b_InitPersonal(blake2b_state *S, size_t outlen, const void *personal, size_t personal_len); +int tc_blake2b_Update(blake2b_state *S, const void *pin, size_t inlen); +int tc_blake2b_Final(blake2b_state *S, void *out, size_t outlen); -int blake2b(const uint8_t *msg, uint32_t msg_len, void *out, size_t outlen); +int tc_blake2b(const uint8_t *msg, uint32_t msg_len, void *out, size_t outlen); // [wallet-core] -int blake2b_Personal(const uint8_t *msg, uint32_t msg_len, const void *personal, size_t personal_len, void *out, size_t outlen); -int blake2b_Key(const uint8_t *msg, uint32_t msg_len, const void *key, size_t keylen, void *out, size_t outlen); +int tc_blake2b_Personal(const uint8_t *msg, uint32_t msg_len, const void *personal, size_t personal_len, void *out, size_t outlen); +int tc_blake2b_Key(const uint8_t *msg, uint32_t msg_len, const void *key, size_t keylen, void *out, size_t outlen); #ifdef __cplusplus } /* extern "C" */ diff --git a/trezor-crypto/include/TrezorCrypto/ed25519-donna/ed25519-hash-custom-blake2b.h b/trezor-crypto/include/TrezorCrypto/ed25519-donna/ed25519-hash-custom-blake2b.h index 9c46630a9bd..68706f37795 100644 --- a/trezor-crypto/include/TrezorCrypto/ed25519-donna/ed25519-hash-custom-blake2b.h +++ b/trezor-crypto/include/TrezorCrypto/ed25519-donna/ed25519-hash-custom-blake2b.h @@ -15,9 +15,9 @@ #include #define ed25519_hash_context BLAKE2B_CTX -#define ed25519_hash_init(ctx) blake2b_Init(ctx, 64) -#define ed25519_hash_update(ctx, in, inlen) blake2b_Update((ctx), (in), (inlen)) -#define ed25519_hash_final(ctx, hash) blake2b_Final((ctx), (hash), 64) -#define ed25519_hash(hash, in, inlen) blake2b((in), (inlen), (hash), 64) +#define ed25519_hash_init(ctx) tc_blake2b_Init(ctx, 64) +#define ed25519_hash_update(ctx, in, inlen) tc_blake2b_Update((ctx), (in), (inlen)) +#define ed25519_hash_final(ctx, hash) tc_blake2b_Final((ctx), (hash), 64) +#define ed25519_hash(hash, in, inlen) tc_blake2b((in), (inlen), (hash), 64) #endif // ED25519_HASH_CUSTOM