LoginSignup
13
5

symbol transfer transaction 中身を全力で読む

Last updated at Posted at 2024-01-02

自分用。読むといっても専門用語はあまり知らないので自分語で自分が見直す用に書くので、もし誰かが見たとしてもヒントになればいいかな程度かもしれませんね。とりあえず読んでみて、そのあと作り方までいけたらオリジナルもいけんじゃね?と。アウトプットは最強のインプット。ということで始める。

すでにpluginにディレクトリがあればだいたい使いまわしでいけそう、全くのオリジナルの場合は、Extenstionと同じで色々フォルダ作ったりCMakeいじったりとかだと思われる。

いくつか疑問が残っていた箇所に対してジャガーさんが回答してくれた。
https://catnotes.xyz/symbol/q-a/toshi-transfer-transaction

前提

プラグイントランザクションは主に以下の要素から構成されているようです

  • モデル:トランザクションのデータ構造を定義します。
  • プラグイン本体:トランザクションのロジックを実装します。

これらの要素は以下のコンポーネントと連携して動作します:

  • 通知(Notification):バリデータやオブザーバが動作するためのトリガーとなるイベントです。
  • バリデータ(Validator):トランザクションのバリデーションを担当します。トランザクションが正しい形式と内容であることを確認します。
  • オブザーバ(Observer):通知を観察し、特定のイベントに対するアクションを実行します。

model

さて、まずはここかな。まず、これは覚えておいてください

DEFINE_TRANSACTION_CONSTANTS(Entity_Type_Transfer, 1)

今後このように大文字&アンダーバーで書かれているものはマクロの呼び出しで、このマクロを通してコードが出力されます。
例えば、DEFINE_TRANSACTION_CONSTANTSの中身は

/// Defines constants for a transaction with \a TYPE and \a VERSION.
#define DEFINE_TRANSACTION_CONSTANTS(TYPE, VERSION) \
	/* Transaction format version. */ \
	static constexpr uint8_t Current_Version = VERSION; \
	/* Transaction type. */ \
	static constexpr EntityType Entity_Type = TYPE;

これを出力すると

public:
    /* Transaction format version. */
    static constexpr uint8_t Current_Version = 1;
    /* Transaction type. */
    static constexpr EntityType Entity_Type = Entity_Type_Transfer;

となります。ではmodelをマクロは出力済で見ていきましょう

/symbol/client/catapult/plugins/txes/transfer/src/model/TransferTransaction.h
namespace catapult { namespace model {

#pragma pack(push, 1)

	/// Binary layout for a transfer transaction body.
	template<typename THeader>
	struct TransferTransactionBody : public THeader {
	private:
		using TransactionType = TransferTransactionBody<THeader>;

	public:
        /* Transaction format version. */
        static constexpr uint8_t Current_Version = 1;
        /* Transaction type. */
        static constexpr EntityType Entity_Type = Entity_Type_Transfer;

	public:
		/// Recipient address.
		UnresolvedAddress RecipientAddress;

		/// Message size in bytes.
		uint16_t MessageSize;

		/// Number of mosaics.
		uint8_t MosaicsCount;

		/// Reserved padding to align Mosaics on 8-byte boundary.
		uint32_t TransferTransactionBody_Reserved1;
		uint8_t TransferTransactionBody_Reserved2;

		// followed by mosaics data if MosaicsCount != 0
		/* Returns a const pointer to the typed data contained in this entity. */
        const UnresolvedMosaic* MosaicsPtr() const {
            return reinterpret_cast<const UnresolvedMosaic*>(MosaicsPtrT(*this));
        }
        
        /* Returns a pointer to the typed data contained in this entity. */
        UnresolvedMosaic* MosaicsPtr() {
            return reinterpret_cast<UnresolvedMosaic*>(MosaicsPtrT(*this));
        }

		// followed by message data if MessageSize != 0
		/* Returns a const pointer to the typed data contained in this entity. */
        const uint8_t* MessagePtr() const {
            return reinterpret_cast<const uint8_t*>(MessagePtrT(*this));
        }
        
