diff --git a/src/block_template.cpp b/src/block_template.cpp index 130a1ec..bc1e391 100644 --- a/src/block_template.cpp +++ b/src/block_template.cpp @@ -689,6 +689,8 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vectorm_outputs.clear(); m_poolBlockTemplate->m_outputs.reserve(num_outputs); + const uint8_t tx_type = m_poolBlockTemplate->get_tx_type(); + uint64_t reward_amounts_weight = 0; for (size_t i = 0; i < num_outputs; ++i) { writeVarint(m_rewards[i], [this, &reward_amounts_weight](uint8_t b) @@ -696,18 +698,24 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vectorget_eph_public_key(m_txkeySec, i, eph_public_key)) { + if (!shares[i].m_wallet->get_eph_public_key(m_txkeySec, i, eph_public_key, view_tag)) { LOGERR(1, "get_eph_public_key failed at index " << i); } m_minerTx.insert(m_minerTx.end(), eph_public_key.h, eph_public_key.h + HASH_SIZE); - m_poolBlockTemplate->m_outputs.emplace_back(m_rewards[i], eph_public_key); + m_poolBlockTemplate->m_outputs.emplace_back(m_rewards[i], eph_public_key, tx_type, view_tag); + } + + if (tx_type == TXOUT_TO_TAGGED_KEY) { + m_minerTx.emplace_back(view_tag); } } diff --git a/src/common.h b/src/common.h index bf0839c..ee20286 100644 --- a/src/common.h +++ b/src/common.h @@ -81,7 +81,8 @@ namespace p2pool { constexpr size_t HASH_SIZE = 32; -constexpr uint8_t HARDFORK_SUPPORTED_VERSION = 14; +constexpr uint8_t HARDFORK_VIEW_TAGS_VERSION = 15; +constexpr uint8_t HARDFORK_SUPPORTED_VERSION = 16; constexpr uint8_t MINER_REWARD_UNLOCK_TIME = 60; constexpr uint8_t NONCE_SIZE = 4; constexpr uint8_t EXTRA_NONCE_SIZE = 4; @@ -89,6 +90,7 @@ constexpr uint8_t EXTRA_NONCE_MAX_SIZE = EXTRA_NONCE_SIZE + 10; constexpr uint8_t TX_VERSION = 2; constexpr uint8_t TXIN_GEN = 0xFF; constexpr uint8_t TXOUT_TO_KEY = 2; +constexpr uint8_t TXOUT_TO_TAGGED_KEY = 3; constexpr uint8_t TX_EXTRA_TAG_PUBKEY = 1; constexpr uint8_t TX_EXTRA_NONCE = 2; constexpr uint8_t TX_EXTRA_MERGE_MINING_TAG = 3; diff --git a/src/crypto.cpp b/src/crypto.cpp index 67cf9df..bb83231 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -144,17 +144,19 @@ public: uv_mutex_destroy(&m); } - bool get_derivation(const hash& key1, const hash& key2, hash& derivation) + bool get_derivation(const hash& key1, const hash& key2, size_t output_index, hash& derivation, uint8_t& view_tag) { - std::array index; + std::array index; memcpy(index.data(), key1.h, HASH_SIZE); memcpy(index.data() + HASH_SIZE, key2.h, HASH_SIZE); + memcpy(index.data() + HASH_SIZE * 2, &output_index, sizeof(size_t)); { MutexLock lock(m); auto it = derivations.find(index); if (it != derivations.end()) { - derivation = it->second; + derivation = it->second.derivation; + view_tag = it->second.view_tag; return true; } } @@ -172,9 +174,11 @@ public: ge_p1p1_to_p2(&point2, &point3); ge_tobytes(reinterpret_cast(&derivation), &point2); + derive_view_tag(derivation, output_index, view_tag); + { MutexLock lock(m); - derivations.emplace(index, derivation); + derivations.emplace(index, DerivationEntry{ derivation, view_tag } ); } return true; @@ -231,16 +235,22 @@ public: } private: + struct DerivationEntry + { + hash derivation; + uint8_t view_tag; + }; + uv_mutex_t m; - unordered_map, hash> derivations; + unordered_map, DerivationEntry> derivations; unordered_map, hash> public_keys; }; static Cache* cache = nullptr; -bool generate_key_derivation(const hash& key1, const hash& key2, hash& derivation) +bool generate_key_derivation(const hash& key1, const hash& key2, size_t output_index, hash& derivation, uint8_t& view_tag) { - return cache->get_derivation(key1, key2, derivation); + return cache->get_derivation(key1, key2, output_index, derivation, view_tag); } bool derive_public_key(const hash& derivation, size_t output_index, const hash& base, hash& derived_key) @@ -248,6 +258,22 @@ bool derive_public_key(const hash& derivation, size_t output_index, const hash& return cache->get_public_key(derivation, output_index, base, derived_key); } +void derive_view_tag(const hash& derivation, size_t output_index, uint8_t& view_tag) +{ + constexpr uint8_t salt[] = "view_tag"; + constexpr size_t SALT_SIZE = sizeof(salt) - 1; + + uint8_t buf[64]; + memcpy(buf, salt, SALT_SIZE); + memcpy(buf + SALT_SIZE, derivation.h, HASH_SIZE); + uint8_t* p = buf + SALT_SIZE + HASH_SIZE; + writeVarint(output_index, [&p](uint8_t b) { *(p++) = b; }); + + hash view_tag_full; + keccak(buf, static_cast(p - buf), view_tag_full.h, HASH_SIZE); + view_tag = view_tag_full.h[0]; +} + void init_crypto_cache() { if (!cache) { diff --git a/src/crypto.h b/src/crypto.h index 21f7d32..490a33e 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -21,8 +21,9 @@ namespace p2pool { void generate_keys(hash& pub, hash& sec); bool check_keys(const hash& pub, const hash& sec); -bool generate_key_derivation(const hash& key1, const hash& key2, hash& derivation); +bool generate_key_derivation(const hash& key1, const hash& key2, size_t output_index, hash& derivation, uint8_t& view_tag); bool derive_public_key(const hash& derivation, size_t output_index, const hash& base, hash& derived_key); +void derive_view_tag(const hash& derivation, size_t output_index, uint8_t& view_tag); void init_crypto_cache(); void destroy_crypto_cache(); diff --git a/src/p2pool.cpp b/src/p2pool.cpp index d46dc0c..fe88794 100644 --- a/src/p2pool.cpp +++ b/src/p2pool.cpp @@ -72,7 +72,8 @@ p2pool::p2pool(int argc, char* argv[]) hash pub, sec, eph_public_key; generate_keys(pub, sec); - if (!m_params->m_wallet.get_eph_public_key(sec, 0, eph_public_key)) { + uint8_t view_tag; + if (!m_params->m_wallet.get_eph_public_key(sec, 0, eph_public_key, view_tag)) { LOGERR(1, "Invalid wallet address: get_eph_public_key failed"); panic(); } @@ -603,13 +604,15 @@ void p2pool::download_block_headers(uint64_t current_height) }); } + const uint64_t start_height = (current_height > BLOCK_HEADERS_REQUIRED) ? (current_height - BLOCK_HEADERS_REQUIRED) : 0; + s.m_pos = 0; - s << "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"get_block_headers_range\",\"params\":{\"start_height\":" << current_height - BLOCK_HEADERS_REQUIRED << ",\"end_height\":" << current_height - 1 << "}}\0"; + s << "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"get_block_headers_range\",\"params\":{\"start_height\":" << start_height << ",\"end_height\":" << current_height - 1 << "}}\0"; JSONRPCRequest::call(m_params->m_host.c_str(), m_params->m_rpcPort, buf, - [this, current_height](const char* data, size_t size) + [this, start_height, current_height](const char* data, size_t size) { - if (parse_block_headers_range(data, size) == BLOCK_HEADERS_REQUIRED) { + if (parse_block_headers_range(data, size) == current_height - start_height) { update_median_timestamp(); if (m_serversStarted.exchange(1) == 0) { m_ZMQReader = new ZMQReader(m_params->m_host.c_str(), m_params->m_zmqPort, this); @@ -624,14 +627,14 @@ void p2pool::download_block_headers(uint64_t current_height) } } else { - LOGERR(1, "fatal error: couldn't download block headers for heights " << current_height - BLOCK_HEADERS_REQUIRED << " - " << current_height - 1); + LOGERR(1, "fatal error: couldn't download block headers for heights " << start_height << " - " << current_height - 1); panic(); } }, - [current_height](const char* data, size_t size) + [start_height, current_height](const char* data, size_t size) { if (size > 0) { - LOGERR(1, "fatal error: couldn't download block headers for heights " << current_height - BLOCK_HEADERS_REQUIRED << " - " << current_height - 1 << ", error " << log::const_buf(data, size)); + LOGERR(1, "fatal error: couldn't download block headers for heights " << start_height << " - " << current_height - 1 << ", error " << log::const_buf(data, size)); panic(); } }); diff --git a/src/pool_block.cpp b/src/pool_block.cpp index 0584eb0..9c754de 100644 --- a/src/pool_block.cpp +++ b/src/pool_block.cpp @@ -157,8 +157,12 @@ void PoolBlock::serialize_mainchain_data(uint32_t nonce, uint32_t extra_nonce, c for (TxOutput& output : m_outputs) { writeVarint(output.m_reward, m_mainChainData); - m_mainChainData.push_back(TXOUT_TO_KEY); + m_mainChainData.push_back(output.m_txType); m_mainChainData.insert(m_mainChainData.end(), output.m_ephPublicKey.h, output.m_ephPublicKey.h + HASH_SIZE); + + if (output.m_txType == TXOUT_TO_TAGGED_KEY) { + m_mainChainData.push_back(output.m_viewTag); + } } m_mainChainOutputsBlobSize = static_cast(m_mainChainData.size()) - m_mainChainOutputsOffset; @@ -301,9 +305,19 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const uint64_t PoolBlock::get_payout(const Wallet& w) const { for (size_t i = 0, n = m_outputs.size(); i < n; ++i) { + const TxOutput& out = m_outputs[i]; hash eph_public_key; - if ((w.get_eph_public_key(m_txkeySec, i, eph_public_key)) && (eph_public_key == m_outputs[i].m_ephPublicKey)) { - return m_outputs[i].m_reward; + + if (out.m_txType == TXOUT_TO_TAGGED_KEY) { + if (w.get_eph_public_key_with_view_tag(m_txkeySec, i, eph_public_key, out.m_viewTag) && (eph_public_key == out.m_ephPublicKey)) { + return out.m_reward; + } + } + else { + uint8_t view_tag; + if (w.get_eph_public_key(m_txkeySec, i, eph_public_key, view_tag) && (eph_public_key == out.m_ephPublicKey)) { + return out.m_reward; + } } } diff --git a/src/pool_block.h b/src/pool_block.h index 5ecca39..e9afc64 100644 --- a/src/pool_block.h +++ b/src/pool_block.h @@ -81,11 +81,13 @@ struct PoolBlock struct TxOutput { - FORCEINLINE TxOutput() : m_reward(0), m_ephPublicKey() {} - FORCEINLINE TxOutput(uint64_t r, const hash& k) : m_reward(r), m_ephPublicKey(k) {} + FORCEINLINE TxOutput() : m_reward(0), m_ephPublicKey(), m_txType(0), m_viewTag(0) {} + FORCEINLINE TxOutput(uint64_t r, const hash& k, uint8_t tx_type, uint8_t view_tag) : m_reward(r), m_ephPublicKey(k), m_txType(tx_type), m_viewTag(view_tag) {} uint64_t m_reward; hash m_ephPublicKey; + uint8_t m_txType; + uint8_t m_viewTag; }; std::vector m_outputs; @@ -139,6 +141,10 @@ struct PoolBlock bool get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const hash& seed_hash, hash& pow_hash); uint64_t get_payout(const Wallet& w) const; + + // Both tx types are allowed by Monero consensus during v15 because it needs to process pre-fork mempool transactions, + // but P2Pool can switch to using only TXOUT_TO_TAGGED_KEY for miner payouts starting from v15 + FORCEINLINE uint8_t get_tx_type() const { return (m_majorVersion < HARDFORK_VIEW_TAGS_VERSION) ? TXOUT_TO_KEY : TXOUT_TO_TAGGED_KEY; } }; } // namespace p2pool diff --git a/src/pool_block_parser.inl b/src/pool_block_parser.inl index eb2e9ab..9849d4a 100644 --- a/src/pool_block_parser.inl +++ b/src/pool_block_parser.inl @@ -99,7 +99,7 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, SideChain& sidechai if (num_outputs > 0) { // Outputs are in the buffer, just read them // Each output is at least 34 bytes, exit early if there's not enough data left - // 1 byte for reward, 1 byte for TXOUT_TO_KEY, 32 bytes for eph_pub_key + // 1 byte for reward, 1 byte for tx_type, 32 bytes for eph_pub_key constexpr uint64_t MIN_OUTPUT_SIZE = 34; if (num_outputs > std::numeric_limits::max() / MIN_OUTPUT_SIZE) return __LINE__; @@ -108,15 +108,23 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, SideChain& sidechai m_outputs.clear(); m_outputs.reserve(num_outputs); + const uint8_t expected_tx_type = get_tx_type(); + for (uint64_t i = 0; i < num_outputs; ++i) { TxOutput t; READ_VARINT(t.m_reward); total_reward += t.m_reward; - EXPECT_BYTE(TXOUT_TO_KEY); + EXPECT_BYTE(expected_tx_type); + t.m_txType = expected_tx_type; + READ_BUF(t.m_ephPublicKey.h, HASH_SIZE); + if (expected_tx_type == TXOUT_TO_TAGGED_KEY) { + READ_BYTE(t.m_viewTag); + } + m_outputs.emplace_back(std::move(t)); } diff --git a/src/side_chain.cpp b/src/side_chain.cpp index 5364948..0e06571 100644 --- a/src/side_chain.cpp +++ b/src/side_chain.cpp @@ -613,13 +613,17 @@ bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::v PoolBlock* b = it->second; const size_t n = b->m_outputs.size(); - blob.reserve(n * 38 + 64); + blob.reserve(n * 39 + 64); writeVarint(n, blob); for (const PoolBlock::TxOutput& output : b->m_outputs) { writeVarint(output.m_reward, blob); - blob.emplace_back(TXOUT_TO_KEY); + blob.emplace_back(output.m_txType); blob.insert(blob.end(), output.m_ephPublicKey.h, output.m_ephPublicKey.h + HASH_SIZE); + + if (output.m_txType == TXOUT_TO_TAGGED_KEY) { + blob.emplace_back(output.m_viewTag); + } } block->m_outputs = b->m_outputs; @@ -632,25 +636,32 @@ bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::v const size_t n = m_tmpShares.size(); - blob.reserve(n * 38 + 64); + blob.reserve(n * 39 + 64); writeVarint(n, blob); block->m_outputs.clear(); block->m_outputs.reserve(n); + const uint8_t tx_type = block->get_tx_type(); + hash eph_public_key; for (size_t i = 0; i < n; ++i) { writeVarint(m_tmpRewards[i], blob); - blob.emplace_back(TXOUT_TO_KEY); + blob.emplace_back(tx_type); - if (!m_tmpShares[i].m_wallet->get_eph_public_key(block->m_txkeySec, i, eph_public_key)) { + uint8_t view_tag; + if (!m_tmpShares[i].m_wallet->get_eph_public_key(block->m_txkeySec, i, eph_public_key, view_tag)) { LOGWARN(6, "get_eph_public_key failed at index " << i); } blob.insert(blob.end(), eph_public_key.h, eph_public_key.h + HASH_SIZE); - block->m_outputs.emplace_back(m_tmpRewards[i], eph_public_key); + if (tx_type == TXOUT_TO_TAGGED_KEY) { + blob.emplace_back(view_tag); + } + + block->m_outputs.emplace_back(m_tmpRewards[i], eph_public_key, tx_type, view_tag); } return true; @@ -737,14 +748,24 @@ void SideChain::print_status() } Wallet w = m_pool->params().m_wallet; - const std::vector& outs = tip->m_outputs; hash eph_public_key; - for (size_t i = 0, n = outs.size(); i < n; ++i) { - if (w.get_eph_public_key(tip->m_txkeySec, i, eph_public_key) && (outs[i].m_ephPublicKey == eph_public_key)) { - your_reward = outs[i].m_reward; + for (size_t i = 0, n = tip->m_outputs.size(); i < n; ++i) { + const PoolBlock::TxOutput& out = tip->m_outputs[i]; + if (!your_reward) { + if (out.m_txType == TXOUT_TO_TAGGED_KEY) { + if (w.get_eph_public_key_with_view_tag(tip->m_txkeySec, i, eph_public_key, out.m_viewTag) && (out.m_ephPublicKey == eph_public_key)) { + your_reward = out.m_reward; + } + } + else { + uint8_t view_tag; + if (w.get_eph_public_key(tip->m_txkeySec, i, eph_public_key, view_tag) && (out.m_ephPublicKey == eph_public_key)) { + your_reward = out.m_reward; + } + } } - total_reward += outs[i].m_reward; + total_reward += out.m_reward; } } @@ -1320,17 +1341,20 @@ void SideChain::verify(PoolBlock* block) } for (size_t i = 0, n = rewards.size(); i < n; ++i) { - if (rewards[i] != block->m_outputs[i].m_reward) { + const PoolBlock::TxOutput& out = block->m_outputs[i]; + + if (rewards[i] != out.m_reward) { LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << - " has invalid reward at index " << i << ": got " << block->m_outputs[i].m_reward << ", expected " << rewards[i]); + " has invalid reward at index " << i << ": got " << out.m_reward << ", expected " << rewards[i]); block->m_invalid = true; return; } hash eph_public_key; - if (!shares[i].m_wallet->get_eph_public_key(block->m_txkeySec, i, eph_public_key)) { + uint8_t view_tag; + if (!shares[i].m_wallet->get_eph_public_key(block->m_txkeySec, i, eph_public_key, view_tag)) { LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << @@ -1339,7 +1363,16 @@ void SideChain::verify(PoolBlock* block) return; } - if (eph_public_key != block->m_outputs[i].m_ephPublicKey) { + if ((out.m_txType == TXOUT_TO_TAGGED_KEY) && (out.m_viewTag != view_tag)) { + LOGWARN(3, "block at height = " << block->m_sidechainHeight << + ", id = " << block->m_sidechainId << + ", mainchain height = " << block->m_txinGenHeight << + " has an incorrect view tag at index " << i); + block->m_invalid = true; + return; + } + + if (eph_public_key != out.m_ephPublicKey) { LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << diff --git a/src/wallet.cpp b/src/wallet.cpp index c18bcd0..940cf55 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -195,10 +195,25 @@ bool Wallet::assign(const hash& spend_pub_key, const hash& view_pub_key, Network return true; } -bool Wallet::get_eph_public_key(const hash& txkey_sec, size_t output_index, hash& eph_public_key) const +bool Wallet::get_eph_public_key(const hash& txkey_sec, size_t output_index, hash& eph_public_key, uint8_t& view_tag) const { hash derivation; - if (!generate_key_derivation(m_viewPublicKey, txkey_sec, derivation)) { + if (!generate_key_derivation(m_viewPublicKey, txkey_sec, output_index, derivation, view_tag)) { + return false; + } + + if (!derive_public_key(derivation, output_index, m_spendPublicKey, eph_public_key)) { + return false; + } + + return true; +} + +bool Wallet::get_eph_public_key_with_view_tag(const hash& txkey_sec, size_t output_index, hash& eph_public_key, uint8_t expected_view_tag) const +{ + hash derivation; + uint8_t view_tag; + if (!generate_key_derivation(m_viewPublicKey, txkey_sec, output_index, derivation, view_tag) || (view_tag != expected_view_tag)) { return false; } diff --git a/src/wallet.h b/src/wallet.h index 7f5950d..2df8785 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -38,7 +38,8 @@ public: FORCEINLINE const hash& spend_public_key() const { return m_spendPublicKey; } FORCEINLINE const hash& view_public_key() const { return m_viewPublicKey; } - bool get_eph_public_key(const hash& txkey_sec, size_t output_index, hash& eph_public_key) const; + bool get_eph_public_key(const hash& txkey_sec, size_t output_index, hash& eph_public_key, uint8_t& view_tag) const; + bool get_eph_public_key_with_view_tag(const hash& txkey_sec, size_t output_index, hash& eph_public_key, uint8_t expected_view_tag) const; FORCEINLINE bool operator<(const Wallet& w) const { return m_spendPublicKey < w.m_spendPublicKey; } FORCEINLINE bool operator==(const Wallet& w) const { return m_spendPublicKey == w.m_spendPublicKey; } diff --git a/tests/src/crypto_tests.cpp b/tests/src/crypto_tests.cpp index ff254ab..657db02 100644 --- a/tests/src/crypto_tests.cpp +++ b/tests/src/crypto_tests.cpp @@ -42,7 +42,8 @@ TEST(crypto, derivation) if (result) { f >> expected_derivation; } - ASSERT_EQ(p2pool::generate_key_derivation(key1, key2, derivation), result); + uint8_t view_tag; + ASSERT_EQ(p2pool::generate_key_derivation(key1, key2, 0, derivation, view_tag), result); if (result) { ASSERT_EQ(derivation, expected_derivation); } @@ -61,6 +62,20 @@ TEST(crypto, derivation) ASSERT_EQ(derived_key, expected_derived_key); } } + else if (name == "derive_view_tag") { + hash derivation; + uint64_t output_index; + std::string result_str; + f >> derivation >> output_index >> result_str; + uint8_t view_tag; + p2pool::derive_view_tag(derivation, output_index, view_tag); + + char buf[log::Stream::BUF_SIZE + 1]; + log::Stream s(buf); + s << log::hex_buf(&view_tag, 1) << '\0'; + + ASSERT_EQ(buf, result_str); + } } while (!f.eof()); } diff --git a/tests/src/crypto_tests.txt b/tests/src/crypto_tests.txt index 62edab8..e8048e2 100644 --- a/tests/src/crypto_tests.txt +++ b/tests/src/crypto_tests.txt @@ -542,3 +542,59 @@ derive_public_key 5ea95a51ab11b80c7d09d0c8f8952b70c67e81d0fd421bbed43ab77c1b7b30 derive_public_key 2c312ef971def53361274c37a90bfde86f959d877a636ea641a9c976ee80c7e3 121 b611ebd2bcfefc81cb772e35e3dd0204575cb0da644f68d4f9828a2683861e6c false derive_public_key 6fa161dd958022caf185faf873dd9adbc5578352cda505e84fff7cc99a8762a7 333934910 c2b56e207862958751d49643f23079009092c32bf82179a1295e3b85a385c1c3 false derive_public_key d85725562544e1984048391413a6112eb221bd217db3baaa9843bee331000e8e 21880446 f3148c3041e634d829e5df463ba3b1d64df282d620d63485e1f10024e003b939 false +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 0 76 +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 1 d6 +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 2 87 +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 3 1b +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 12 d6 +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 13 e9 +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 14 12 +derive_view_tag 0fc47054f355ced4d67de73bfa12e4c78ff19089548fffa7d07a674741860f97 15 26 +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 0 70 +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 1 81 +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 2 a0 +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 3 ec +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 12 22 +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 13 0a +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 14 87 +derive_view_tag a36ba7b4d31349ad278a6df8f77adb76748b59f4929348e67dd92adb9fa174dc 15 76 +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 0 93 +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 1 67 +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 2 9d +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 3 2d +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 12 63 +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 13 cf +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 14 ef +derive_view_tag 7498d5bf0b69e08653f6d420a17f866dd2bd490ab43074f46065cb501fe7e2d8 15 10 +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 0 90 +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 1 5a +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 2 de +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 3 21 +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 12 57 +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 13 52 +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 14 6f +derive_view_tag fe7770c4b076e95ddb8026affcfab39d31c7c4a2266e0e25e343bc4badc907d0 15 eb +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 0 c6 +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 1 60 +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 2 f0 +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 3 71 +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 12 0e +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 13 42 +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 14 b2 +derive_view_tag ea9337d0ddf480abdc4fc56a0cb223702729cb230ae7b9de50243ad25ce90e8d 15 61 +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 0 4c +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 1 9b +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 2 64 +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 3 ff +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 12 e3 +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 13 24 +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 14 ea +derive_view_tag 25d538315bcb81aff9574189ea65f418aeb0392f5cbbc84cd8a33c7ade31ef0a 15 3b +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 0 74 +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 1 77 +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 2 a9 +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 3 44 +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 12 75 +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 13 05 +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 14 ca +derive_view_tag 8edfabada2b24ef4d8d915826c9ff0245910e4b835b59c2cf8ed8fc991b2e1e8 15 00