diff --git a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json index 6c6e14d3ed8..b8b517c9e4c 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json @@ -11,10 +11,12 @@ "Protocols": "Http2" } }, - "Swarm": { - "IsEnabled": true + "Solo": { + "IsEnabled": false, + "Proposer": true }, - "Validator": { - "IsEnabled": true + "PubsubSwarm": { + "IsEnabled": true, + "Proposer": false } } diff --git a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs index e9634e78421..32a4b3edc9b 100644 --- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs +++ b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs @@ -40,9 +40,14 @@ public static ILibplanetNodeBuilder AddLibplanetNode( services.AddSingleton, ValidatorOptionsValidator>(); services.AddOptions() - .Bind(configuration.GetSection(SoloOptions.Position)); + .Bind(configuration.GetSection(SoloOptions.Position)); services.AddSingleton, SoloOptionsConfigurator>(); + services.AddOptions() + .Bind(configuration.GetSection(PubsubSwarmOptions.Position)); + services + .AddSingleton, PubsubSwarmOptionsConfigurator>(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(s => (IStoreService)s.GetRequiredService()); @@ -61,6 +66,12 @@ public static ILibplanetNodeBuilder AddLibplanetNode( nodeBuilder.WithSolo(); } + if (configuration.IsOptionsEnabled(PubsubSwarmOptions.Position)) + { + Console.WriteLine("Start PubsubSwarm"); + nodeBuilder.WithPubsubSwarm(); + } + if (configuration.IsOptionsEnabled(SwarmOptions.Position)) { nodeBuilder.WithSwarm(); diff --git a/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs b/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs index 51b3a7687fb..09b96be60a3 100644 --- a/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs +++ b/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs @@ -1,6 +1,8 @@ +using Libplanet.Node.Protocols; using Libplanet.Node.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Nethermind.Libp2p.Stack; namespace Libplanet.Node.Extensions.NodeBuilder; @@ -39,4 +41,14 @@ public ILibplanetNodeBuilder WithValidator() _scopeList.Add("Validator"); return this; } + + public ILibplanetNodeBuilder WithPubsubSwarm() + { + Services.AddSingleton(); + Services.AddLibp2p(builder => builder.AddAppLayerProtocol()); + Services.AddScoped(); + Services.AddHostedService(); + _scopeList.Add("PubsubSwarm"); + return this; + } } diff --git a/sdk/node/Libplanet.Node/Libplanet.Node.csproj b/sdk/node/Libplanet.Node/Libplanet.Node.csproj index c4f5e4198eb..473152aa33c 100644 --- a/sdk/node/Libplanet.Node/Libplanet.Node.csproj +++ b/sdk/node/Libplanet.Node/Libplanet.Node.csproj @@ -17,7 +17,8 @@ - + + diff --git a/sdk/node/Libplanet.Node/Options/PubsubSwarmOptions.cs b/sdk/node/Libplanet.Node/Options/PubsubSwarmOptions.cs new file mode 100644 index 00000000000..592b13b05b0 --- /dev/null +++ b/sdk/node/Libplanet.Node/Options/PubsubSwarmOptions.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using Libplanet.Node.DataAnnotations; + +namespace Libplanet.Node.Options; + +[Options(Position)] +public class PubsubSwarmOptions : OptionsBase +{ + public const string Position = "PubsubSwarm"; + + public bool Proposer { get; set; } + + public long BlockInterval { get; set; } = 4000; + + [PrivateKey] + [Description("The private key of the node.")] + public string PrivateKey { get; set; } = string.Empty; +} diff --git a/sdk/node/Libplanet.Node/Options/PubsubSwarmOptionsConfigurator.cs b/sdk/node/Libplanet.Node/Options/PubsubSwarmOptionsConfigurator.cs new file mode 100644 index 00000000000..ae545d55e6a --- /dev/null +++ b/sdk/node/Libplanet.Node/Options/PubsubSwarmOptionsConfigurator.cs @@ -0,0 +1,21 @@ +using Libplanet.Common; +using Libplanet.Crypto; +using Microsoft.Extensions.Logging; + +namespace Libplanet.Node.Options; + +internal sealed class PubsubSwarmOptionsConfigurator( + ILogger logger) + : OptionsConfiguratorBase +{ + protected override void OnConfigure(PubsubSwarmOptions options) + { + if (options.PrivateKey == string.Empty) + { + options.PrivateKey = ByteUtil.Hex(new PrivateKey().ByteArray); + logger.LogWarning( + "Node's private key is not set. A new private key is generated: {PrivateKey}", + options.PrivateKey); + } + } +} diff --git a/sdk/node/Libplanet.Node/Protocols/MessageContentCodec.cs b/sdk/node/Libplanet.Node/Protocols/MessageContentCodec.cs new file mode 100644 index 00000000000..9e8ed426050 --- /dev/null +++ b/sdk/node/Libplanet.Node/Protocols/MessageContentCodec.cs @@ -0,0 +1,39 @@ +using Libplanet.Net.Messages; + +namespace Libplanet.Node.Protocols; + +public static class MessageContentCodec +{ + public static MessageContent Deserialize(byte[] bytes) + { + var rawFrames = bytes; + var type = rawFrames[0]; + rawFrames = rawFrames.Skip(1).ToArray(); + var dataFrames = new List(); + var frameCount = BitConverter.ToInt32(rawFrames[..4]); + rawFrames = rawFrames.Skip(4).ToArray(); + for (int i = 0; i < frameCount; i++) + { + var frameSize = BitConverter.ToInt32(rawFrames[..4]); + dataFrames.Add(rawFrames.Skip(4).Take(frameSize).ToArray()); + rawFrames = rawFrames.Skip(4 + frameSize).ToArray(); + } + + return NetMQMessageCodec.CreateMessage( + (MessageContent.MessageType)type, + dataFrames.ToArray()); + } + + public static byte[] Serialize(MessageContent messageContent) + { + IEnumerable ba = [(byte)messageContent.Type]; + ba = ba.Concat(BitConverter.GetBytes(messageContent.DataFrames.Count())); + foreach (var bytes in messageContent.DataFrames) + { + ba = ba.Concat(BitConverter.GetBytes(bytes.Length)); + ba = ba.Concat(bytes); + } + + return ba.ToArray(); + } +} diff --git a/sdk/node/Libplanet.Node/Protocols/PingPongProtocol.cs b/sdk/node/Libplanet.Node/Protocols/PingPongProtocol.cs new file mode 100644 index 00000000000..64eeb03b951 --- /dev/null +++ b/sdk/node/Libplanet.Node/Protocols/PingPongProtocol.cs @@ -0,0 +1,67 @@ +using System.Buffers; +using Libplanet.Node.Services; +using Nethermind.Libp2p.Core; + +namespace Libplanet.Node.Protocols +{ + internal class PingPongProtocol(IMessageChannel messageChannel) : IProtocol + { + public string Id => "/blockchain/ping-pong/1.0.0"; + + public async Task DialAsync( + IChannel downChannel, + IChannelFactory? upChannelFactory, + IPeerContext context) + { + TimeSpan elapsed = TimeSpan.Zero; + + // Keep connection for 5 seconds + while (elapsed < TimeSpan.FromSeconds(5)) + { + if (messageChannel.TryGetMessageToSend( + context.RemotePeer.Address, + out byte[]? msg)) + { + if (msg is not null) + { + await downChannel.WriteAsync( + new ReadOnlySequence(msg)); + ReadOnlySequence read = + await downChannel.ReadAsync(0, ReadBlockingMode.WaitAny).OrThrow(); + messageChannel.ReceiveMessage( + context.RemotePeer.Address, + read.ToArray(), + reply => _ = + downChannel.WriteAsync( + new ReadOnlySequence(reply))); + } + + elapsed = TimeSpan.Zero; + } + else + { + await Task.Delay(50); + elapsed += TimeSpan.FromMilliseconds(50); + } + } + } + + public async Task ListenAsync( + IChannel downChannel, + IChannelFactory? upChannelFactory, + IPeerContext context) + { + while (true) + { + ReadOnlySequence read = + await downChannel.ReadAsync(0, ReadBlockingMode.WaitAny).OrThrow(); + messageChannel.ReceiveMessage( + context.RemotePeer.Address, + read.ToArray(), + msg => _ = + downChannel.WriteAsync( + new ReadOnlySequence(msg))); + } + } + } +} diff --git a/sdk/node/Libplanet.Node/Services/IMessageChannel.cs b/sdk/node/Libplanet.Node/Services/IMessageChannel.cs new file mode 100644 index 00000000000..e3979a57f05 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/IMessageChannel.cs @@ -0,0 +1,23 @@ +using Libplanet.Net.Messages; +using Libplanet.Node.Protocols; +using Multiformats.Address; + +namespace Libplanet.Node.Services; + +public interface IMessageChannel +{ + EventHandler<(Multiaddress, byte[], Action)>? OnMessageReceived + { + get; + set; + } + + void SendMessage(Multiaddress address, byte[] message); + + void ReceiveMessage( + Multiaddress sender, + byte[] message, + Action callback); + + bool TryGetMessageToSend(Multiaddress address, out byte[]? message); +} diff --git a/sdk/node/Libplanet.Node/Services/MessageChannel.cs b/sdk/node/Libplanet.Node/Services/MessageChannel.cs new file mode 100644 index 00000000000..0cdc0f8a924 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/MessageChannel.cs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using System.Collections.Concurrent; +using Multiformats.Address; + +namespace Libplanet.Node.Services; + +public class MessageChannel : IMessageChannel +{ + private readonly ConcurrentDictionary> + _messagesToSend = new(); + + public EventHandler<(Multiaddress, byte[], Action)>? OnMessageReceived + { + get; + set; + } + + public void SendMessage(Multiaddress address, byte[] message) + { + if (!_messagesToSend.TryGetValue(address, out ConcurrentBag? bag)) + { + bag = []; + _messagesToSend.TryAdd(address, bag); + } + + bag.Add(message); + } + + public void ReceiveMessage( + Multiaddress sender, + byte[] message, + Action callback) + { + OnMessageReceived?.Invoke(this, (sender, message, callback)); + } + + public bool TryGetMessageToSend(Multiaddress address, out byte[]? message) + { + if (_messagesToSend.TryGetValue(address, out ConcurrentBag? bag)) + { + if (bag.IsEmpty) + { + message = null; + return false; + } + + return bag.TryTake(out message); + } + + message = null; + return false; + } +} diff --git a/sdk/node/Libplanet.Node/Services/PubsubSwarm.cs b/sdk/node/Libplanet.Node/Services/PubsubSwarm.cs new file mode 100644 index 00000000000..233252fb335 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/PubsubSwarm.cs @@ -0,0 +1,310 @@ +using System.Text; +using Bencodex; +using Libplanet.Blockchain; +using Libplanet.Crypto; +using Libplanet.Net; +using Libplanet.Net.Messages; +using Libplanet.Node.Options; +using Libplanet.Node.Protocols; +using Libplanet.Types.Blocks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Multiformats.Address; +using Nethermind.Libp2p.Core; +using Nethermind.Libp2p.Protocols; +using Nethermind.Libp2p.Protocols.Pubsub; + +namespace Libplanet.Node.Services; + +public class PubsubSwarm +{ + private readonly BlockChain _blockChain; + private readonly PrivateKey _privateKey; + private readonly bool _proposer; + private readonly IMessageChannel _channel; + private readonly IPeerFactory _peerFactory; + private readonly PubsubRouter _router; + private readonly MDnsDiscoveryProtocol _discoveryProtocol; + private readonly TimeSpan _blockInterval; + private readonly ILogger _logger; + + private Multiaddress? _listenerAddress; + private ITopic? _topic; + + public PubsubSwarm( + IServiceProvider serviceProvider, + IBlockChainService blockChainService, + ILogger logger, + IMessageChannel messageChannel, + IOptions pubsubSwarmOptions) + { + _blockChain = blockChainService.BlockChain; + var options = pubsubSwarmOptions.Value; + _privateKey = PrivateKey.FromString(options.PrivateKey); + _proposer = options.Proposer; + _channel = messageChannel; + + _peerFactory = serviceProvider.GetService()!; + _router = serviceProvider.GetService()!; + _discoveryProtocol = serviceProvider.GetService()!; + + _blockInterval = TimeSpan.FromMilliseconds(options.BlockInterval); + _logger = logger; + _logger.LogInformation( + "PubsubSwarmService initialized. Interval: {BlockInterval}ms", + _blockInterval); + } + + public async Task StartAsync(CancellationToken stoppingToken) + { + _channel.OnMessageReceived += OnMessageReceived; + _blockChain.TipChanged += OnTipChanged; + var tasks = new List + { + TransportTask(stoppingToken), + }; + + if (_proposer) + { + tasks.Add(BlockChainTask(stoppingToken)); + } + + await Task.WhenAny(tasks); + } + + private async Task BlockChainTask(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + _logger.LogInformation("Proposing a new block..."); + var tip = _blockChain.Tip; + var block = _blockChain.ProposeBlock( + _privateKey, + _blockChain.GetBlockCommit(tip.Hash)); + _blockChain.Append( + block, + _blockChain.GetBlockCommit(tip.Hash), + validate: false); + + await Task.Delay(_blockInterval, cancellationToken); + } + } + + private async Task TransportTask(CancellationToken cancellationToken) + { + Identity localPeerIdentity = new(); + string addr = $"/ip4/0.0.0.0/tcp/0/p2p/{localPeerIdentity.PeerId}"; + ILocalPeer peer = _peerFactory.Create(localPeerIdentity, Multiaddress.Decode(addr)); + + _topic = _router.GetTopic("blockchain:broadcast"); + _topic.OnMessage += bytes => + { + int addressLength = BitConverter.ToInt32(bytes[..4]); + Multiaddress sender = Multiaddress.Decode( + Encoding.UTF8.GetString(bytes[4..(4 + addressLength)])); + byte[] msg = bytes[(4 + addressLength)..]; + _logger.LogTrace( + "Received message {Message}", + msg.Aggregate(string.Empty, (s, b) => s + b)); + HandleMessage(sender, msg, _ => { }); + }; + + IListener listener = await peer.ListenAsync(addr, cancellationToken); + _listenerAddress = listener.Address; + + _ = _discoveryProtocol.DiscoverAsync(peer.Address, token: cancellationToken); + _ = _router.RunAsync(peer, token: cancellationToken); + + _logger.LogInformation("Listener started at {Address}", _listenerAddress); + + listener.OnConnection += remotePeer => + { + _logger.LogInformation("A peer connected {Remote}", remotePeer.Address); + return Task.CompletedTask; + }; + cancellationToken.Register((_, _) => { listener.DisconnectAsync(); }, this); + + await listener; + } + + private void OnTipChanged(object? e, (Block OldTip, Block NewTip) args) + { + BroadcastMessage( + MessageContentCodec.Serialize( + new BlockHeaderMsg(_blockChain.Genesis.Hash, args.NewTip.Header))); + } + + private void BroadcastMessage(byte[] msg) + { + if (_listenerAddress is not { } listenerAddress) + { + _logger.LogError("Cannot broadcast message before initialize"); + return; + } + + _logger.LogTrace( + "Publish Message: {Message}", + msg.Aggregate(string.Empty, (s, b) => s + b)); + + // NOTE: Use ToString instead of ToBytes because it has bug + byte[] listenerAddressBytes = Encoding.UTF8.GetBytes(listenerAddress.ToString()); + _topic?.Publish(BitConverter.GetBytes(listenerAddressBytes.Length) + .Concat(listenerAddressBytes) + .Concat(msg).ToArray()); + } + + private void OnMessageReceived( + object? e, + (Multiaddress Sender, byte[] Message, Action Callback) arg) + { + HandleMessage(arg.Sender, arg.Message, arg.Callback); + } + + private void SendMessage(Multiaddress target, byte[] msg) + { + _channel.SendMessage(target, msg); + } + + private void HandleMessage(Multiaddress sender, byte[] message, Action callback) + { + // Sender may be inaccurate. It works correctly when receiving a broadcasted message + var messageContent = MessageContentCodec.Deserialize(message); + var codec = new Codec(); + switch (messageContent) + { + case GetBlockHashesMsg getBlockHashesMsg: + _logger.LogInformation("Received GetBlockHashesMsg {@Message}", getBlockHashesMsg); + _blockChain.FindNextHashes( + getBlockHashesMsg.Locator, + getBlockHashesMsg.Stop + ).Deconstruct( + out long? offset, + out IReadOnlyList hashes + ); + callback(MessageContentCodec.Serialize(new BlockHashesMsg(offset, hashes))); + break; + + case BlockHashesMsg blockHashesMsg: + _logger.LogInformation("Received BlockHashesMsg {@Message}", blockHashesMsg); + callback(MessageContentCodec.Serialize(new GetBlocksMsg(blockHashesMsg.Hashes))); + break; + + case GetBlocksMsg getBlocksMsg: + _logger.LogInformation("Received GetBlocksMsg {@Message}", getBlocksMsg); + var payloads = new List(); + foreach (BlockHash hash in getBlocksMsg.BlockHashes) + { + if (_blockChain[hash] is { } block) + { + byte[] blockPayload = codec.Encode(block.MarshalBlock()); + payloads.Add(blockPayload); + byte[] commitPayload = _blockChain.GetBlockCommit(block.Hash) is { } commit + ? codec.Encode(commit.Bencoded) + : []; + payloads.Add(commitPayload); + } + + if (payloads.Count / 2 == getBlocksMsg.ChunkSize) + { + var response = new BlocksMsg(payloads); + callback(MessageContentCodec.Serialize(response)); + payloads.Clear(); + } + } + + break; + + case BlocksMsg blocksMsg: + _logger.LogInformation("Received BlocksMsg {@Message}", blocksMsg); + for (int i = 0; i < blocksMsg.Payloads.Count; i += 2) + { + byte[] blockPayload = blocksMsg.Payloads[i]; + byte[] commitPayload = blocksMsg.Payloads[i + 1]; + Block block = BlockMarshaler.UnmarshalBlock( + (Bencodex.Types.Dictionary)codec.Decode(blockPayload)); + BlockCommit? commit = commitPayload.Length == 0 + ? null + : new BlockCommit(codec.Decode(commitPayload)); + + _logger.LogInformation("Appending block {Block}", block); + _blockChain.Append(block, commit); + } + + break; + + case BlockHeaderMsg blockHeaderMsg: + _logger.LogInformation("Received BlockHeaderMsg {@Message}", blockHeaderMsg); + ProcessBlockHeader(sender, blockHeaderMsg); + + break; + + default: + _logger.LogError("Received unexpected message {@Message}", message); + break; + } + } + + private void ProcessBlockHeader(Multiaddress remote, BlockHeaderMsg message) + { + if (!message.GenesisHash.Equals(_blockChain.Genesis.Hash)) + { + _logger.LogError( + "{MessageType} message was sent from a peer {Peer} with " + + "a different genesis block {Hash}", + remote, + nameof(BlockHeaderMsg), + message.GenesisHash + ); + return; + } + + BlockHeader header; + try + { + header = message.GetHeader(); + } + catch (InvalidBlockException ibe) + { + _logger.LogError( + ibe, + "Received header #{BlockIndex} {BlockHash} is invalid", + message.HeaderHash, + message.HeaderIndex + ); + return; + } + + _logger.LogInformation( + "Received {MessageName} #{ReceivedIndex} {ReceivedHash} from peer {Peer}", + nameof(BlockHeader), + header.Index, + header.Hash, + remote); + + if (message.HeaderIndex == _blockChain.Tip.Index + 1) + { + SendMessage( + remote, + MessageContentCodec.Serialize( + new GetBlocksMsg([message.HeaderHash], 1))); + } + else if (message.HeaderIndex > _blockChain.Tip.Index) + { + SendMessage( + remote, + MessageContentCodec.Serialize( + new GetBlockHashesMsg(_blockChain.GetBlockLocator(), header.Hash))); + } + else + { + _logger.LogTrace( + "Discarding received header #{ReceivedIndex} {ReceivedHash} " + + "as it is not needed for the current chain with tip #{TipIndex} {TipHash}", + header.Index, + header.Hash, + _blockChain.Tip.Index, + _blockChain.Tip.Hash); + } + } +} diff --git a/sdk/node/Libplanet.Node/Services/PubsubSwarmService.cs b/sdk/node/Libplanet.Node/Services/PubsubSwarmService.cs new file mode 100644 index 00000000000..f01b42febdc --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/PubsubSwarmService.cs @@ -0,0 +1,29 @@ +using System.Text; +using Bencodex; +using Libplanet.Blockchain; +using Libplanet.Crypto; +using Libplanet.Net.Messages; +using Libplanet.Node.Options; +using Libplanet.Node.Protocols; +using Libplanet.Types.Blocks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Multiformats.Address; +using Nethermind.Libp2p.Core; +using Nethermind.Libp2p.Protocols; +using Nethermind.Libp2p.Protocols.Pubsub; + +namespace Libplanet.Node.Services; + +public class PubsubSwarmService(IServiceProvider serviceProvider) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await using var serviceScope = serviceProvider.CreateAsyncScope(); + var swarm = serviceScope.ServiceProvider.GetRequiredService(); + + await swarm.StartAsync(stoppingToken); + } +} diff --git a/sdk/node/Libplanet.Node/Services/SoloProposeService.cs b/sdk/node/Libplanet.Node/Services/SoloProposeService.cs index 4898a83665d..e2eaaf7cbda 100644 --- a/sdk/node/Libplanet.Node/Services/SoloProposeService.cs +++ b/sdk/node/Libplanet.Node/Services/SoloProposeService.cs @@ -28,7 +28,7 @@ public SoloProposeService( _logger = logger; _logger.LogInformation( "SoloProposeService initialized. Interval: {BlockInterval}ms", - _blockInterval); + _blockInterval.Microseconds); } protected async override Task ExecuteAsync(CancellationToken stoppingToken) @@ -48,15 +48,15 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken) } } - private Task ProposeBlockAsync(CancellationToken cancellationToken) + private async Task ProposeBlockAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { ProposeBlock(); - Task.Delay(_blockInterval, cancellationToken); + await Task.Delay(_blockInterval, cancellationToken); } - return Task.CompletedTask; + await Task.CompletedTask; } private void ProposeBlock() diff --git a/src/Libplanet.Net/Messages/BlockHashesMsg.cs b/src/Libplanet.Net/Messages/BlockHashesMsg.cs index d01a992f154..2646d3c82d7 100644 --- a/src/Libplanet.Net/Messages/BlockHashesMsg.cs +++ b/src/Libplanet.Net/Messages/BlockHashesMsg.cs @@ -6,7 +6,7 @@ namespace Libplanet.Net.Messages { - internal class BlockHashesMsg : MessageContent + public class BlockHashesMsg : MessageContent { public BlockHashesMsg(long? startIndex, IEnumerable hashes) { diff --git a/src/Libplanet.Net/Messages/BlockHeaderMsg.cs b/src/Libplanet.Net/Messages/BlockHeaderMsg.cs index 61c1c0e0b02..dacdfc4fb8c 100644 --- a/src/Libplanet.Net/Messages/BlockHeaderMsg.cs +++ b/src/Libplanet.Net/Messages/BlockHeaderMsg.cs @@ -4,7 +4,7 @@ namespace Libplanet.Net.Messages { - internal class BlockHeaderMsg : MessageContent + public class BlockHeaderMsg : MessageContent { private static readonly Codec Codec = new Codec(); diff --git a/src/Libplanet.Net/Messages/BlocksMsg.cs b/src/Libplanet.Net/Messages/BlocksMsg.cs index babfd36c4e5..51325c5a114 100644 --- a/src/Libplanet.Net/Messages/BlocksMsg.cs +++ b/src/Libplanet.Net/Messages/BlocksMsg.cs @@ -5,7 +5,7 @@ namespace Libplanet.Net.Messages { - internal class BlocksMsg : MessageContent + public class BlocksMsg : MessageContent { /// /// Creates an instance of with given . diff --git a/src/Libplanet.Net/Messages/GetBlockHashesMsg.cs b/src/Libplanet.Net/Messages/GetBlockHashesMsg.cs index 311dd0e66f1..79656f1f8a3 100644 --- a/src/Libplanet.Net/Messages/GetBlockHashesMsg.cs +++ b/src/Libplanet.Net/Messages/GetBlockHashesMsg.cs @@ -6,7 +6,7 @@ namespace Libplanet.Net.Messages { - internal class GetBlockHashesMsg : MessageContent + public class GetBlockHashesMsg : MessageContent { public GetBlockHashesMsg(BlockLocator locator, BlockHash? stop) { diff --git a/src/Libplanet.Net/Messages/GetBlocksMsg.cs b/src/Libplanet.Net/Messages/GetBlocksMsg.cs index 8e345dbfb43..6a785afaa9a 100644 --- a/src/Libplanet.Net/Messages/GetBlocksMsg.cs +++ b/src/Libplanet.Net/Messages/GetBlocksMsg.cs @@ -5,7 +5,7 @@ namespace Libplanet.Net.Messages { - internal class GetBlocksMsg : MessageContent + public class GetBlocksMsg : MessageContent { public GetBlocksMsg(IEnumerable hashes, int chunkSize = 100) { diff --git a/src/Libplanet.Net/Messages/NetMQMessageCodec.cs b/src/Libplanet.Net/Messages/NetMQMessageCodec.cs index 5fa79fc817f..06d7417db54 100644 --- a/src/Libplanet.Net/Messages/NetMQMessageCodec.cs +++ b/src/Libplanet.Net/Messages/NetMQMessageCodec.cs @@ -171,7 +171,8 @@ public Message Decode(NetMQMessage encoded, bool reply) return new Message(content, version, remote, timestamp, identity); } - internal static MessageContent CreateMessage( + #pragma warning disable + public static MessageContent CreateMessage( MessageContent.MessageType type, byte[][] dataframes) { diff --git a/src/Libplanet/Blockchain/BlockChain.cs b/src/Libplanet/Blockchain/BlockChain.cs index 6b9fca6cb26..77e218f3607 100644 --- a/src/Libplanet/Blockchain/BlockChain.cs +++ b/src/Libplanet/Blockchain/BlockChain.cs @@ -215,7 +215,7 @@ private BlockChain( /// // FIXME: This should be completely replaced by IRenderer.RenderBlock() or any other // alternatives. - internal event EventHandler<(Block OldTip, Block NewTip)> TipChanged; + public event EventHandler<(Block OldTip, Block NewTip)> TipChanged; #pragma warning disable MEN002 ///