        /* Returns a pointer to the typed data contained in this entity. */
        uint8_t* MessagePtr() {
            return reinterpret_cast<uint8_t*>(MessagePtrT(*this));
        }

	private:
		template<typename T>
		static auto* MosaicsPtrT(T& transaction) {
			return transaction.MosaicsCount ? THeader::PayloadStart(transaction) : nullptr;
		}

		template<typename T>
		static auto* MessagePtrT(T& transaction) {
			auto* pPayloadStart = THeader::PayloadStart(transaction);
			return transaction.MessageSize && pPayloadStart
					? pPayloadStart + transaction.MosaicsCount * sizeof(UnresolvedMosaic)
					: nullptr;
		}

	public:
		/// Calculates the real size of transfer \a transaction.
		static constexpr uint64_t CalculateRealSize(const TransactionType& transaction) noexcept {
			return sizeof(TransactionType) + transaction.MessageSize + transaction.MosaicsCount * sizeof(UnresolvedMosaic);
		}
	};
 
	struct EmbeddedTransferTransaction : public TransferTransactionBody<model::EmbeddedTransaction> {};
    struct TransferTransaction : public TransferTransactionBody<model::Transaction> {};

#pragma pack(pop)
}}

これでだいぶ見やすくなりました。

このファイルではEmbeddedTransferTransactionTransferTransactionの2つの構造体を定義している。それらは両方ともTransferTransactionBodyであるが、それぞれmodel::EmbeddedTransactionmodel::Transactionを継承している(ここでその違いは触れないけど、なんとなく想像つくと思う。

TransferTransactionBody構造体の中身はUnresolvedAddress RecipientAddress;とかが定義されてる。なのでこれを真似るなら型が一般的な物(uint16_tみたいな)はそのまま書いて自作(カタパルト)モデルならマクロを使う。さらにオリジナルを使うならマクロも書く必要がある

plugin

例のごとくマクロは出力済みで書きます、そのほうが分かりやすい

/symbol/client/catapult/plugins/txes/transfer/src/plugins/TransferPlugin.h
#pragma once
#include "catapult/plugins.h"

namespace catapult { namespace plugins { class PluginManager; } }

namespace catapult { namespace plugins {

	/// Registers transfer support with \a manager.
	std::unique_ptr<model::TransactionPlugin> __attribute__ ((visibility ("default"))) CreateStealTransactionPlugin();  // UNIX系環境
	void RegisterTransferSubsystem(PluginManager& manager);
}}

このヘッダファイルではRegisterTransferSubsystemという関数を外部から呼び出すための定義が行われているようです。なので必要だけどコピペでいいと思う。中身のほうを見よう

/symbol/client/catapult/plugins/txes/transfer/src/plugins/TransferPlugin.cpp
#include "TransferPlugin.h"
#include "TransferTransactionPlugin.h"
#include "src/config/TransferConfiguration.h"
#include "src/observers/Observers.h"
#include "src/validators/Validators.h"
#include "catapult/config/CatapultDataDirectory.h"
#include "catapult/config/CatapultKeys.h"
#include "catapult/crypto/OpensslKeyUtils.h"
#include "catapult/model/Address.h"
#include "catapult/plugins/PluginManager.h"
#include "StealTransactionPlugin.h"

namespace catapult { namespace plugins {

	void RegisterTransferSubsystem(PluginManager& manager) {
		manager.addTransactionSupport(CreateTransferTransactionPlugin());
		manager.addTransactionSupport(CreateStealTransactionPlugin());

		auto config = model::LoadPluginConfiguration<config::TransferConfiguration>(manager.config(), "catapult.plugins.transfer");
		manager.addStatelessValidatorHook([config](auto& builder) {
			builder.add(validators::CreateTransferMessageValidator(config.MaxMessageSize));
			builder.add(validators::CreateTransferMosaicsValidator());
		});

		if (!manager.userConfig().EnableDelegatedHarvestersAutoDetection)
			return;

		auto encryptionPrivateKeyPemFilename = config::GetNodePrivateKeyPemFilename(manager.userConfig().CertificateDirectory);
		auto encryptionPublicKey = crypto::ReadPublicKeyFromPrivateKeyPemFile(encryptionPrivateKeyPemFilename);
		auto recipient = model::PublicKeyToAddress(encryptionPublicKey, manager.config().Network.Identifier);
		auto dataDirectory = config::CatapultDataDirectory(manager.userConfig().DataDirectory);
		manager.addObserverHook([recipient, dataDirectory](auto& builder) {
			builder.add(observers::CreateTransferMessageObserver(0xE201735761802AFE, recipient, dataDirectory.dir("transfer_message")));
		});
	}
}}

extern "C" PLUGIN_API
void RegisterSubsystem(catapult::plugins::PluginManager& manager) {
	catapult::plugins::RegisterTransferSubsystem(manager);
}

RegisterSubsystemという関数の引数にPluginManagerが渡され、そこでmanagerのtransactionSuportにCreateTransferTransactionPluginを渡してるね。このあとTransferTransactionPluginを見ていくけど、ここで追加していることが見て取れる。

TransferConfigurationをロードして、設定に合わせてバリデータの追加、Transferのバリデータ追加はCreateTransferMessageValidatorCreateTransferMosaicsValidatorということが見て取れる。(TransferPluginだけで使うものを追加してるという意味)

その後、EnableDelegatedHarvestersAutoDetectionがtrueであればいくつかのオブザーバーを登録している。

どこでPluginは登録されているのか

このRegisterSubsystemはどこで呼び出されるかというと、

/symbol/client/catapult/src/catapult/local/server/LocalNode.cpp
...
public:
    void boot() {
        CATAPULT_LOG(info) << "registering system plugins";
        m_pluginModules = LoadAllPlugins(*m_pBootstrapper);
...

ここでプラグインローダーで全部ロード。ローダーの中身は

/symbol/client/catapult/src/catapult/local/HostUtils.cpp
...
class BootstrapperPluginLoader {
    public:
        explicit BootstrapperPluginLoader(extensions::ProcessBootstrapper& bootstrapper)
                : m_config(bootstrapper.config())
                , m_extensionManager(bootstrapper.extensionManager())
                , m_pluginManager(bootstrapper.pluginManager())
        {}

    public:
        const std::vector<plugins::PluginModule>& modules() {
            return m_pluginModules;
        }

    public:
        void loadAll() {
            for (const auto& pluginName : m_extensionManager.systemPluginNames())
                loadOne(pluginName);

            for (const auto& pair : m_config.Blockchain.Plugins)
                loadOne(pair.first);
        }

    private:
        void loadOne(const std::string& pluginName) {
            LoadPluginByName(m_pluginManager, m_pluginModules, m_config.User.PluginsDirectory, pluginName);
        }

    private:
        const config::CatapultConfiguration& m_config;
        const extensions::ExtensionManager& m_extensionManager;
        plugins::PluginManager& m_pluginManager;

        std::vector<plugins::PluginModule> m_pluginModules;
    };
}

std::vector<plugins::PluginModule> LoadAllPlugins(extensions::ProcessBootstrapper& bootstrapper) {
    BootstrapperPluginLoader loader(bootstrapper);
    loader.loadAll();
    return loader.modules();
}

m_extensionManager.systemPluginNames(){ "catapult.plugins.coresystem", "catapult.plugins.signature" }のようなのでこの2つは必須で他はconfigによって変わるようです。

/symbol/client/catapult/src/catapult/plugins/PluginLoader.cpp
...
void LoadPlugin(PluginManager& manager, const PluginModule& module, const char* symbolName) {
    auto registerSubsystem = module.symbol<decltype(::RegisterSubsystem)*>(symbolName);

    try {
        registerSubsystem(manager);
    } catch (...) {
        // since the module will be unloaded after this function exits, throw a copy of the exception that
        // is not dependent on the (soon to be unloaded) module
        auto exInfo = boost::diagnostic_information(boost::current_exception());
        CATAPULT_THROW_AND_LOG_0(plugin_load_error, exInfo.c_str());
    }
}
}

