stratisproject / StratisBitcoinFullNode

Bitcoin full node in C#
https://stratisplatform.com
MIT License
787 stars 314 forks source link

[Static flags in NStratis] Get rid of the static flags in NStratis #676

Closed dangershony closed 6 years ago

dangershony commented 6 years ago

The two static properties are used to control serialisation of trx and blocks, they are also used in various other places.

                Transaction.TimeStamp = false;
                Block.BlockSignature = false;

This task is about making those flags none static.

quantumagi commented 6 years ago

Currently when creating a new Block, BlockHeader or Transaction we are not passing any arguments to qualify the network-specific serialization that these objects and their contained objects may undergo.

For this reason we would ideally be passing options to the constructors of these objects or any object that act as a repository for these objects. The assumption is that these flags would be pretty much fixed on network level, so that all objects in a repository would share the same set of flags.

dangershony commented 6 years ago

Using Options will work for serialisation, we can even use Network instead and pass Network in to the IBitcoinSerializer for determining serialisation flags. How ever the biggest problem is hashing the block id, this will require passing Options in the constructor of Block which I am not the biggest fan of.

Another approach I had in mind, and I am not sure its the right one, is to use Network to get the block id so we will expose a method on network network.GetBlockId(BlockHeader blockHeader) that takes a block and use that every where, we could also use this approach for a trx, its very common practice that serialization is not done inside the component itself.

quantumagi commented 6 years ago

Regardless of the approach we won't easily escape passing an argument to the Block constructor. I have tried the approach of deriving the block options from the transactions it contains but it turned out to be a much less reliable approach and requiring more changes. We need the options to determine BlockSignature even if there are no transactions present (headers-only blocks).

Our biggest challenge is passing the TransactionOptions or Network to everywhere it is needed. We can either: 1) relay the TransactionOptions down the call hierarchy so that we can serialize objects and resolve block ids at the required points - which are often very deep in the call hierarchy with total loss of network context. 2) relay the Network object down the call hierarchy for the same purpose.

In both cases serialization will still happen as before - with both approaches providing the required information to replace the static flags.

The first approach appears to align better with the current coding paradigm which seems to include avoiding references from child objects to parent objects where possible. I can see how this would lead to better structured code.

In either case, TransactionOptions or Network, these values need to be reconstructed during deserialization. I suggest we do that from options recorded on the stream/repo rather than trying to come up with parameters that we can't always determine from the available context.

The primary objects that require these flags are: Network, Block/BlockHeader, Transaction, BitcoinStream, and the repositories, based on NoSqlRepository, used for testing. Each of these would have a "TransactionOptions" (or "Network") member. Due to the network/options being associated with the BitcoinStream or repositories they are available to the custom or generic deserialization methods of all the relevant objects. Methods such as GetAsync benefit from the options recorded on the repository instead of requiring flags to be passed that can often not be determined from the available context.

Many of the serialization methods are already taking a TransactionOptions (and ProtocolVersion) parameter. If we decide on the second approach then an additional Network parameter would have to be added to these methods for reasons similar to why we already have the pre-existing TransactionOptions (and ProtocolVersion) parameters.

To get an appreciation for the number of changes involved (and this is the only way) I have attempted both methods. The number of files that require changes are the same and more than expected. However most files only have one or two changes. The most changes occur in BitcoinSerializable and BitcoinStream. We need to change both for relay and deserialization purposes. A significant number of small changes are required to the POS related test cases to pass the block and/or transaction options. There is no way around that.

Some simplifications can be made:

TransactionOptions.POS = TransactionOptions.TimeStamp | TransactionOptions.BlockSignature
TransactionOptions.POSAll = TransactionOptions.POS | TransactionOptions.All

Please note that similar to the current static flags the TransactionOptions approach also makes the assumption that the flags are the same at all block heights.

dangershony commented 6 years ago

