From a4892db1ef8f9e9306963ca1ee369f218bb818ca Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Tue, 26 Jan 2021 14:26:21 +0900 Subject: [PATCH 1/9] Add timestamp field to message --- .../Net/Messages/BlockHashesTest.cs | 5 ++-- Libplanet.Tests/Net/Messages/MessageTest.cs | 7 ++++- Libplanet.Tests/Net/SwarmTest.cs | 30 +++++++++++++++---- Libplanet/Net/Messages/Message.cs | 30 +++++++++++++++---- Libplanet/Net/NetMQTransport.cs | 18 +++++++++-- 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/Libplanet.Tests/Net/Messages/BlockHashesTest.cs b/Libplanet.Tests/Net/Messages/BlockHashesTest.cs index 1921d26050..c3f92ed5d5 100644 --- a/Libplanet.Tests/Net/Messages/BlockHashesTest.cs +++ b/Libplanet.Tests/Net/Messages/BlockHashesTest.cs @@ -34,8 +34,9 @@ public void DataFrames() var privKey = new PrivateKey(); AppProtocolVersion ver = AppProtocolVersion.Sign(privKey, 3); Peer peer = new BoundPeer(privKey.PublicKey, new DnsEndPoint("0.0.0.0", 1234)); - NetMQFrame[] frames = - msg.ToNetMQMessage(privKey, peer, ver).Skip(Message.CommonFrames).ToArray(); + NetMQFrame[] frames = msg.ToNetMQMessage(privKey, peer, DateTimeOffset.UtcNow, ver) + .Skip(Message.CommonFrames) + .ToArray(); var restored = new BlockHashes(frames); Assert.Equal(msg.StartIndex, restored.StartIndex); Assert.Equal(msg.Hashes, restored.Hashes); diff --git a/Libplanet.Tests/Net/Messages/MessageTest.cs b/Libplanet.Tests/Net/Messages/MessageTest.cs index d78e1b25b7..bf2fb86ca6 100644 --- a/Libplanet.Tests/Net/Messages/MessageTest.cs +++ b/Libplanet.Tests/Net/Messages/MessageTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; using Libplanet.Crypto; using Libplanet.Net; @@ -25,7 +26,11 @@ public void DifferentAppProtocolVersionWithSameStructure() ImmutableArray.Empty, default(Address)); var message = new Ping(); - var netMQMessage = message.ToNetMQMessage(privateKey, peer, validAppProtocolVersion); + var netMQMessage = message.ToNetMQMessage( + privateKey, + peer, + DateTimeOffset.UtcNow, + validAppProtocolVersion); Assert.Throws(() => Message.Parse( diff --git a/Libplanet.Tests/Net/SwarmTest.cs b/Libplanet.Tests/Net/SwarmTest.cs index d69a0b1bb3..f5c63c8df0 100644 --- a/Libplanet.Tests/Net/SwarmTest.cs +++ b/Libplanet.Tests/Net/SwarmTest.cs @@ -588,7 +588,11 @@ public async Task GetMultipleBlocksAtOnce() { var request = new GetBlocks(hashes.Select(pair => pair.Item2), 2); socket.SendMultipartMessage( - request.ToNetMQMessage(privateKey, swarmB.AsPeer, swarmB.AppProtocolVersion) + request.ToNetMQMessage( + privateKey, + swarmB.AsPeer, + DateTimeOffset.UtcNow, + swarmB.AppProtocolVersion) ); NetMQMessage response = socket.ReceiveMultipartMessage(); @@ -2374,7 +2378,11 @@ public async Task BlockDemand() var request = new BlockHeaderMessage(swarm.BlockChain.Genesis.Hash, higherBlock.Header); socket.SendMultipartMessage( - request.ToNetMQMessage(privateKey1, sender1, swarm.AppProtocolVersion) + request.ToNetMQMessage( + privateKey1, + sender1, + DateTimeOffset.UtcNow, + swarm.AppProtocolVersion) ); await swarm.BlockHeaderReceived.WaitAsync(); await Task.Delay(100); @@ -2385,7 +2393,11 @@ public async Task BlockDemand() request = new BlockHeaderMessage(swarm.BlockChain.Genesis.Hash, lowerBlock.Header); socket.SendMultipartMessage( - request.ToNetMQMessage(privateKey2, sender2, swarm.AppProtocolVersion) + request.ToNetMQMessage( + privateKey2, + sender2, + DateTimeOffset.UtcNow, + swarm.AppProtocolVersion) ); await swarm.BlockHeaderReceived.WaitAsync(); // Await for context change @@ -2401,7 +2413,11 @@ await Task.Delay( request = new BlockHeaderMessage(swarm.BlockChain.Genesis.Hash, higherBlock.Header); socket.SendMultipartMessage( - request.ToNetMQMessage(privateKey1, sender1, swarm.AppProtocolVersion) + request.ToNetMQMessage( + privateKey1, + sender1, + DateTimeOffset.UtcNow, + swarm.AppProtocolVersion) ); await swarm.BlockHeaderReceived.WaitAsync(); await Task.Delay(100); @@ -2413,7 +2429,11 @@ await Task.Delay( request = new BlockHeaderMessage(swarm.BlockChain.Genesis.Hash, lowerBlock.Header); socket.SendMultipartMessage( - request.ToNetMQMessage(privateKey2, sender2, swarm.AppProtocolVersion) + request.ToNetMQMessage( + privateKey2, + sender2, + DateTimeOffset.UtcNow, + swarm.AppProtocolVersion) ); await swarm.BlockHeaderReceived.WaitAsync(); await Task.Delay(100); diff --git a/Libplanet/Net/Messages/Message.cs b/Libplanet/Net/Messages/Message.cs index 765c2b914c..f730351829 100644 --- a/Libplanet/Net/Messages/Message.cs +++ b/Libplanet/Net/Messages/Message.cs @@ -18,7 +18,7 @@ public abstract class Message /// /// The number of frames that all messages commonly contain. /// - public const int CommonFrames = 4; + public const int CommonFrames = 5; /// /// Enum represents the type of the . @@ -121,10 +121,15 @@ private enum MessageFrame /// Peer = 2, + /// + /// Frame containing the datetime when the is created. + /// + Timestamp = 3, + /// /// Frame containing signature of the . /// - Sign = 3, + Sign = 4, } /// @@ -140,6 +145,11 @@ private enum MessageFrame /// public AppProtocolVersion Version { get; set; } + /// + /// The timestamp of the message is created. + /// + public DateTimeOffset Timestamp { get; set; } + /// /// The sender of the message. /// @@ -191,8 +201,8 @@ public static Message Parse( throw new ArgumentException("Can't parse empty NetMQMessage."); } - // (reply == true) [version, type, peer, sign, frames...] - // (reply == false) [identity, version, type, peer, sign, frames...] + // (reply == true) [version, type, peer, timestamp, sign, frames...] + // (reply == false) [identity, version, type, peer, timestamp, sign, frames...] NetMQFrame[] remains = reply ? raw.ToArray() : raw.Skip(1).ToArray(); var versionToken = remains[(int)MessageFrame.Version].ConvertToString(); @@ -223,7 +233,7 @@ public static Message Parse( } var rawType = (MessageType)remains[(int)MessageFrame.Type].ConvertToInt32(); - var peer = remains[(int)MessageFrame.Peer].ToByteArray(); + var ticks = remains[(int)MessageFrame.Timestamp].ConvertToInt64(); byte[] signature = remains[(int)MessageFrame.Sign].ToByteArray(); NetMQFrame[] body = remains.Skip(CommonFrames).ToArray(); @@ -265,6 +275,7 @@ public static Message Parse( type, new[] { body }); message.Version = remoteVersion; message.Remote = remotePeer; + message.Timestamp = new DateTimeOffset(ticks, TimeSpan.Zero); if (!message.Remote.PublicKey.Verify(body.ToByteArray(), signature)) { @@ -287,13 +298,19 @@ public static Message Parse( /// -typed representation of the /// sender's transport layer. /// + /// The of the message is created. + /// /// -typed version of the /// transport layer. /// A containing the signed . /// /// Thrown when is /// null. - public NetMQMessage ToNetMQMessage(PrivateKey key, Peer peer, AppProtocolVersion version) + public NetMQMessage ToNetMQMessage( + PrivateKey key, + Peer peer, + DateTimeOffset timestamp, + AppProtocolVersion version) { if (peer is null) { @@ -310,6 +327,7 @@ public NetMQMessage ToNetMQMessage(PrivateKey key, Peer peer, AppProtocolVersion // Write headers. (inverse order) message.Push(key.Sign(message.ToByteArray())); + message.Push(timestamp.Ticks); message.Push(SerializePeer(peer)); message.Push((byte)Type); message.Push(version.Token); diff --git a/Libplanet/Net/NetMQTransport.cs b/Libplanet/Net/NetMQTransport.cs index e50e9e36b1..0104cc7224 100644 --- a/Libplanet/Net/NetMQTransport.cs +++ b/Libplanet/Net/NetMQTransport.cs @@ -490,7 +490,11 @@ public void ReplyMessage(Message message) { string identityHex = ByteUtil.Hex(message.Identity); _logger.Debug("Reply {Message} to {Identity}...", message, identityHex); - _replyQueue.Enqueue(message.ToNetMQMessage(_privateKey, AsPeer, _appProtocolVersion)); + _replyQueue.Enqueue(message.ToNetMQMessage( + _privateKey, + AsPeer, + DateTimeOffset.UtcNow, + _appProtocolVersion)); } private void ReceiveMessage(object sender, NetMQSocketEventArgs e) @@ -568,7 +572,11 @@ private void DoBroadcast(object sender, NetMQQueueEventArgs<(Address?, Message)> _logger.Debug("Broadcasting message: {Message} as {AsPeer}", msg, AsPeer); _logger.Debug("Peers to broadcast: {PeersCount}", peers.Count); - NetMQMessage message = msg.ToNetMQMessage(_privateKey, AsPeer, _appProtocolVersion); + NetMQMessage message = msg.ToNetMQMessage( + _privateKey, + AsPeer, + DateTimeOffset.UtcNow, + _appProtocolVersion); foreach (BoundPeer peer in peers) { @@ -716,7 +724,11 @@ private async Task ProcessRequest(MessageRequest req, CancellationToken cancella req.Message, req.Peer ); - var message = req.Message.ToNetMQMessage(_privateKey, AsPeer, _appProtocolVersion); + var message = req.Message.ToNetMQMessage( + _privateKey, + AsPeer, + DateTimeOffset.UtcNow, + _appProtocolVersion); var result = new List(); TaskCompletionSource> tcs = req.TaskCompletionSource; try From fa50945ced2aa93b8f8cfbd32d63f7d3d1149745 Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Tue, 26 Jan 2021 16:30:07 +0900 Subject: [PATCH 2/9] Add unit test for message parsing --- Libplanet.Tests/Net/Messages/MessageTest.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Libplanet.Tests/Net/Messages/MessageTest.cs b/Libplanet.Tests/Net/Messages/MessageTest.cs index bf2fb86ca6..308d198899 100644 --- a/Libplanet.Tests/Net/Messages/MessageTest.cs +++ b/Libplanet.Tests/Net/Messages/MessageTest.cs @@ -65,5 +65,25 @@ public void DifferentAppProtocolVersionWithDifferentStructure() ImmutableHashSet.Empty, null)); } + + [Fact] + public void Parse() + { + var privateKey = new PrivateKey(); + var peer = new Peer(privateKey.PublicKey); + var dateTimeOffset = DateTimeOffset.UtcNow; + var appProtocolVersion = new AppProtocolVersion( + 1, + new Bencodex.Types.Integer(0), + ImmutableArray.Empty, + default(Address)); + var message = new Ping(); + NetMQMessage raw = + message.ToNetMQMessage(privateKey, peer, dateTimeOffset, appProtocolVersion); + var parsed = Message.Parse(raw, true, appProtocolVersion, null, null); + Assert.Equal(peer, parsed.Remote); + Assert.Equal(appProtocolVersion, parsed.Version); + Assert.Equal(dateTimeOffset, parsed.Timestamp); + } } } From 01a02747d63f947928e9c8b981ea8fa7a6fac2bc Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Wed, 27 Jan 2021 13:45:30 +0900 Subject: [PATCH 3/9] Check message timestamp validity --- Libplanet.Tests/Net/Messages/MessageTest.cs | 4 +- Libplanet.Tests/Net/SwarmTest.cs | 2 + Libplanet/Net/InvalidTimestampException.cs | 77 +++++++++++++++++++++ Libplanet/Net/Messages/Message.cs | 21 +++++- Libplanet/Net/NetMQTransport.cs | 40 ++++++++++- Libplanet/Net/Protocols/KademliaProtocol.cs | 11 +++ Libplanet/Net/Swarm.cs | 5 +- Libplanet/Net/SwarmOptions.cs | 6 ++ 8 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 Libplanet/Net/InvalidTimestampException.cs diff --git a/Libplanet.Tests/Net/Messages/MessageTest.cs b/Libplanet.Tests/Net/Messages/MessageTest.cs index 308d198899..0e2baa26c3 100644 --- a/Libplanet.Tests/Net/Messages/MessageTest.cs +++ b/Libplanet.Tests/Net/Messages/MessageTest.cs @@ -38,6 +38,7 @@ public void DifferentAppProtocolVersionWithSameStructure() true, invalidAppProtocolVersion, ImmutableHashSet.Empty, + null, null)); } @@ -63,6 +64,7 @@ public void DifferentAppProtocolVersionWithDifferentStructure() true, invalidAppProtocolVersion, ImmutableHashSet.Empty, + null, null)); } @@ -80,7 +82,7 @@ public void Parse() var message = new Ping(); NetMQMessage raw = message.ToNetMQMessage(privateKey, peer, dateTimeOffset, appProtocolVersion); - var parsed = Message.Parse(raw, true, appProtocolVersion, null, null); + var parsed = Message.Parse(raw, true, appProtocolVersion, null, null, null); Assert.Equal(peer, parsed.Remote); Assert.Equal(appProtocolVersion, parsed.Version); Assert.Equal(dateTimeOffset, parsed.Timestamp); diff --git a/Libplanet.Tests/Net/SwarmTest.cs b/Libplanet.Tests/Net/SwarmTest.cs index f5c63c8df0..f6630950e2 100644 --- a/Libplanet.Tests/Net/SwarmTest.cs +++ b/Libplanet.Tests/Net/SwarmTest.cs @@ -601,6 +601,7 @@ public async Task GetMultipleBlocksAtOnce() true, swarmA.AppProtocolVersion, swarmA.TrustedAppProtocolVersionSigners, + null, null); Libplanet.Net.Messages.Blocks blockMessage = (Libplanet.Net.Messages.Blocks)parsedMessage; @@ -613,6 +614,7 @@ public async Task GetMultipleBlocksAtOnce() true, swarmA.AppProtocolVersion, swarmA.TrustedAppProtocolVersionSigners, + null, null); blockMessage = (Libplanet.Net.Messages.Blocks)parsedMessage; diff --git a/Libplanet/Net/InvalidTimestampException.cs b/Libplanet/Net/InvalidTimestampException.cs new file mode 100644 index 0000000000..58c3227b84 --- /dev/null +++ b/Libplanet/Net/InvalidTimestampException.cs @@ -0,0 +1,77 @@ +#nullable enable +using System; +using System.Runtime.Serialization; + +namespace Libplanet.Net +{ + [Serializable] + public class InvalidTimestampException : Exception + { + internal InvalidTimestampException( + string message, + DateTimeOffset createdOffset, + TimeSpan lifespan, + DateTimeOffset currentOffset, + Exception innerException + ) + : base(message, innerException) + { + CreatedOffset = createdOffset; + Lifespan = lifespan; + CurrentOffset = currentOffset; + } + + internal InvalidTimestampException( + string message, + DateTimeOffset createdOffset, + TimeSpan lifespan, + DateTimeOffset currentOffset) + : base(message) + { + CreatedOffset = createdOffset; + Lifespan = lifespan; + CurrentOffset = currentOffset; + } + + protected InvalidTimestampException( + SerializationInfo info, + StreamingContext context + ) + : base(info, context) + { + CreatedOffset = info.GetValue(nameof(CreatedOffset), typeof(DateTimeOffset)) + is DateTimeOffset createdOffset + ? createdOffset + : throw new SerializationException( + $"{nameof(CreatedOffset)} is expected to be a non-null " + + $"{nameof(DateTimeOffset)}."); + Lifespan = info.GetValue(nameof(Lifespan), typeof(TimeSpan)) + is TimeSpan lifetime + ? lifetime + : throw new SerializationException( + $"{nameof(Lifespan)} is expected to be a non-null " + + $"{nameof(TimeSpan)}."); + CurrentOffset = info.GetValue(nameof(CurrentOffset), typeof(DateTimeOffset)) + is DateTimeOffset currentOffset + ? currentOffset + : throw new SerializationException( + $"{nameof(CurrentOffset)} is expected to be a non-null " + + $"{nameof(DateTimeOffset)}."); + } + + internal DateTimeOffset CreatedOffset { get; } + + internal TimeSpan Lifespan { get; } + + internal DateTimeOffset CurrentOffset { get; } + + public override void GetObjectData( + SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(CreatedOffset), CreatedOffset); + info.AddValue(nameof(Lifespan), Lifespan); + info.AddValue(nameof(CurrentOffset), CurrentOffset); + } + } +} diff --git a/Libplanet/Net/Messages/Message.cs b/Libplanet/Net/Messages/Message.cs index f730351829..31c2b00029 100644 --- a/Libplanet/Net/Messages/Message.cs +++ b/Libplanet/Net/Messages/Message.cs @@ -178,6 +178,10 @@ private enum MessageFrame /// If this callback returns false, an encountered peer is ignored. If this callback /// is omitted, all peers with different s are ignored. /// + /// + /// The lifetime of a message. + /// Messages generated before this value from the current time are ignored. + /// If null is given, messages will not be ignored by its timestamp. /// A parsed from . /// Thrown when empty is given. /// @@ -194,7 +198,8 @@ public static Message Parse( bool reply, AppProtocolVersion localVersion, IImmutableSet trustedAppProtocolVersionSigners, - DifferentAppProtocolVersionEncountered differentAppProtocolVersionEncountered) + DifferentAppProtocolVersionEncountered differentAppProtocolVersionEncountered, + TimeSpan? lifetime) { if (raw.FrameCount == 0) { @@ -234,6 +239,18 @@ public static Message Parse( var rawType = (MessageType)remains[(int)MessageFrame.Type].ConvertToInt32(); var ticks = remains[(int)MessageFrame.Timestamp].ConvertToInt64(); + var timestamp = new DateTimeOffset(ticks, TimeSpan.Zero); + + var currentTime = DateTimeOffset.UtcNow; + if (!(lifetime is null) && + (currentTime < timestamp || timestamp + lifetime < currentTime)) + { + var msg = $"Received message is invalid, created at {timestamp} " + + $"but designated lifetime is {lifetime} and the current datetime " + + $"offset is {currentTime}."; + throw new InvalidTimestampException(msg, timestamp, lifetime.Value, currentTime); + } + byte[] signature = remains[(int)MessageFrame.Sign].ToByteArray(); NetMQFrame[] body = remains.Skip(CommonFrames).ToArray(); @@ -275,7 +292,7 @@ public static Message Parse( type, new[] { body }); message.Version = remoteVersion; message.Remote = remotePeer; - message.Timestamp = new DateTimeOffset(ticks, TimeSpan.Zero); + message.Timestamp = timestamp; if (!message.Remote.PublicKey.Verify(body.ToByteArray(), signature)) { diff --git a/Libplanet/Net/NetMQTransport.cs b/Libplanet/Net/NetMQTransport.cs index 0104cc7224..e77da6b4cc 100644 --- a/Libplanet/Net/NetMQTransport.cs +++ b/Libplanet/Net/NetMQTransport.cs @@ -40,6 +40,7 @@ public class NetMQTransport : ITransport private readonly IList _iceServers; private readonly ILogger _logger; private readonly AsyncLock _turnClientMutex; + private readonly TimeSpan? _messageLifespan; private NetMQQueue _replyQueue; private NetMQQueue<(Address?, Message)> _broadcastQueue; @@ -98,6 +99,10 @@ public class NetMQTransport : ITransport /// If this callback returns false, an encountered peer is ignored. If this callback /// is omitted, all peers with different s are ignored. /// + /// + /// The lifespan of a message. + /// Messages generated before this value from the current time are ignored. + /// If null is given, messages will not be ignored by its timestamp. /// Thrown when both and /// are null. public NetMQTransport( @@ -109,7 +114,8 @@ public NetMQTransport( string host, int? listenPort, IEnumerable iceServers, - DifferentAppProtocolVersionEncountered differentAppProtocolVersionEncountered) + DifferentAppProtocolVersionEncountered differentAppProtocolVersionEncountered, + TimeSpan? messageLifespan = null) { Running = false; @@ -121,6 +127,7 @@ public NetMQTransport( _differentAppProtocolVersionEncountered = differentAppProtocolVersionEncountered; _turnClientMutex = new AsyncLock(); _table = table; + _messageLifespan = messageLifespan; if (_host != null && _listenPort is int listenPortAsInt) { @@ -447,6 +454,20 @@ await _requests.AddAsync( _logger.Error(e, logMsg, peer.Address, reqId, e.ExpectedVersion, e.ActualVersion); throw; } + catch (InvalidTimestampException ite) + { + const string logMsg = + "{PeerAddress} sent a reply to {RequestId} with stale timestamp; " + + "(timestamp: {Timestamp}, lifespan: {Lifespan}, current: {Current})"; + _logger.Error( + logMsg, + peer.Address, + reqId, + ite.CreatedOffset, + ite.Lifespan, + ite.CurrentOffset); + throw; + } catch (TimeoutException) { _logger.Debug( @@ -518,8 +539,10 @@ private void ReceiveMessage(object sender, NetMQSocketEventArgs e) false, _appProtocolVersion, _trustedAppProtocolVersionSigners, - _differentAppProtocolVersionEncountered); + _differentAppProtocolVersionEncountered, + _messageLifespan); _logger.Debug("A message has parsed: {0}, from {1}", message, message.Remote); + MessageHistory.Enqueue(message); LastMessageTimestamp = DateTimeOffset.UtcNow; @@ -545,6 +568,12 @@ private void ReceiveMessage(object sender, NetMQSocketEventArgs e) ReplyMessage(differentVersion); _logger.Debug("Message from peer with different version received."); } + catch (InvalidTimestampException ite) + { + const string logMsg = "The received message is stale. " + + "(timestamp: {Timestamp}, lifespan: {Lifespan}, current: {Current})"; + _logger.Debug(logMsg, ite.CreatedOffset, ite.Lifespan, ite.CurrentOffset); + } catch (InvalidMessageException ex) { _logger.Error(ex, $"Could not parse NetMQMessage properly; ignore: {{0}}", ex); @@ -756,7 +785,8 @@ await dealer.SendMultipartMessageAsync( true, _appProtocolVersion, _trustedAppProtocolVersionSigners, - _differentAppProtocolVersionEncountered); + _differentAppProtocolVersionEncountered, + _messageLifespan); _logger.Debug( "A reply has parsed: {Reply} from {ReplyRemote}", reply, @@ -772,6 +802,10 @@ await dealer.SendMultipartMessageAsync( { tcs.TrySetException(dapve); } + catch (InvalidTimestampException ite) + { + tcs.TrySetException(ite); + } catch (TimeoutException te) { tcs.TrySetException(te); diff --git a/Libplanet/Net/Protocols/KademliaProtocol.cs b/Libplanet/Net/Protocols/KademliaProtocol.cs index 92410f2991..e260eeaf8f 100644 --- a/Libplanet/Net/Protocols/KademliaProtocol.cs +++ b/Libplanet/Net/Protocols/KademliaProtocol.cs @@ -467,6 +467,12 @@ internal async Task PingAsync( target, $"Timeout occurred during dial to {target}."); } + catch (InvalidTimestampException) + { + throw new PingTimeoutException( + target, + $"Received Pong from {target} has invalid timestamp."); + } catch (DifferentAppProtocolVersionException) { _logger.Error( @@ -652,6 +658,11 @@ private async Task> GetNeighbors( return neighbors.Found; } + catch (InvalidTimestampException) + { + _logger.Debug($"Reply of {nameof(GetNeighbors)}'s timestamp is stale."); + return ImmutableArray.Empty; + } catch (TimeoutException) { RemovePeer(addressee); diff --git a/Libplanet/Net/Swarm.cs b/Libplanet/Net/Swarm.cs index 7d934c62f3..95e9dbfb5d 100644 --- a/Libplanet/Net/Swarm.cs +++ b/Libplanet/Net/Swarm.cs @@ -142,6 +142,7 @@ internal Swarm( _logger = Log.ForContext>() .ForContext("SwarmId", loggerId); + Options = options ?? new SwarmOptions(); RoutingTable = new RoutingTable(Address, tableSize, bucketSize); Transport = new NetMQTransport( RoutingTable, @@ -152,10 +153,10 @@ internal Swarm( host, listenPort, iceServers, - differentAppProtocolVersionEncountered); + differentAppProtocolVersionEncountered, + Options.MessageLifespan); Transport.ProcessMessageHandler += ProcessMessageHandler; PeerDiscovery = new KademliaProtocol(RoutingTable, Transport, Address); - Options = options ?? new SwarmOptions(); } ~Swarm() diff --git a/Libplanet/Net/SwarmOptions.cs b/Libplanet/Net/SwarmOptions.cs index dee4249966..d0bbc1b789 100644 --- a/Libplanet/Net/SwarmOptions.cs +++ b/Libplanet/Net/SwarmOptions.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using Libplanet.Blocks; +using Libplanet.Net.Messages; using Libplanet.Tx; namespace Libplanet.Net @@ -41,5 +42,10 @@ public class SwarmOptions /// The lifespan of block demand. /// public TimeSpan BlockDemandLifespan { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// The lifespan of . + /// + public TimeSpan? MessageLifespan { get; set; } = null; } } From be8f55fdde82661add0dacbd4528bd1cf7e6da7f Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Wed, 27 Jan 2021 13:45:50 +0900 Subject: [PATCH 4/9] Add unit test testing message timestamp validity --- Libplanet.Tests/Net/Messages/MessageTest.cs | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Libplanet.Tests/Net/Messages/MessageTest.cs b/Libplanet.Tests/Net/Messages/MessageTest.cs index 0e2baa26c3..3975379f14 100644 --- a/Libplanet.Tests/Net/Messages/MessageTest.cs +++ b/Libplanet.Tests/Net/Messages/MessageTest.cs @@ -68,6 +68,43 @@ public void DifferentAppProtocolVersionWithDifferentStructure() null)); } + [Fact] + public void InvalidTimestamp() + { + var privateKey = new PrivateKey(); + var peer = new Peer(privateKey.PublicKey); + var futureOffset = DateTimeOffset.MaxValue; + var pastOffset = DateTimeOffset.MinValue; + var appProtocolVersion = new AppProtocolVersion( + 1, + new Bencodex.Types.Integer(0), + ImmutableArray.Empty, + default(Address)); + var message = new Ping(); + NetMQMessage futureRaw = + message.ToNetMQMessage(privateKey, peer, futureOffset, appProtocolVersion); + // Messages from the future throws InvalidTimestampException. + Assert.Throws(() => + Message.Parse( + futureRaw, + true, + appProtocolVersion, + ImmutableHashSet.Empty, + null, + TimeSpan.FromSeconds(1))); + NetMQMessage pastRaw = + message.ToNetMQMessage(privateKey, peer, futureOffset, appProtocolVersion); + // Messages from the far past throws InvalidTimestampException. + Assert.Throws(() => + Message.Parse( + pastRaw, + true, + appProtocolVersion, + ImmutableHashSet.Empty, + null, + TimeSpan.FromSeconds(1))); + } + [Fact] public void Parse() { From 6d64786d3aaf8476b25203389d9ec5b9ad92e9e2 Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Wed, 27 Jan 2021 14:03:44 +0900 Subject: [PATCH 5/9] Update CHANGES.md --- CHANGES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e224c0d743..55aa542a82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -49,6 +49,9 @@ To be released. - `RecentStates` message type (with the type number `0x13`) - `GetBlockStates` message type (with the type number `0x22`) - `BlockStates` message type (with the type number `0x23`) + - `Swarm` became to ignore messages more than a certain amount of time + since they were created, the value is `SwarmOptions.MessageLifespan`. + [[#1160], [#1171]] ### Backward-incompatible storage format changes @@ -211,11 +214,13 @@ To be released. [#1149]: https://github.com/planetarium/libplanet/pull/1149 [#1152]: https://github.com/planetarium/libplanet/pull/1152 [#1155]: https://github.com/planetarium/libplanet/issues/1155 +[#1160]: https://github.com/planetarium/libplanet/issues/1160 [#1162]: https://github.com/planetarium/libplanet/pull/1162 [#1163]: https://github.com/planetarium/libplanet/pull/1163 [#1165]: https://github.com/planetarium/libplanet/pull/1165 [#1168]: https://github.com/planetarium/libplanet/pull/1168 [#1170]: https://github.com/planetarium/libplanet/pull/1170 +[#1171]: https://github.com/planetarium/libplanet/pull/1171 [#1172]: https://github.com/planetarium/libplanet/pull/1172 From d66f2fdde7d32a8ced77348347fad3f40185aeb4 Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Fri, 29 Jan 2021 17:11:25 +0900 Subject: [PATCH 6/9] Apply suggestions from the code review --- CHANGES.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 55aa542a82..ec1df3ae67 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,9 @@ To be released. - Removed `trustedStateValidators` parameter from `Swarm.PreloadAsync()` method. [[#1117]] - Added `IActionContext.BlockAction` property. [[#1143]] + - Added nullable `TimeSpan`-typed `messageLifespan` parameter into + `NetMQTransport` constructor. [[#1171]] + ### Backward-incompatible network protocol changes @@ -49,9 +52,8 @@ To be released. - `RecentStates` message type (with the type number `0x13`) - `GetBlockStates` message type (with the type number `0x22`) - `BlockStates` message type (with the type number `0x23`) - - `Swarm` became to ignore messages more than a certain amount of time - since they were created, the value is `SwarmOptions.MessageLifespan`. - [[#1160], [#1171]] + - `Swarm` became to ignore messages made earlier than a certain amount of time, + which is configured by `SwarmOptions.MessageLifespan`. [[#1160], [#1171]] ### Backward-incompatible storage format changes @@ -122,6 +124,8 @@ To be released. - Added `InvalidBlockPreEvaluationHashException` class. [[#1148]] - Added the parameter `validate` which is `true` by default, to `Transaction.Deserialize()`. [[#1149]] + - Added `SwarmOptions.MessageLifespan` property. [[#1171]] + - Added `InvalidTimestampException` class. [[#1171]] ### Behavioral changes From ee4b88910321020b059882ae7231766eb0f58a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hong=20Minhee=20=28=E6=B4=AA=20=E6=B0=91=E6=86=99=29?= Date: Fri, 29 Jan 2021 18:02:26 +0900 Subject: [PATCH 7/9] Trivial adjustments on the changelog --- CHANGES.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ec1df3ae67..844cab12f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,7 +36,7 @@ To be released. method. [[#1117]] - Added `IActionContext.BlockAction` property. [[#1143]] - Added nullable `TimeSpan`-typed `messageLifespan` parameter into - `NetMQTransport` constructor. [[#1171]] + `NetMQTransport()` constructor. [[#1171]] ### Backward-incompatible network protocol changes @@ -52,8 +52,9 @@ To be released. - `RecentStates` message type (with the type number `0x13`) - `GetBlockStates` message type (with the type number `0x22`) - `BlockStates` message type (with the type number `0x23`) - - `Swarm` became to ignore messages made earlier than a certain amount of time, - which is configured by `SwarmOptions.MessageLifespan`. [[#1160], [#1171]] + - `Swarm` became to ignore messages made earlier than a certain amount of + time, which is configured by `SwarmOptions.MessageLifespan`. + [[#1160], [#1171]] ### Backward-incompatible storage format changes From 48bc0246e39d8703dd271b4e828f90dd0a473b75 Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Fri, 29 Jan 2021 19:26:50 +0900 Subject: [PATCH 8/9] Fix MessageTest.InvalidTimestamp test --- Libplanet.Tests/Net/Messages/MessageTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libplanet.Tests/Net/Messages/MessageTest.cs b/Libplanet.Tests/Net/Messages/MessageTest.cs index 3975379f14..298897892e 100644 --- a/Libplanet.Tests/Net/Messages/MessageTest.cs +++ b/Libplanet.Tests/Net/Messages/MessageTest.cs @@ -93,7 +93,7 @@ public void InvalidTimestamp() null, TimeSpan.FromSeconds(1))); NetMQMessage pastRaw = - message.ToNetMQMessage(privateKey, peer, futureOffset, appProtocolVersion); + message.ToNetMQMessage(privateKey, peer, pastOffset, appProtocolVersion); // Messages from the far past throws InvalidTimestampException. Assert.Throws(() => Message.Parse( From 034ad3859cfd8c7cb61ff90388febdaae997f32b Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Fri, 29 Jan 2021 19:28:09 +0900 Subject: [PATCH 9/9] Fix calender issue in DateTimeOffset.ToString --- Libplanet/Net/Messages/Message.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Libplanet/Net/Messages/Message.cs b/Libplanet/Net/Messages/Message.cs index 31c2b00029..f8e64c4afe 100644 --- a/Libplanet/Net/Messages/Message.cs +++ b/Libplanet/Net/Messages/Message.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; @@ -20,6 +21,8 @@ public abstract class Message /// public const int CommonFrames = 5; + internal const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; + /// /// Enum represents the type of the . /// @@ -245,9 +248,11 @@ public static Message Parse( if (!(lifetime is null) && (currentTime < timestamp || timestamp + lifetime < currentTime)) { - var msg = $"Received message is invalid, created at {timestamp} " + + var msg = $"Received message is invalid, created at " + + $"{timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture)} " + $"but designated lifetime is {lifetime} and the current datetime " + - $"offset is {currentTime}."; + $"offset is " + + $"{currentTime.ToString(TimestampFormat, CultureInfo.InvariantCulture)}."; throw new InvalidTimestampException(msg, timestamp, lifetime.Value, currentTime); }