void LoadPluginByName(PluginManager& manager, PluginModules& modules, const std::string& directory, const std::string& name) {
    CATAPULT_LOG(info) << "registering dynamic plugin " << name;

    modules.emplace_back(directory, name);
    LoadPlugin(manager, modules.back(), "RegisterSubsystem");
}

最後にここregisterSubsystem(manager);でmanagerを渡してます。

TransferTransactionPlugin

/symbol/client/catapult/plugins/txes/transfer/src/plugins/TransferTransactionPlugin.h
#pragma once
#include "catapult/plugins.h"
#include <memory>

namespace catapult { namespace model { class TransactionPlugin; } }

namespace catapult { namespace plugins {

	/// Creates a transfer transaction plugin.
	PLUGIN_API
	std::unique_ptr<model::TransactionPlugin> CreateTransferTransactionPlugin();
}}

先程追加したCreateTransferTransactionPluginのヘッダファイル、外部から呼び出せるようにうんぬんかんぬん
中身は以下

/symbol/client/catapult/plugins/txes/transfer/src/plugins/TransferTransactionPlugin.cpp
#include "TransferTransactionPlugin.h"
#include "src/model/TransferNotifications.h"
#include "src/model/TransferTransaction.h"
#include "catapult/model/NotificationSubscriber.h"
#include "catapult/model/TransactionPluginFactory.h"