Right, Passing something in the constructor will indeed solve the problem (you also need to remember that the Protocol messages create transactions.

So a third approach where we use Network/Options to get the block/transaction hash (or also the serialisation of those types) is not a viable possibility?

If we pass the Network/TrasnactionOption in the BitcoinStream that solves the problem of serialisation (we will need though to pass Network in the constructor of BitcoinStream every where.

But we still don't solve the problem of the BlockHeader.GetHash()

quantumagi commented 6 years ago

The payload messages are serializing via BitcoinStream so any transactions or other objects being serialized would be getting their options from the stream.

We can pass Network to the BitcoinStream but we would be recording the Network.TransactionOptions.

In the "BlockSignature" case the BlockHeader.GetHash() uses ToBytes() which would look as follows:

public static byte[] ToBytes(this IBitcoinSerializable serializable, ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION,  TransactionOptions options = TransactionOptions.All)
{
        if (serializable is IHaveTransactionOptions)
            options |= (serializable as IHaveTransactionOptions).GetTransactionOptions();

        MemoryStream ms = new MemoryStream();
        var bms = new BitcoinStream(ms, true)
        {
            ProtocolVersion = version,
            TransactionOptions = options
        };
    serializable.ReadWrite(bms);
    return ToArrayEfficient(ms);
}

"GetTransactionOptions" would be returning either TransactionOptions.POSAll or TransactionOptions.All depending on the network.

If you think we are close enough I can create a PR and we can then update that as required. NBitcoin/Integration and Unit test cases are all passing.

dangershony commented 6 years ago

I would like to talk more about the approach. We have it seems several approachs

Determining serialisation (and block hashing)

@Aprogiena suggested we should think of the future requirements we will have and it may very well be that future networks will need to have different transaction and Block structures all together.

This brings inheritance to as a prime candidate.

Lets discuss inheritance for a bit.

How would we go about using Block, BlockHeader and Transaction in the node if we had to have, for now, two types of Blocks (and Transactions). A block has the additional BlockSignature property and a Transaction has the additional TimeStamp property.

If we had a to change our code to have a TransactionBase and BlockBase and the current Transaction and Block will inherit form that base classes (we can rename to PowBlock and PowTransaction but probably as a later task) then have a new PosBlock and PosTransaction that have those additional fields.

How would this effect the current code base? I presume the TxPayload will now have to look like this TxPayload<TransactionBase> and we would have to somehow register the correct TrasnactionType on the creation on the NetworkPeer itself

We might find we need to do things like

 var builder = new NetworkBuilder()
                .SetName("StratisTest")
                .SetConsensus(consensus)
                .SetMagic(magic)
                .SetBlockType<PosBlock>
                .SetTransactionType<PosTransactoin>
                .SetGenesis(genesis)
                .SetPort(26178)
                ...

The default will be the current Transaction I guess (not TransactionBase)

quantumagi commented 6 years ago

Yep we should consider using inheritance. It is important that we all feel comfortable with whatever solution we come up with. Although any approach would seem better than the current one I am not sure the number of changes would be less in the final count though. The only thing we are certain about right now are the number of changes in the TransactionOption approach. If however, we can implement inheritance in a more gradual manner then that would be a plus point. I expect we would still have similar issues though in getting the required information through the blind spots in the call hierarchy - and that's the main reason for the large number of changes required. We would be facing the same issues in a different guise - it will not magically disappear with a different approach. Adding a different class to avoid passing an extra parameter can easily be added to what we have now.

Another option:

var builder = new NetworkBuilder<Stratis>()
                .SetName("StratisTest")
                .SetConsensus(consensus)
                .SetMagic(magic)
                .SetGenesis(genesis)
                .SetPort(26178)
                ...

Implicit Network, Transaction and Block types would be Network<StratisOptions> , Transaction<StratisOptions> and Block<StratisOptions>. or perhaps simply Network<Stratis> , Transaction<Stratis> and Block<Stratis>:

    public abstract class NetworkOptions
    {
        public virtual int BlockHeight { set { } }
        public virtual bool IsProofOfStake { get; set; }

        public NetworkOptions() { this.BlockHeight = 0; }
    }

    public class Stratis : NetworkOptions
    {
        public override int BlockHeight
        {
            set
            {
                this.IsProofOfStake = true;
            }
        }
    }

    public class Bitcoin : NetworkOptions
    {
        public override int BlockHeight
        {
            set
            {
            }
        }
    }

    public class HasNetworkOptions<T> where T : NetworkOptions, new()
    {
        public virtual T Options { get; set; } = new T();
    }

    public class Network<T> : HasNetworkOptions<T> where T : NetworkOptions, new()
    {
    }

    public class Block<T> : HasNetworkOptions<T> where T : NetworkOptions, new()
    {
    }

    public class Transaction<T> : HasNetworkOptions<T> where T : NetworkOptions, new()
    {
    }

    public class Repo<T> : HasNetworkOptions<T> where T : NetworkOptions, new()
    {
        public Block<T> GetBlock(int blockHeight)
        {
            return new Block<T>() { Options = new T() { BlockHeight = blockHeight } };
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var tx = new Transaction<Stratis>();
            Debug.Assert(tx.Options.IsProofOfStake == true);
            var ty = new Transaction<Bitcoin>();
            Debug.Assert(ty.Options.IsProofOfStake == false);
            var tz = new Repo<Stratis>().GetBlock(0);
            Debug.Assert(tz.Options.IsProofOfStake == true);
        }
    }
quantumagi commented 6 years ago

An inheritance approach may look something like this. This is also showing a different approach to TransactionOptions which allows us to resolve the options (as a function of block height) at the point where we have the most information available:

    public enum NetworkOption
    {
        IsProofOfStake
    }

    public class NetworkOptions
    {
        Func<NetworkOption, int, object> callback;

        public NetworkOptions(Func<NetworkOption, int, object> callback)
        {
            this.callback = callback;
        }

        public bool GetOption(NetworkOption option, int blockHeight = 0)
        {
            return (bool)this.callback(option, blockHeight);
        }
    }

    public static class StratisNetwork
    {
        public const int LastPOWBlock = 20;
        public static NetworkOptions NetworkOptions = null;

        static StratisNetwork()
        {
            NetworkOptions = new NetworkOptions((option, height) => {
                return option == NetworkOption.IsProofOfStake && height > LastPOWBlock;
            });
        }
    }

    public static class BitcoinNetwork
    {
        public static NetworkOptions NetworkOptions = null;

        static BitcoinNetwork()
        {
            NetworkOptions = new NetworkOptions((option, height) => { return false; });
        }
    }

    public interface IHaveNetworkOptions
    {
        NetworkOptions NetworkOptions { get; set; }
    }

    public class Block:IHaveNetworkOptions
    {
        public int BlockHeight { get; set; }
        public NetworkOptions NetworkOptions { get; set; }

        public Block(NetworkOptions options)
        {
            this.NetworkOptions = new NetworkOptions((option, dummy) => options.GetOption(option, BlockHeight));
        } 

        public Transaction CreateTransaction()
        {
            return this.NetworkOptions.GetOption(NetworkOption.IsProofOfStake) ?
                new POSTransaction(this.NetworkOptions) as Transaction: 
                new POWTransaction(this.NetworkOptions) as Transaction;
        }
    }

    public abstract class Transaction:IHaveNetworkOptions
    {
        public NetworkOptions NetworkOptions { get; set; }
        public Transaction(NetworkOptions options)
        {
            this.NetworkOptions = options;
        }
        public bool IsPOS { get { return this.NetworkOptions.GetOption(NetworkOption.IsProofOfStake); } }
    }

    public class POSTransaction:Transaction        
    {
        public POSTransaction(NetworkOptions options) : base(options) { }
        // This parameterless approach only passes through the POS/POW information in the options.
        // While adequate for now it makes it difficult to see the benefits of inheritance vs the Block approach above.
        public POSTransaction() 
            : this(new NetworkOptions((option, dummy) => { return option == NetworkOption.IsProofOfStake; }))
        {
        }
    }

    public class POWTransaction : Transaction
    {
        public POWTransaction(NetworkOptions options) : base(options) { }
        // This parameterless approach only passes through the POS/POW information in the options.
        // While adequate for now it makes it difficult to see the benefits of inheritance vs the Block approach above.
        public POWTransaction() 
            : this(new NetworkOptions((option, dummy) => { return false; }))
        {
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var tx = new POSTransaction();
            Debug.Assert(tx.IsPOS == true);
            var ty = new POWTransaction();
            Debug.Assert(ty.IsPOS == false);
            var block = new Block(StratisNetwork.NetworkOptions) { BlockHeight = 100 };
            var tz = block.CreateTransaction();
            Debug.Assert(tz is POSTransaction);
            Debug.Assert(tz.IsPOS == true);
            var block2 = new Block(StratisNetwork.NetworkOptions) { BlockHeight = 10 };
            var tw = block2.CreateTransaction();
            Debug.Assert(tw is POWTransaction);
            Debug.Assert(tw.IsPOS == false);
        }
    }

The Transaction inheritance approach followed above does not show clear benefits over the Block approach we are using here. There are also no apparent benefits over the earlier template-based approach either.

Is there a reason that we can't do this? Since we are dealing with options that require resolving up to Network level it seems to make more sense to type the transaction after the network.

var tx = new Transaction<Stratis>();

Aprogiena commented 6 years ago

When mentioning inheritance, I was thinking slightly differently. You still have some kind of network options here, which I wanted to avoid completely.

Especially I wanted to avoid this:

        public Transaction CreateTransaction()
        {
            return this.NetworkOptions.GetOption(NetworkOption.IsProofOfStake) ?
                new POSTransaction(this.NetworkOptions) as Transaction: 
                new POWTransaction(this.NetworkOptions) as Transaction;
        }

The way I wanted to avoid it is to have a base class of a transaction (or block, but let's talk about tx only now) and a class based on that would be a stratis transaction (and another could be e.g. segwit transaction etc.) So the base class would serialize to a non-segwit bitcoin tx format. The stratis transaction would then inject its special fields into that serialization.

Would that work?

quantumagi commented 6 years ago

Having a Stratis transaction vs a Segwit transaction seems to be mixing apples and pears - but maybe you meant this in a network sense. I think we will run into combinatorial explosion class-wise if we attempt to encode options in class names and the number of options increases. My thinking is that we either have a Stratis network/block/transaction or a Bitcoin network/block/transaction and combine that with options. When we register objects fields for serialization we can provide (1) the field order, (2) masks over the options to determine if serialization occurs or not. What do you think?

Aprogiena commented 6 years ago

I don't think we will have explosion because no one said StratisTransaction has to be sealed. So SmartContractTransaction could just be derrived from StratisTransaction.

I'm not sure what you mean with mixing apples and pears, because we have in serialization code such an if (witSupported) and if we instead have this SegWitTransaction, this could be handled in its serialization and such an if would not be there in the base raw old BitcoinTransaction class

dangershony commented 6 years ago

We need to understand the different scenarios we may have.

Same network different types:
Witness for example is the ability to serialize a trx in two different ways on the same network (so you have a bitcoin transaction that can be serialized in two ways with-witness/without-witness).

Different networks:
The second scenario is different serialization networks, stratis/bitcoin/smartcontract-sidechian etc.. Stratis adds a timestamp filed to the serialized payload that will always be there, but stratis may still have different types of transactions segwit/no-segwit/some-additional-property

This two possibilities are handled differently, for example if a network has only one Transaction type its simple for the Node.cs to know what to deserialize to and internally the trx will know how to populate its fields, this also goes inline with the current protocol code where deserialization id determined during the deserialization processes.

If the same blockchain has 3 different types of trx how do we know what to deserialize a payload in to? Unless we temper with the protocol messages (which perhaps we should) currently the TransactionPayload is determined as so

    [Payload("tx")]
    public class TxPayload : BitcoinSerializablePayload<Transaction>
    {
        public TxPayload()
        {
        }
       ..
    }

To introduce inheritance we would need to allow more trx types such as

    [Payload("cstx")]
    public class TxPayload : BitcoinSerializablePayload<SmartContractTransaction>
    {
        public TxPayload()
        {
        }
       ..
    }

In case of POS the payload is the same so we would register a new payload to the Node.cs

    [Payload("tx")]
    public class PosTxPayload : BitcoinSerializablePayload<PosTransaction>
    {
        public PosTxPayload()
        {
        }
       ..
    }

Same goes for Block.

@quantumagi suggested a hybrid approach and I am supporting that.

We keep the current approach where a transaction can internally determine flow using some options (like how witness is done).

But also allow to add new TransactionTypes and use inheritance in the case of POS because the structure are very similar to POW And if we use a PosTransaction that inherits from PowTransaction we can still use the TransactionBuilder

I believe, this approach will give us the most freedom we need .

Aprogiena commented 6 years ago

I have no stronger opinion here, so I'm OK with whatever you agree on.

dangershony commented 6 years ago

If we are considering the Network<NetworkOptions> -> Network<Stratis> then @bokobza should be involved as he is looking in this area as well with making Settings and Network more generic.

quantumagi commented 6 years ago

Currently we have code such as:

var tx = new Transaction();

or

var block = new Block();

Unfortunately that gives the objects very little information and led to the introduction of the static flags - Transaction.TimeStamp and Block.BlockSignature. Ideally we should know, not only the transaction type, but also the network which is involved in order to be able to deal with network-specific serialization.

So instead of assuming that we can always derive the network type from the transaction type we could have:

var tx1 = new Transaction<Bitcoin>();
var tx2 = new POSTransaction<Stratis>();
var block = new Block<Bitcoin>();

// or even,

var tx3 = new Transaction<StratisRegTest>(); // if required

where Bitcoin, Stratis and StratisRegTest are classes derived from the class NetworkOptions.

The above will allow us to do - e.g.:

var options = tx3.GetNetworkOptions(SerializationType.Hash, ProtocolVersion.PROTOCOL_VERSION);

which would not be possible in the top-most example.

We could also do:

    [Payload("cstx")]
    public class TxPayload : BitcoinSerializablePayload<SmartContractTransaction<Stratis>>
    {
        public TxPayload()
        {
        }
       ..
    }

and have the network context, and serialization options. implied by the payload.

This is a rough "idea" of how to achieve some of the above:

    public class Stratis:NetworkOptions
    {
    }

    public class Bitcoin:NetworkOptions
    {
    }

    public interface IHaveNetworkOptions
    {
        NetworkOptions GetNetworkOptions(SerializationType type = SerializationType.Disk,
            ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION);
    }

    public class Network<T> : IHaveNetworkOptions where T : NetworkOptions, new()
    {
        private T NetworkOptions { get; set; } = new T();

        public T GetNetworkOptions(SerializationType type = SerializationType.Disk,
            ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION) 
        { 
             return this.NetworkOptions; 
        }

        public Block<T> CreateBlock()
        {
            return new Block<T>(this);
        }
    }

    public class Block<T> : IBitcoinSerializable, IHaveNetworkOptions where T : NetworkOptions, new()
    {
        private IHaveNetworkOptions parent = null;

        public T GetNetworkOptions(SerializationType type = SerializationType.Disk,
            ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION) 
        { 
             return this.parentOptions?.GetNetworkOptions(type,version) ?? new T(); 
        }

        public Block() { }
        public Block(IHaveNetworkOptions parent)
        {
            this.parent = parent;
        }

        public Transaction<T> CreateTransaction()
        {
            if (this.parentOptions is Stratis)
                return new POSTransaction<T>(this);
            else
                return new Transaction<T>(this);
        }
    }

    public class Transaction<T> : IBitcoinSerializable, IHaveNetworkOptions where T : NetworkOptions, new()
    {
        private IHaveNetworkOptions parent = null;

        public T GetNetworkOptions(SerializationType type = SerializationType.Disk,
            ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION) 
        { 
             return this.parent?.GetNetworkOptions(type, version) ?? new T(); 
        }

        public Transaction() { }
        public Transaction(IHaveNetworkOptions parent)
        {
            this.parent = parent;
        }
    }

    public class POSTransaction<T> : Transaction<T> where T : NetworkOptions, new()
    {
        public T GetNetworkOptions(SerializationType type = SerializationType.Disk,
            ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION) 
        { 
             return this.parentOptions.GetNetworkOptions(type,version) | NetworkOptions.POS; 
        }
    }
dangershony commented 6 years ago

This is probably not so correct var tx1 = new Transaction<Bitcoin>(); How do we create a new trx in code (or from serializer) if the code is locked to a network?

If we do indeed go down a generic block and transaction then we could be much more creative

Then why not specify the header as a generic and the trx new Block<THeader, TTransaction>()

I also dont fully understand IHaveNetworkOptions its just used to wrap serializers right?

quantumagi commented 6 years ago

The code is locked to "class Bitcoin:NetworkOptions" - not to a network.

The line of code you are quoting will work as is:

By using Transaction<Bitcoin> we are creating a Transaction object that carries a NetworkOptions object of type Bitcoin (derived from NetworkOptions) internally.

This means the Transaction object can instantiate an object of type "Bitcoin" (NetworkOptions) and then call GetNetworkOptions on that options object.

Although the above will work, and will probably be used a lot in tests, it is probably better to have methods on the parent objects for creating the child objects. That way the parent objects are in a better position to influence the options of the child objects as we are allowing here:

        public T GetNetworkOptions(SerializationType type = SerializationType.Disk,
            ProtocolVersion version = ProtocolVersion.PROTOCOL_VERSION) 
        { 
             return this.parent?.GetNetworkOptions(type, version) ?? new T(); 
        }

IHaveNetworkOptions is an interface containing the above method.

MithrilMan commented 6 years ago

Hey! Just a question, why not using a TransactionFactory? Having a TransactionFactory that's responsible to create a Transaction instance, and having that TransactionFactory specified during Node configuration, would allow you to use DI and you can then resolve the dependency to obtain the right transaction

you could have a ITransactionFactory<Network> interface and register its implementation during the network configuration

this way you can bury the transaction serialization/deserialization logic in that factory class, that can access to the Network instance or any other service it needs

You could even reference that transaction factory in the network instance (using DI or explicit parameter, but of course DI is more flexible, like you did in the Features constructors) and then you can have a method in the Network class that create the instance for you, without the need to reference the ITransactionService

Sorry if i misunderstood the problem, it's late and I've been a lot out of stratis project :)

quantumagi commented 6 years ago

@MithrilMan, I had already added a CreateTransaction method to the Network class for my next PR so I can definitely agree with you on that last point.

There is a lot of focus on the serialization/deserialization, as it is the ultimate goal, however the main problem is not that. The main problem is to have the network context available everywhere it would be needed for serialization/deserialization to happen correctly. That in itself presents the main challenge: how to pass that information along without adding additional, possibly superfluous, arguments to a significant number of existing methods.

The network context is often lost as methods call deeper methods and unfortunately we often need the network context deep down in the call hierarchy to perform serialization appropriately. There is no doubt that we need access to the Network object before we can even start thinking about serialization. This is the reason that NetworkOptions was introduced - it would provide better accessibility control than directly exposing the entire network object to all call depths.

The idea I had was to, as part of the solution, hand over the NetworkOptions from Network -> Block -> Transaction. To complete the loop we also have to hand over the NetworkOptions between the serialized objects and the BitcoinStream object so that e.g. the more granular serialized objects can also access NetworkOptions. Sometimes a BitcoinStream object gets instantiated while streaming a normal Stream object. That makes things even more tricky.

All of this does mean that Transaction would require an additional argument/parameter for its construction (Network/NetworkOptions) - implicitly or explicitly - even if this fact is hidden inside of CreateTransaction. I.e.:

Inheritance can be used to make the creation of different transaction types more elegant but we will still need the network options for task-specific serialization - e.g. for purposes of hashing etc. This is not even a serialization-only concern as actual specialized transaction object derivatives are being created for these purposes - in particular the use of serialization to clone into existence different "flavours" of the same transaction type. In that sense the NetworkOptions uniquely helps us to identify the variant of transaction we are dealing with for the purposes of serialization. This means that we should probably not have the mindset that inheritance is somehow a replacement for network options. NetworkOptions should be seen as something that helps qualify the flavour of object we are dealing with.

MithrilMan commented 6 years ago

the concept of Service and ServiceLocator is what saves you in those scenario. If you register the NetworkOption (and probably you already did, i have to check) allow you to be able to get it back when needed.

One way is using Dependency Injection as parameters in the object that need to use that, but in this scenario you should change every constructor of those object so this is something you actually maybe want to avoid, the other way is to expose a method, or a straight property like YourObject.NetworkOptions and populate it using the DI container, because DI containers can do more that just bind an interface to an instance of an object, they can instantiate the object you need using a factory class/method (i need to check what our current DI container allow)

This way, the factory method/class can populate the property of the object. Easier is to register as a service within the container, a BlockFactory, TransactionFactory, etc... using default DI on constructor and delegating to them the burden of creating and instantiating a transaction and handling the serialization/deserialization (this mean at the end that Transaction become a mere DTO (data transfer object) without any logic

Depending on the choice it would require more or less refactory, but i think the main point should be to have a good codebase, foundation should be the main aspect to cover first, otherwise the project will be in trouble later

Anyway, and i say this as last because i don't like global/magic objects, another fast approach would be to have a public static singleton object where you can register all needed sharable resources, divided by an identification that can bind to any network (so you can have separate storage for separate network of the same type, if in future you want to run on a node 2 different stratislike nodes on two separate network)

"This Message Will Self Destruct In Five Seconds"