diff --git a/Explorer/Assets/DCL/Tests/Editor/DecentralandIdentityShould.cs b/Explorer/Assets/DCL/Tests/Editor/DecentralandIdentityShould.cs new file mode 100644 index 00000000000..732861ad511 --- /dev/null +++ b/Explorer/Assets/DCL/Tests/Editor/DecentralandIdentityShould.cs @@ -0,0 +1,80 @@ +using DCL.Web3; +using DCL.Web3.Abstract; +using DCL.Web3.Chains; +using DCL.Web3.Identities; +using NSubstitute; +using NUnit.Framework; +using System; + +namespace DCL.Tests.Editor +{ + public class DecentralandIdentityShould + { + private const string ADDRESS = "0x0000000000000000000000000000000000000001"; + + private static DecentralandIdentity NewIdentity() + { + AuthChain authChain = AuthChain.Create(); + authChain.SetSigner(ADDRESS); + + authChain.Set(new AuthLink + { + type = AuthLinkType.ECDSA_EPHEMERAL, + payload = "ephemeral payload", + signature = "0xephemeralsignature", + }); + + IWeb3Account ephemeralAccount = Substitute.For(); + ephemeralAccount.Sign(Arg.Any()).Returns("0xentitysignature"); + + return new DecentralandIdentity(new Web3Address(ADDRESS), ephemeralAccount, DateTime.UtcNow.AddDays(1), authChain, IWeb3Identity.Web3IdentitySource.None); + } + + [Test] + public void SignEntityWhileAlive() + { + // Arrange + DecentralandIdentity identity = NewIdentity(); + + // Act + AuthChain signed = identity.Sign("entityId"); + + // Assert + Assert.IsTrue(signed.TryGet(AuthLinkType.ECDSA_SIGNED_ENTITY, out AuthLink link)); + Assert.AreEqual("entityId", link.payload); + + signed.Dispose(); + identity.Dispose(); + } + + [Test] + public void ThrowObjectDisposedOnSignAfterDispose() + { + // Arrange + DecentralandIdentity identity = NewIdentity(); + + // Act + identity.Dispose(); + + // Assert + Assert.Throws(() => identity.Sign("entityId")); + } + + [Test] + public void ThrowObjectDisposedEvenAfterPoolReissuesItsAuthChain() + { + // Arrange + DecentralandIdentity identity = NewIdentity(); + identity.Dispose(); + + // Act - the pool can re-issue the identity's released AuthChain instance with the + // disposed flag reset; the identity guard must not depend on that pooled state. + AuthChain reissued = AuthChain.Create(); + + // Assert + Assert.Throws(() => identity.Sign("entityId")); + + reissued.Dispose(); + } + } +} diff --git a/Explorer/Assets/DCL/Tests/Editor/DecentralandIdentityShould.cs.meta b/Explorer/Assets/DCL/Tests/Editor/DecentralandIdentityShould.cs.meta new file mode 100644 index 00000000000..f2e30ec540c --- /dev/null +++ b/Explorer/Assets/DCL/Tests/Editor/DecentralandIdentityShould.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca6f29b7ea2f469a82a653db411b47c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Web3/Chains/AuthChain.cs b/Explorer/Assets/DCL/Web3/Chains/AuthChain.cs index a6c3ecaea09..8383d943efc 100644 --- a/Explorer/Assets/DCL/Web3/Chains/AuthChain.cs +++ b/Explorer/Assets/DCL/Web3/Chains/AuthChain.cs @@ -16,8 +16,16 @@ public class AuthChain : IEnumerable, IDisposable private bool disposed; - public static AuthChain Create() => - POOL.Get()!; + public static AuthChain Create() + { + // Reset the recycled instance: a pooled AuthChain comes back with disposed == true + // (set by the Dispose that released it), which made its next Dispose a no-op — the + // instance never returned to the pool again and reads saw stale state. + AuthChain instance = POOL.Get()!; + instance.disposed = false; + instance.chain.Clear(); + return instance; + } private AuthChain() { } diff --git a/Explorer/Assets/DCL/Web3/Identities/DecentralandIdentity.cs b/Explorer/Assets/DCL/Web3/Identities/DecentralandIdentity.cs index 71eb3728c0e..1b3de9ced2a 100644 --- a/Explorer/Assets/DCL/Web3/Identities/DecentralandIdentity.cs +++ b/Explorer/Assets/DCL/Web3/Identities/DecentralandIdentity.cs @@ -7,6 +7,8 @@ namespace DCL.Web3.Identities { public class DecentralandIdentity : IWeb3Identity { + private bool disposed; + public Web3Address Address { get; } public DateTime Expiration { get; } public IWeb3Account EphemeralAccount { get; } @@ -33,11 +35,22 @@ public DecentralandIdentity( public void Dispose() { + if (disposed) + return; + + disposed = true; AuthChain.Dispose(); } public AuthChain Sign(string entityId) { + // Tracked on the identity itself: the pooled AuthChain's disposed flag is reset when the + // pool re-issues the instance, so it cannot be trusted after this identity released it. + // ObjectDisposedException (not OperationCanceledException) so the failure is not silently + // swallowed by the standard catch (OperationCanceledException) { } blocks. + if (disposed) + throw new ObjectDisposedException(nameof(DecentralandIdentity), $"Cannot sign, the identity for {Address} has been disposed"); + if (Expiration < DateTime.UtcNow) throw new Web3IdentityException(this, $"Cannot sign, identity has expired: {Expiration:s}"); diff --git a/Explorer/Assets/DCL/Web3/Identities/IWeb3IdentityCache.cs b/Explorer/Assets/DCL/Web3/Identities/IWeb3IdentityCache.cs index c37e860da83..7e14cab02f1 100644 --- a/Explorer/Assets/DCL/Web3/Identities/IWeb3IdentityCache.cs +++ b/Explorer/Assets/DCL/Web3/Identities/IWeb3IdentityCache.cs @@ -18,8 +18,10 @@ class Fake : IWeb3IdentityCache { private readonly IWeb3Identity? identity; +#pragma warning disable CS0067 public event Action? OnIdentityCleared; public event Action? OnIdentityChanged; +#pragma warning restore CS0067 public Fake() : this(new IWeb3Identity.Random()) { }