using namespace catapult::model;

namespace catapult { namespace plugins {

	namespace {
		template<typename TTransaction>
		void Publish(const TTransaction& transaction, const PublishContext& context, NotificationSubscriber& sub) {
			auto padding = transaction.TransferTransactionBody_Reserved1 << 8 | transaction.TransferTransactionBody_Reserved2;
			sub.notify(InternalPaddingNotification(padding));
			sub.notify(AccountAddressNotification(transaction.RecipientAddress));
			sub.notify(AddressInteractionNotification(context.SignerAddress, transaction.Type, { transaction.RecipientAddress }));

			const auto* pMosaics = transaction.MosaicsPtr();
			for (auto i = 0u; i < transaction.MosaicsCount; ++i) {
				sub.notify(BalanceTransferNotification(
						context.SignerAddress,
						transaction.RecipientAddress,
						pMosaics[i].MosaicId,
						pMosaics[i].Amount));
			}

			if (transaction.MessageSize) {
				sub.notify(TransferMessageNotification(
						transaction.SignerPublicKey,
						transaction.RecipientAddress,
						transaction.MessageSize,
						transaction.MessagePtr()));
			}

			if (transaction.MosaicsCount)
				sub.notify(TransferMosaicsNotification(transaction.MosaicsCount, pMosaics));
		}
	}

	std::unique_ptr<TransactionPlugin> CreateTransferTransactionPlugin() {
    using Factory = TransactionPluginFactory<TransactionPluginFactoryOptions::Default>;
    return Factory::Create<TransferTransaction, EmbeddedTransferTransaction>(
            Publish<TransferTransaction>,
            Publish<EmbeddedTransferTransaction>);
    }
}}

マクロは出力してます

さっき登録したCreateTransferTransactionPluginですね。
この関数自体はTransferTransactionPluginを作成する。TransactionPluginFactoryを使用して、TransferTransactionEmbeddedTransferTransactionのためのプラグインを作成し、これらのトランザクションが発生したときにPublish()関数を呼び出す

このPublish関数が、トランザクションの中身ですね。

一個一個見ていきます、自分のためにね

auto padding = transaction.TransferTransactionBody_Reserved1 << 8 | transaction.TransferTransactionBody_Reserved2;
sub.notify(InternalPaddingNotification(padding));

TransferTransactionBody_Reserved1TransferTransactionBody_Reserved2も0なのでそれを通知しています。これら通知がなんのためにあるかはマクロが使われているため少し探さなければいけない。

通知先の探し方メモ

まず、その通知がコアなのかプラグイン専用なのか
TransferならTransferPlugin.cppを見る

