From 0ee01facb3e332108c76c13d743d6e021a5d24f8 Mon Sep 17 00:00:00 2001 From: hewigovens <360470+hewigovens@users.noreply.github.com> Date: Wed, 24 Feb 2021 01:16:23 +0900 Subject: [PATCH] [Polkadot] prepare for MultiAddress::AccountId (#1304) --- src/Polkadot/Extrinsic.cpp | 39 +++++++++++++-------- src/Polkadot/Extrinsic.h | 11 ++++-- src/Polkadot/ScaleCodec.h | 20 +++++------ src/SS58Address.h | 5 +++ swift/Tests/Blockchains/PolkadotTests.swift | 33 +++++++++++++++-- tests/Polkadot/ScaleCodecTests.cpp | 14 ++++---- tests/Polkadot/SignerTests.cpp | 33 +++++++++++++++++ 7 files changed, 118 insertions(+), 37 deletions(-) diff --git a/src/Polkadot/Extrinsic.cpp b/src/Polkadot/Extrinsic.cpp index 26272b235e2..473526f49f1 100644 --- a/src/Polkadot/Extrinsic.cpp +++ b/src/Polkadot/Extrinsic.cpp @@ -14,6 +14,8 @@ using namespace TW::Polkadot; static constexpr uint8_t signedBit = 0x80; static constexpr uint8_t sigTypeEd25519 = 0x00; static constexpr uint8_t extrinsicFormat = 4; +static constexpr uint32_t multiAddrSpecVersion = 28; +static constexpr uint32_t multiAddrSpecVersionKsm = 2028; static const std::string balanceTransfer = "Balances.transfer"; static const std::string utilityBatch = "Utility.batch"; @@ -24,9 +26,10 @@ static const std::string stakingWithdrawUnbond = "Staking.withdraw_unbonded"; static const std::string stakingNominate = "Staking.nominate"; static const std::string stakingChill = "Staking.chill"; +// Readable decoded call index can be found from https://polkascan.io static std::map polkadotCallIndices = { {balanceTransfer, Data{0x05, 0x00}}, - {utilityBatch, Data{0x1a, 0x00}}, // https://polkascan.io/polkadot/runtime-call/27-utility-batch + {utilityBatch, Data{0x1a, 0x00}}, {stakingBond, Data{0x07, 0x00}}, {stakingBondExtra, Data{0x07, 0x01}}, {stakingUnbond, Data{0x07, 0x02}}, @@ -54,9 +57,17 @@ static Data getCallIndex(TWSS58AddressType network, const std::string& key) { } } +bool Extrinsic::encodeRawAccount(TWSS58AddressType network, uint32_t specVersion) { + if ((network == TWSS58AddressTypePolkadot && specVersion >= multiAddrSpecVersion) || + (network == TWSS58AddressTypeKusama && specVersion >= multiAddrSpecVersionKsm)) { + return false; + } + return true; +} + Data Extrinsic::encodeEraNonceTip() const { Data data; - // immortal era + // era append(data, era); // nonce append(data, encodeCompact(nonce)); @@ -70,14 +81,14 @@ Data Extrinsic::encodeCall(const Proto::SigningInput& input) { Data data; auto network = TWSS58AddressType(input.network()); if (input.has_balance_call()) { - data = encodeBalanceCall(input.balance_call(), network); + data = encodeBalanceCall(input.balance_call(), network, input.spec_version()); } else if (input.has_staking_call()) { - data = encodeStakingCall(input.staking_call(), network); + data = encodeStakingCall(input.staking_call(), network, input.spec_version()); } return data; } -Data Extrinsic::encodeBalanceCall(const Proto::Balance& balance, TWSS58AddressType network) { +Data Extrinsic::encodeBalanceCall(const Proto::Balance& balance, TWSS58AddressType network, uint32_t specVersion) { Data data; auto transfer = balance.transfer(); auto address = SS58Address(transfer.to_address(), network); @@ -85,7 +96,7 @@ Data Extrinsic::encodeBalanceCall(const Proto::Balance& balance, TWSS58AddressTy // call index append(data, getCallIndex(network, balanceTransfer)); // destination - append(data, encodeAddress(address)); + append(data, encodeAccountId(address.keyBytes(), encodeRawAccount(network, specVersion))); // value append(data, encodeCompact(value)); return data; @@ -98,7 +109,7 @@ Data Extrinsic::encodeBatchCall(const std::vector& calls, TWSS58AddressTyp return data; } -Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressType network) { +Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressType network, uint32_t specVersion) { Data data; switch (staking.message_oneof_case()) { case Proto::Staking::kBond: @@ -109,7 +120,7 @@ Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressTy // call index append(data, getCallIndex(network, stakingBond)); // controller - append(data, encodeAddress(address)); + append(data, encodeAccountId(address.keyBytes(), encodeRawAccount(network, specVersion))); // value append(data, encodeCompact(value)); // reward destination @@ -128,7 +139,7 @@ Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressTy bond->set_value(staking.bond_and_nominate().value()); bond->set_reward_destination(staking.bond_and_nominate().reward_destination()); // recursive call - call1 = encodeStakingCall(staking1, network); + call1 = encodeStakingCall(staking1, network, specVersion); } // encode call2 @@ -140,7 +151,7 @@ Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressTy nominate->add_nominators(staking.bond_and_nominate().nominators(i)); } // recursive call - call2 = encodeStakingCall(staking2, network); + call2 = encodeStakingCall(staking2, network, specVersion); } auto calls = std::vector{call1, call2}; @@ -180,14 +191,14 @@ Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressTy case Proto::Staking::kNominate: { - std::vector addresses; + std::vector accountIds; for (auto& n : staking.nominate().nominators()) { - addresses.push_back(SS58Address(n, network)); + accountIds.push_back(SS58Address(n, network)); } // call index append(data, getCallIndex(network, stakingNominate)); // nominators - append(data, encodeAddresses(addresses)); + append(data, encodeAccountIds(accountIds, encodeRawAccount(network, specVersion))); } break; @@ -224,7 +235,7 @@ Data Extrinsic::encodeSignature(const PublicKey& signer, const Data& signature) // version header append(data, Data{extrinsicFormat | signedBit}); // signer public key - append(data, encodeAddress(signer)); + append(data, encodeAccountId(signer.bytes, encodeRawAccount(network, specVersion))); // signature type append(data, sigTypeEd25519); // signature diff --git a/src/Polkadot/Extrinsic.h b/src/Polkadot/Extrinsic.h index 87871cc5713..dc20eb75908 100644 --- a/src/Polkadot/Extrinsic.h +++ b/src/Polkadot/Extrinsic.h @@ -30,6 +30,8 @@ class Extrinsic { Data era; // encoded Call data Data call; + // network + TWSS58AddressType network; Extrinsic(const Proto::SigningInput& input) : blockHash(input.block_hash().begin(), input.block_hash().end()) @@ -41,8 +43,10 @@ class Extrinsic { if (input.has_era()) { era = encodeEra(input.era().block_number(), input.era().period()); } else { - era = encodeCompact(0); + // immortal era + era = encodeCompact(0); } + network = TWSS58AddressType(input.network()); call = encodeCall(input); } @@ -53,8 +57,9 @@ class Extrinsic { Data encodeSignature(const PublicKey& signer, const Data& signature) const; protected: - static Data encodeBalanceCall(const Proto::Balance& balance, TWSS58AddressType network); - static Data encodeStakingCall(const Proto::Staking& staking, TWSS58AddressType network); + static bool encodeRawAccount(TWSS58AddressType network, uint32_t specVersion); + static Data encodeBalanceCall(const Proto::Balance& balance, TWSS58AddressType network, uint32_t specVersion); + static Data encodeStakingCall(const Proto::Staking& staking, TWSS58AddressType network, uint32_t specVersion); static Data encodeBatchCall(const std::vector& calls, TWSS58AddressType network); Data encodeEraNonceTip() const; }; diff --git a/src/Polkadot/ScaleCodec.h b/src/Polkadot/ScaleCodec.h index a96a5097ca9..6a244383440 100644 --- a/src/Polkadot/ScaleCodec.h +++ b/src/Polkadot/ScaleCodec.h @@ -98,23 +98,21 @@ inline Data encodeVector(const std::vector& vec) { return data; } -inline Data encodeAddress(const PublicKey& key) { +inline Data encodeAccountId(const Data& bytes, bool raw) { auto data = Data{}; - append(data, Data(key.bytes.begin(), key.bytes.end())); - return data; -} - -inline Data encodeAddress(const SS58Address& address) { - auto data = Data{}; - // first byte is network - append(data, Data(address.bytes.begin() + 1, address.bytes.end())); + if (!raw) { + // MultiAddress::AccountId + // https://github.com/paritytech/substrate/blob/master/primitives/runtime/src/multiaddress.rs#L28 + append(data, 0x00); + } + append(data, bytes); return data; } -inline Data encodeAddresses(const std::vector& addresses) { +inline Data encodeAccountIds(const std::vector& addresses, bool raw) { std::vector vec; for (auto addr : addresses) { - vec.push_back(encodeAddress(addr)); + vec.push_back(encodeAccountId(addr.keyBytes(), raw)); } return encodeVector(vec); } diff --git a/src/SS58Address.h b/src/SS58Address.h index d51063663bf..b9ccfa627b6 100644 --- a/src/SS58Address.h +++ b/src/SS58Address.h @@ -83,6 +83,11 @@ class SS58Address { append(result, checksum); return Base58::bitcoin.encode(result); } + + /// Returns public key bytes + Data keyBytes() const { + return Data(bytes.begin() + 1, bytes.end()); + } }; inline bool operator==(const SS58Address& lhs, const SS58Address& rhs) { diff --git a/swift/Tests/Blockchains/PolkadotTests.swift b/swift/Tests/Blockchains/PolkadotTests.swift index 3d3d0e000a2..636d216cdf0 100644 --- a/swift/Tests/Blockchains/PolkadotTests.swift +++ b/swift/Tests/Blockchains/PolkadotTests.swift @@ -35,8 +35,35 @@ class PolkadotTests: XCTestCase { XCTAssertEqual(address.data, pubkey.data) } + func testSignTransfer() { + // real key in 1p test + let wallet = HDWallet.test + let key = wallet.getKey(coin: .polkadot, derivationPath: "m/44'/354'/0'") + + let input = PolkadotSigningInput.with { + $0.genesisHash = genesisHash + $0.blockHash = Data(hexString: "0x7d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd")! + $0.nonce = 1 + $0.specVersion = 28 + $0.network = .polkadot + $0.transactionVersion = 6 + $0.privateKey = key.data + $0.era = PolkadotEra.with { + $0.blockNumber = 3910736 + $0.period = 64 + } + $0.balanceCall.transfer = PolkadotBalance.Transfer.with { + $0.toAddress = "13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5" + $0.value = Data(hexString: "0x02540be400")! // 1 DOT + } + } + let output: PolkadotSigningOutput = AnySigner.sign(input: input, coin: .polkadot) + + // https://polkadot.subscan.io/extrinsic/0x72dd5b5a3e01e0f3e779c5fa39e53de05ee381b9138d24e2791a775a6d1ff679 + XCTAssertEqual(output.encoded.hexString, "410284008d96660f14babe708b5e61853c9f5929bc90dd9874485bf4d6dc32d3e6f22eaa0038ec4973ab9773dfcbf170b8d27d36d89b85c3145e038d68914de83cf1f7aca24af64c55ec51ba9f45c5a4d74a9917dee380e9171108921c3e5546e05be15206050104000500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402") + } + func testSigningBond() { - // https://polkadot.subscan.io/extrinsic/0x5ec2ec6633b4b6993d9cf889ef42c457a99676244dc361a9ae17935d331dc39a // real key in 1p test let wallet = HDWallet.test let key = wallet.getKey(coin: .polkadot, derivationPath: "m/44'/354'/0'") @@ -53,12 +80,12 @@ class PolkadotTests: XCTestCase { $0.stakingCall.bond = PolkadotStaking.Bond.with { $0.controller = address $0.rewardDestination = .staked - // 0.01 - $0.value = Data(hexString: "0x02540be400")! + $0.value = Data(hexString: "0x02540be400")! // 0.01 old DOT } } let output: PolkadotSigningOutput = AnySigner.sign(input: input, coin: .polkadot) + // https://polkadot.subscan.io/extrinsic/0x5ec2ec6633b4b6993d9cf889ef42c457a99676244dc361a9ae17935d331dc39a XCTAssertEqual(output.encoded.hexString, "3902848d96660f14babe708b5e61853c9f5929bc90dd9874485bf4d6dc32d3e6f22eaa00a559867d1304cc95bac7cfe5d1b2fd49aed9f43c25c7d29b9b01c1238fa1f6ffef34b9650e42325de41e20fd502af7b074c67a9ec858bd9a1ba6d4212e3e0d0f00000007008d96660f14babe708b5e61853c9f5929bc90dd9874485bf4d6dc32d3e6f22eaa0700e40b540200") } diff --git a/tests/Polkadot/ScaleCodecTests.cpp b/tests/Polkadot/ScaleCodecTests.cpp index 13231d58465..efdd07515c8 100644 --- a/tests/Polkadot/ScaleCodecTests.cpp +++ b/tests/Polkadot/ScaleCodecTests.cpp @@ -52,20 +52,22 @@ TEST(PolkadotCodec, EncodeLengthPrefix) { ASSERT_EQ(hex(encoded), "2d0284ff88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0034a113577b56545c45e18969471eebe11ed434f3b2f06e2e3dc8dc137ba804caf60757787ebdeb298327e2f29d68c5520965405ef5582db0445c06e1c11a8a0e0000000400ff8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); } -TEST(PolkadotCodec, EncodeAddress) { +TEST(PolkadotCodec, encodeAccountId) { auto address = Kusama::Address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP"); - auto encoded = encodeAddress(address); + auto encoded = encodeAccountId(address.keyBytes(), true); + auto encoded2 = encodeAccountId(address.keyBytes(), false); ASSERT_EQ(hex(encoded), "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"); + ASSERT_EQ(hex(encoded2), "008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"); } -TEST(PolkadotCodec, EncodeVectorAddresses) { +TEST(PolkadotCodec, EncodeVectorAccountIds) { auto addresses = std::vector{ - Kusama::Address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP"), + Kusama::Address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP"), Kusama::Address("CtwdfrhECFs3FpvCGoiE4hwRC4UsSiM8WL899HjRdQbfYZY") }; - auto encoded = encodeAddresses(addresses); - ASSERT_EQ(hex(encoded), "088eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480e33fdfb980e4499e5c3576e742a563b6a4fc0f6f598b1917fd7a6fe393ffc72"); + auto encoded = encodeAccountIds(addresses, false); + ASSERT_EQ(hex(encoded), "08008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000e33fdfb980e4499e5c3576e742a563b6a4fc0f6f598b1917fd7a6fe393ffc72"); } TEST(PolkadotCodec, EncodeEra) { diff --git a/tests/Polkadot/SignerTests.cpp b/tests/Polkadot/SignerTests.cpp index 89f96d511f6..9014e93d016 100644 --- a/tests/Polkadot/SignerTests.cpp +++ b/tests/Polkadot/SignerTests.cpp @@ -20,6 +20,7 @@ namespace TW::Polkadot { auto privateKey = PrivateKey(parse_hex("0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115")); + auto privateKeyIOS = PrivateKey(parse_hex("37932b086586a6675e66e562fe68bd3eeea4177d066619c602fe3efc290ada62")); auto privateKeyThrow2 = PrivateKey(parse_hex("70a794d4f1019c3ce002f33062f45029c4f930a56b3d20ec477f7668c6bbc37f")); auto addressThrow2 = "14Ztd3KJDaB9xyJtRkREtSZDdhLSbm7UUKt8Z7AwSv7q85G2"; auto toPublicKey = PublicKey(parse_hex("0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"), TWPublicKeyTypeED25519); @@ -97,6 +98,38 @@ TEST(PolkadotSigner, SignTransferDOT) { ASSERT_EQ(hex(output.encoded()), "29028488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee003d91a06263956d8ce3ce5c55455baefff299d9cb2bb3f76866b6828ee4083770b6c03b05d7b6eb510ac78d047002c1fe5c6ee4b37c9c5a8b09ea07677f12e50d3200000005008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); } +TEST(PolkadotSigner, SignTransfer_72dd5b) { + + auto blockHash = parse_hex("7d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd"); + + auto input = Proto::SigningInput(); + input.set_genesis_hash(genesisHash.data(), genesisHash.size()); + input.set_block_hash(blockHash.data(), blockHash.size()); + + input.set_nonce(1); + input.set_spec_version(28); + input.set_private_key(privateKeyIOS.bytes.data(), privateKeyIOS.bytes.size()); + input.set_network(Proto::Network::POLKADOT); + input.set_transaction_version(6); + + auto &era = *input.mutable_era(); + era.set_block_number(3910736); + era.set_period(64); + + auto balanceCall = input.mutable_balance_call(); + auto &transfer = *balanceCall->mutable_transfer(); + auto value = store(uint256_t(10000000000)); + transfer.set_to_address("13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5"); + transfer.set_value(value.data(), value.size()); + + auto extrinsic = Extrinsic(input); + auto preimage = extrinsic.encodePayload(); + auto output = Signer::sign(input); + + ASSERT_EQ(hex(preimage), "0500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402050104001c0000000600000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c37d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd"); + ASSERT_EQ(hex(output.encoded()), "410284008d96660f14babe708b5e61853c9f5929bc90dd9874485bf4d6dc32d3e6f22eaa0038ec4973ab9773dfcbf170b8d27d36d89b85c3145e038d68914de83cf1f7aca24af64c55ec51ba9f45c5a4d74a9917dee380e9171108921c3e5546e05be15206050104000500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402"); +} + TEST(PolkadotSigner, SignBond_8da66d) { auto input = Proto::SigningInput(); input.set_genesis_hash(genesisHash.data(), genesisHash.size());