Skip to content

Commit

Permalink
[Polkadot] prepare for MultiAddress::AccountId (#1304)
Browse files Browse the repository at this point in the history
  • Loading branch information
hewigovens committed Feb 23, 2021
1 parent 7fd0ef1 commit 0ee01fa
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 37 deletions.
39 changes: 25 additions & 14 deletions src/Polkadot/Extrinsic.cpp
Expand Up @@ -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";
Expand All @@ -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<const std::string, Data> 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}},
Expand Down Expand Up @@ -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));
Expand All @@ -70,22 +81,22 @@ 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);
auto value = load(transfer.value());
// 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;
Expand All @@ -98,7 +109,7 @@ Data Extrinsic::encodeBatchCall(const std::vector<Data>& 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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<Data>{call1, call2};
Expand Down Expand Up @@ -180,14 +191,14 @@ Data Extrinsic::encodeStakingCall(const Proto::Staking& staking, TWSS58AddressTy

case Proto::Staking::kNominate:
{
std::vector<SS58Address> addresses;
std::vector<SS58Address> 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;

Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/Polkadot/Extrinsic.h
Expand Up @@ -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())
Expand All @@ -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);
}

Expand All @@ -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<Data>& calls, TWSS58AddressType network);
Data encodeEraNonceTip() const;
};
Expand Down
20 changes: 9 additions & 11 deletions src/Polkadot/ScaleCodec.h
Expand Up @@ -98,23 +98,21 @@ inline Data encodeVector(const std::vector<Data>& 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<SS58Address>& addresses) {
inline Data encodeAccountIds(const std::vector<SS58Address>& addresses, bool raw) {
std::vector<Data> vec;
for (auto addr : addresses) {
vec.push_back(encodeAddress(addr));
vec.push_back(encodeAccountId(addr.keyBytes(), raw));
}
return encodeVector(vec);
}
Expand Down
5 changes: 5 additions & 0 deletions src/SS58Address.h
Expand Up @@ -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) {
Expand Down
33 changes: 30 additions & 3 deletions swift/Tests/Blockchains/PolkadotTests.swift
Expand Up @@ -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'")
Expand All @@ -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")
}

Expand Down
14 changes: 8 additions & 6 deletions tests/Polkadot/ScaleCodecTests.cpp
Expand Up @@ -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<SS58Address>{
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) {
Expand Down
33 changes: 33 additions & 0 deletions tests/Polkadot/SignerTests.cpp
Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit 0ee01fa

Please sign in to comment.