/symbol/client/catapult/plugins/txes/transfer/src/plugins/TransferPlugin.cpp
manager.addStatelessValidatorHook([config](auto& builder) {
			builder.add(validators::CreateTransferMessageValidator(config.MaxMessageSize));
			builder.add(validators::CreateTransferMosaicsValidator());
		});
...
manager.addObserverHook([recipient, dataDirectory](auto& builder) {
			builder.add(observers::CreateTransferMessageObserver(0xE201735761802AFE, recipient, dataDirectory.dir("transfer_message")));
		});

この例ならTransferMessageValidator, TransferMosaicsValidator, TransferMessageObserverこれらはこのプラグイン専用、そうじゃなければコアで登録されている

例)InternalPaddingNotificationの場合
これはTransferPlugin.cppで登録されていない。その場合は以下

/symbol/client/catapult/plugins/coresystem/src/observers/Observers.h
// ここにはない、つまりオブザーバーは無い
/symbol/client/catapult/plugins/coresystem/src/validators/Validators.h
/// Validator that applies to all internal padding notifications and validates that:
/// - internal padding is zero
DECLARE_STATELESS_VALIDATOR(ZeroInternalPadding, model::InternalPaddingNotification)();

バリデーターにはありました。バリデーター名はZeroInternalPaddingです。同階層のZeroInternalPaddingValidator.cppがバリデーターの本体です

/symbol/client/catapult/plugins/coresystem/src/validators/ZeroInternalPaddingValidator.cpp
#include "Validators.h"

namespace catapult { namespace validators {

	using Notification = model::InternalPaddingNotification;

	DEFINE_STATELESS_VALIDATOR(ZeroInternalPadding, [](const Notification& notification) {
		return 0 == notification.Padding ? ValidationResult::Success : Failure_Core_Nonzero_Internal_Padding;
	})
}}

ほい、きた。
paddingは0じゃないと駄目らしい。

AddressInteractionNotificationについてはこちら

stateful::NotificationValidatorPointerT<validators::Notification> CreateRemoteInteractionValidator() {
    return std::make_unique<stateful::FunctionalNotificationValidatorT<validators::Notification>>("RemoteInteractionValidator", [](const Notification& notification, const ValidatorContext& context) {
        if (model::AccountKeyLinkTransaction::Entity_Type == notification.TransactionType)
            return ValidationResult::Success;

        const auto& cache = context.Cache.sub<cache::AccountStateCache>();
        const auto& addresses = notification.ParticipantsByAddress;
        auto predicate = [&cache, &context](const auto& address) {
            return IsRemote(cache, GetResolvedKey(address, context.Resolvers));
        };
        return std::any_of(addresses.cbegin(), addresses.cend(), predicate)
                ? Failure_AccountLink_Remote_Account_Participant_Prohibited
                : ValidationResult::Success;
    });
}

ちょっと中身見ていくの面倒なので飛ばします。たぶん、アカウントにキーリンクされてるアドレスはTransferTransactionの受信者にできないとかそんな感じっぽい。

これに関してのジャガーさんの回答
https://catnotes.xyz/symbol/q-a/toshi-transfer-transaction#remote-interaction-validator

https://docs.symbol.dev/ja/guides/harvesting/activating-delegated-harvesting-manual.html#prerequisites

リモートアカウント (R) は M とノード間でプロキシとして機能します。このアカウントは トランザクションを送受信したことがない 必要があり、デリゲートアカウントである間はトランザクションに関与できません。

と、はっきり書かれている。

とりあえず、トランザクション内で通知するということは=バリデートしなければいけない何かがあるということのはず(間違ってたら指摘してください)バリデートに通らなければエラーを返してトランザクションを通さない。コントラクトの重要な箇所。

※追記
これはちょっと違ってて、通知の対象は
バリデート or オブザーバー or 両方
なお、ここを見ればどこに通知してるか分かる

/symbol/client/catapult/plugins/txes/transfer/src/model/TransferNotifications.h
/// Transfer was received with a message.
// 第三引数がCHANNELなのでこれはALL(ALLはBOTHじゃないのはなぜか分からん
DEFINE_TRANSFER_NOTIFICATION(Message, 0x0001, All);

// これはValidatorだけ
/// Transfer was received with at least one mosaic.
DEFINE_TRANSFER_NOTIFICATION(Mosaics, 0x0002, Validator);

※追記ここまで

BalanceTransferNotificationを見てみよう

const auto* pMosaics = transaction.MosaicsPtr();
for (auto i = 0u; i < transaction.MosaicsCount; ++i) {
    sub.notify(BalanceTransferNotification(
        context.SignerAddress,
        transaction.RecipientAddress,
        pMosaics[i].MosaicId,
        pMosaics[i].Amount));
}

トランザクション内のモザイク配列の数だけ通知を設定している
引数は、署名者アドレス、受信者アドレス、モザイクID、Amount

BalanceTransferNotification(
    const ResolvableAddress& sender,
    const ResolvableAddress& recipient,
    UnresolvedMosaicId mosaicId,
    catapult::Amount amount,
    AmountType transferAmountType = AmountType::Static)
    : BasicBalanceNotification(sender, mosaicId, amount)
    , Recipient(recipient)
    , TransferAmountType(transferAmountType)
{}

バリデーション

Balanceバリデーター

/symbol/client/catapult/plugins/coresystem/src/validators/BalanceValidator.cpp

...
    template<typename TNotification>
		ValidationResult CheckBalance(const TNotification& notification, const ValidatorContext& context) {
			const auto& cache = context.Cache.sub<cache::AccountStateCache>();

			Amount amount;
			auto mosaicId = context.Resolvers.resolve(notification.MosaicId);
			if (FindAccountBalance(cache, notification.Sender.resolved(context.Resolvers), mosaicId, amount)) {
				Amount effectiveAmount;
				auto dynamicFeeMultiplier = context.Cache.dependentState().DynamicFeeMultiplier;
				if (TryGetEffectiveAmount(notification, dynamicFeeMultiplier, effectiveAmount) && amount >= effectiveAmount)
					return ValidationResult::Success;
			}

			return Failure_Core_Insufficient_Balance;
		}
	}

    stateful::NotificationValidatorPointerT<BalanceTransferNotification> CreateBalanceTransferValidator() {
        return std::make_unique<stateful::FunctionalNotificationValidatorT<BalanceTransferNotification>>("BalanceTransferValidator", CheckBalance<BalanceTransferNotification>);
    ...
}

つまり通知を飛ばすとこのバリデーションが行われている。内容は簡単に言うと該当のモザイクIDが指定したアドレスに十分な残高があるかどうかをチェック、falseならFailure_Core_Insufficient_Balanceを返す。見たことある文字列のはず。

オブザーバー

また、この通知を監視しているオブザーバーも存在する。それがこちら

/symbol/client/catapult/plugins/coresystem/src/observers/BalanceTransferObserver.cpp

namespace {
    void Transfer(state::AccountState& debitState, state::AccountState& creditState, MosaicId mosaicId, Amount amount) {
        debitState.Balances.debit(mosaicId, amount);
        creditState.Balances.credit(mosaicId, amount);
    }
}
observers::NotificationObserverPointerT<model::BalanceTransferNotification> CreateBalanceTransferObserver() {
    return std::make_unique<observers::FunctionalNotificationObserverT<model::BalanceTransferNotification>>("BalanceTransferObserver", [](
            const model::BalanceTransferNotification& notification,
            const ObserverContext& context) {
        auto& cache = context.Cache.sub<cache::AccountStateCache>();
        auto senderIter = cache.find(notification.Sender.resolved(context.Resolvers));
        auto recipientIter = cache.find(notification.Recipient.resolved(context.Resolvers));

        auto& senderState = senderIter.get();
        auto& recipientState = recipientIter.get();

        auto effectiveAmount = notification.Amount;
        if (model::BalanceTransferNotification::AmountType::Dynamic == notification.TransferAmountType)
            effectiveAmount = Amount(notification.Amount.unwrap() * context.Cache.dependentState().DynamicFeeMultiplier.unwrap());

        auto mosaicId = context.Resolvers.resolve(notification.MosaicId);
        if (NotifyMode::Commit == context.Mode)
            Transfer(senderState, recipientState, mosaicId, effectiveAmount);
        else
            Transfer(recipientState, senderState, mosaicId, effectiveAmount);
    });
}

このオブザーバーは通知を発見したら、最終的にTransfer関数を呼び出している。そのTransferはdebitとcreditという関数をsenderとrecieverに対して実行している

この中身はこちら

/symbol/client/catapult/src/catapult/state/AccountBalances.cpp
AccountBalances& AccountBalances::credit(MosaicId mosaicId, Amount amount) {
    if (IsZero(amount))
        return *this;

    auto iter = m_balances.find(mosaicId);
    if (m_balances.end() == iter)
        m_balances.insert(std::make_pair(mosaicId, amount));
    else
        iter->second = iter->second + amount;

    return *this;
}

AccountBalances& AccountBalances::debit(MosaicId mosaicId, Amount amount) {
    if (IsZero(amount))
        return *this;

    auto iter = m_balances.find(mosaicId);
    auto hasZeroBalance = m_balances.end() == iter;
    if (hasZeroBalance || amount > iter->second) {
        auto currentBalance = hasZeroBalance ? Amount(0) : iter->second;
        std::ostringstream out;
        out
                << "debit amount (" << amount << ") is greater than current balance (" << currentBalance
                << ") for mosaic " << utils::HexFormat(mosaicId);
        CATAPULT_THROW_RUNTIME_ERROR(out.str().c_str());
    }

    iter->second = iter->second - amount;
    if (IsZero(iter->second))
        m_balances.erase(mosaicId);

    return *this;
}

全て中身を見たわけじゃないけど、creditのほうはamountが増加していてdebitのほうはamountが減少している。帳簿上の残高移動ってことだと思います

なお、これらオブザーバーとバリデータは以下のCoreSystemによって登録済である

/symbol/client/catapult/plugins/coresystem/src/plugins/CoreSystem.cpp
...
manager.addStatefulValidatorHook([&config](auto& builder) {
    builder
        ...
        .add(validators::CreateBalanceDebitValidator())
        // ここで登録されている
        .add(validators::CreateBalanceTransferValidator())
        .add(validators::CreateImportanceBlockValidator(
                config.ForkHeights.TotalVotingBalanceCalculationFix,
                config.VotingSetGrouping));
    });

manager.addObserverHook([harvestFeeOptions, &calculator](auto& builder) {
    builder
        ...
        .add(observers::CreateBalanceDebitObserver())
        // ここで登録されている
        .add(observers::CreateBalanceTransferObserver())
        .add(observers::CreateBeneficiaryObserver())
...

TransferMessageNotificationを見てみよう

ここまではこのプラグインじゃなくコアシステムから使ってたけどここからはTransfer専用。なのでTransferPlugin.cppで登録されていたね

やってることはだいたい同じなので途中省きます。

バリデータ

/symbol/client/catapult/plugins/txes/transfer/src/validators/TransferMessageValidator.cpp
DECLARE_STATELESS_VALIDATOR(TransferMessage, Notification)(uint16_t maxMessageSize) {
    return MAKE_STATELESS_VALIDATOR(TransferMessage, [maxMessageSize](const Notification& notification) {
        return notification.MessageSize > maxMessageSize ? Failure_Transfer_Message_Too_Large : ValidationResult::Success;
    });
}

シンプル!分かりやすい!messageのサイズがmaxMessageSize(現在のメインネットなら1024)を超えていればFailure_Transfer_Message_Too_Large

オブザーバー

/symbol/client/catapult/plugins/txes/transfer/src/observers/TransferMessageObserver.cpp
namespace {
    constexpr auto Marker_Size = sizeof(uint64_t);

    using Notification = model::TransferMessageNotification;
}

observers::NotificationObserverPointerT<Notification> CreateTransferMessageObserver(
    uint64_t marker,
    const Address& recipient,
    const config::CatapultDirectory& directory) {
        return std::make_unique<observers::FunctionalNotificationObserverT<Notification>>("TransferMessageObserver", ([marker, recipient, directory](
            const Notification& notification,
            ObserverContext& context) {
        if (notification.MessageSize <= Marker_Size || marker != reinterpret_cast<const uint64_t&>(*notification.MessagePtr))
            return;

        if (recipient != context.Resolvers.resolve(notification.Recipient))
            return;

        io::FileQueueWriter writer(directory.str());
        io::Write8(writer, NotifyMode::Commit == context.Mode ? 0 : 1);
        io::Write(writer, context.Height);
        writer.write(notification.SenderPublicKey);
        writer.write({ notification.MessagePtr + Marker_Size, notification.MessageSize - Marker_Size });
        writer.flush();
    }));
}

どうやら何かのファイルに書き込んでるみたい。メッセージ全文なのかどうかまで見ていない。まぁ、実際にメッセージを使うとなればこのオブザーバーとバリデータを使えよってわけで疲れたので全部は見ません。もし、ガチで使いたいならちゃんと中身理解して使ってくださいね。

これに関してのジャガーさんの回答
https://catnotes.xyz/symbol/q-a/toshi-transfer-transaction#transfer-message-observer

これは通常のトランザクションとは別でハーベスト委任の際に使われるものだと思う。いわゆるPersistentHarvestingDelegationMessageだと思ったが、sdk内のマジックコードはFE2A8061577301E2だった。一方こちらはE201735761802AFEなので何かが違う。
ちなみに解体新書
https://symbol.openapostille.com/apostille/2617CDEDA05679A1FE9F14E826BFAE7D21216041B8BB52F6076EF13BF9792FBB
P85にもそれについて書かれていてマジックコードはE201735761802AFEなのでやはりこの話だとは思うのだが…聞いてみる

※回答をもらった。
E201735761802AFEFE2A8061577301E2のリトルエンディアンだった。以上(気づけよ笑

TransferMosaicsNotificationを見てみよう

最後、これはバリデータだけ

/symbol/client/catapult/plugins/txes/transfer/src/validators/TransferMosaicsValidator.cpp
using Notification = model::TransferMosaicsNotification;

DEFINE_STATELESS_VALIDATOR(TransferMosaics, [](const Notification& notification) {
    // check strict ordering of mosaics
    if (1 >= notification.MosaicsCount)
        return ValidationResult::Success;

    auto pMosaics = notification.MosaicsPtr;
    auto lastMosaicId = pMosaics[0].MosaicId;
    for (auto i = 1u; i < notification.MosaicsCount; ++i) {
        auto currentMosaicId = pMosaics[i].MosaicId;
        if (lastMosaicId >= currentMosaicId)
            return Failure_Transfer_Out_Of_Order_Mosaics;

        lastMosaicId = currentMosaicId;
    }

    return ValidationResult::Success;
})

ザクッと言うと複数モザイクを送信する場合にモザイクIDがuint64の昇順になってるかのバリデーション。これSDK作る人がいれば注意。カタパルト側でここでチェックしてるからクライアント側でもその順番で送信しなければいけない。なぜ昇順でなければいけないのかまでは知りません。

これに関してのジャガーさんの回答
https://catnotes.xyz/symbol/q-a/toshi-transfer-transaction#mosaic-ordering

つまり、ハッシュを一致させるためにモザイクの順序にルールを定めている。これを定めなければ順序に関しても記憶する必要があり余計な領域を使ってしまう

以上!!!!!!!!

おわりに

TransferTransactionについてはだいぶ理解が深まりました。たぶん、他も同じように見ていけば基本的には理解できるはず。プラグイントランザクションを作成する場合は、まずは既存のオブザーバーやバリデーター、Notificationを使いこなしてトランザクションを作ればいいと思う。が、もし既存のものでは足りない場合は、がんばって作りましょう。ひたすら中を掘り探っていけばヒントはかなりありそうでした。

次は、他のトランザクション見ても面白いと思うし、何か作ってみても良いかな。

13
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5