From 9897b9043acd91d02a546a956bf62af4cbf86a2c Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 21:33:39 +0900 Subject: [PATCH 1/3] feat: Add CancelTrace method to the GasTracer --- src/Libplanet.Action/ActionEvaluator.cs | 5 +++-- src/Libplanet.Action/GasMeter.cs | 20 ++++++----------- src/Libplanet.Action/GasTracer.cs | 30 ++++++++++++++++++++----- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Libplanet.Action/ActionEvaluator.cs b/src/Libplanet.Action/ActionEvaluator.cs index c95607613d4..5dc9ab2fe79 100644 --- a/src/Libplanet.Action/ActionEvaluator.cs +++ b/src/Libplanet.Action/ActionEvaluator.cs @@ -471,13 +471,14 @@ internal IEnumerable EvaluateTx( IWorld previousState) { GasTracer.Initialize(tx.GasLimit ?? long.MaxValue); - GasTracer.StartTrace(); var evaluations = ImmutableList.Empty; if (_policyActionsRegistry.BeginTxActions.Length > 0) { + GasTracer.IsTxAction = true; evaluations = evaluations.AddRange( EvaluatePolicyBeginTxActions(block, tx, previousState)); previousState = evaluations.Last().OutputState; + GasTracer.IsTxAction = false; } ImmutableList actions = @@ -500,7 +501,7 @@ internal IEnumerable EvaluateTx( EvaluatePolicyEndTxActions(block, tx, previousState)); } - GasTracer.EndTrace(); + GasTracer.Release(); return evaluations; } diff --git a/src/Libplanet.Action/GasMeter.cs b/src/Libplanet.Action/GasMeter.cs index 5e06a8c59de..767bd79eea2 100644 --- a/src/Libplanet.Action/GasMeter.cs +++ b/src/Libplanet.Action/GasMeter.cs @@ -2,10 +2,14 @@ namespace Libplanet.Action { internal class GasMeter : IGasMeter { - public GasMeter(long gasLimit, long gasUsed = 0) + public GasMeter(long gasLimit) { - SetGasLimit(gasLimit); - GasUsed = gasUsed; + if (gasLimit < 0) + { + throw new GasLimitNegativeException(); + } + + GasLimit = gasLimit; } public long GasAvailable => GasLimit - GasUsed; @@ -39,15 +43,5 @@ public void UseGas(long gas) GasUsed = newGasUsed; } - - private void SetGasLimit(long gasLimit) - { - if (gasLimit < 0) - { - throw new GasLimitNegativeException(); - } - - GasLimit = gasLimit; - } } } diff --git a/src/Libplanet.Action/GasTracer.cs b/src/Libplanet.Action/GasTracer.cs index 3a5ce0ce7aa..364a7e26c28 100644 --- a/src/Libplanet.Action/GasTracer.cs +++ b/src/Libplanet.Action/GasTracer.cs @@ -16,6 +16,8 @@ public static class GasTracer private static readonly AsyncLocal IsTrace = new AsyncLocal(); + private static readonly AsyncLocal IsTraceCancelled = new AsyncLocal(); + /// /// The amount of gas used so far. /// @@ -26,6 +28,8 @@ public static class GasTracer /// public static long GasAvailable => GasMeterValue.GasAvailable; + internal static bool IsTxAction { get; set; } + private static GasMeter GasMeterValue => GasMeter.Value ?? throw new InvalidOperationException( "GasTracer is not initialized."); @@ -41,23 +45,39 @@ public static void UseGas(long gas) if (IsTrace.Value) { GasMeterValue.UseGas(gas); + if (IsTraceCancelled.Value) + { + throw new InvalidOperationException("GasTracing was canceled."); + } } } - internal static void Initialize(long gasLimit) + public static void CancelTrace() { - GasMeter.Value = new GasMeter(gasLimit); - IsTrace.Value = false; + if (!IsTxAction) + { + throw new InvalidOperationException("CancelTrace can only be called in TxAction."); + } + + if (IsTraceCancelled.Value) + { + throw new InvalidOperationException("GasTracing is already canceled."); + } + + IsTraceCancelled.Value = true; } - internal static void StartTrace() + internal static void Initialize(long gasLimit) { + GasMeter.Value = new GasMeter(gasLimit); IsTrace.Value = true; + IsTraceCancelled.Value = false; } - internal static void EndTrace() + internal static void Release() { IsTrace.Value = false; + IsTraceCancelled.Value = false; } } } From 1169be3e6d1130a50ef1b4053501544f1103ca6b Mon Sep 17 00:00:00 2001 From: s2quake Date: Sat, 26 Oct 2024 19:56:26 +0900 Subject: [PATCH 2/3] test: Add test for GasTracer cancelling --- .../Action/ActionEvaluatorTest.GasTracer.cs | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 test/Libplanet.Tests/Action/ActionEvaluatorTest.GasTracer.cs diff --git a/test/Libplanet.Tests/Action/ActionEvaluatorTest.GasTracer.cs b/test/Libplanet.Tests/Action/ActionEvaluatorTest.GasTracer.cs new file mode 100644 index 00000000000..8e9f68403f3 --- /dev/null +++ b/test/Libplanet.Tests/Action/ActionEvaluatorTest.GasTracer.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Blockchain; +using Libplanet.Blockchain.Policies; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; +using Xunit; +using static Libplanet.Action.State.KeyConverters; +using static Libplanet.Tests.TestUtils; + +namespace Libplanet.Tests.Action +{ + public partial class ActionEvaluatorTest + { + [Theory] + [InlineData(false, 1, 1)] + [InlineData(true, 1, 0)] + public void Evaluate_WithGasTracer( + bool cancelTrace, long goldAmount, long expectedGoldAmount) + { + var gold = Currency.Uncapped("FOO", 18, null); + var gas = Currency.Uncapped("GAS", 18, null); + var privateKey = new PrivateKey(); + var policy = new BlockPolicy( + new PolicyActionsRegistry( + beginBlockActions: ImmutableArray.Empty, + endBlockActions: ImmutableArray.Empty, + beginTxActions: ImmutableArray.Create( + new GasTraceAction() { CancelTrace = cancelTrace }), + endTxActions: ImmutableArray.Empty), + getMaxTransactionsBytes: _ => 50 * 1024); + + var store = new MemoryStore(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + var chain = TestUtils.MakeBlockChain( + policy: policy, + store: store, + stateStore: stateStore, + actionLoader: new SingleActionLoader(typeof(UseGasAction))); + var action = new UseGasAction + { + GasUsage = 10, + MintValue = gold * goldAmount, + Receiver = privateKey.Address, + Memo = string.Empty, + }; + + var tx = Transaction.Create( + nonce: 0, + privateKey: privateKey, + genesisHash: chain.Genesis.Hash, + actions: new[] { action }.ToPlainValues(), + maxGasPrice: gas * 10, + gasLimit: 10); + var expectedGold = gold * expectedGoldAmount; + + chain.StageTransaction(tx); + var miner = new PrivateKey(); + Block block = chain.ProposeBlock(miner); + chain.Append(block, CreateBlockCommit(block)); + var evaluations = chain.ActionEvaluator.Evaluate( + block, chain.GetNextStateRootHash((BlockHash)block.PreviousHash)); + + var actualGold = chain.GetNextWorldState().GetBalance(privateKey.Address, gold); + + Assert.Equal(expectedGold, actualGold); + } + + [Fact] + public void Evaluate_CancelTrace_BeginBlockAction_Throw() + { + var policy = new BlockPolicy( + new PolicyActionsRegistry( + beginBlockActions: ImmutableArray.Create( + new GasTraceAction() { CancelTrace = true }), + endBlockActions: ImmutableArray.Empty, + beginTxActions: ImmutableArray.Empty, + endTxActions: ImmutableArray.Empty), + getMaxTransactionsBytes: _ => 50 * 1024); + var evaluations = Evaluate_CancelTrace(policy); + var exception = (UnexpectedlyTerminatedActionException)evaluations[0].Exception; + + Assert.IsType(exception.Action); + Assert.IsType(exception.InnerException); + Assert.Equal( + "CancelTrace can only be called in TxAction.", exception.InnerException.Message); + } + + [Fact] + public void Evaluate_CancelTrace_EndBlockAction_Throw() + { + var policy = new BlockPolicy( + new PolicyActionsRegistry( + beginBlockActions: ImmutableArray.Empty, + endBlockActions: ImmutableArray.Create( + new GasTraceAction() { CancelTrace = true }), + beginTxActions: ImmutableArray.Empty, + endTxActions: ImmutableArray.Empty), + getMaxTransactionsBytes: _ => 50 * 1024); + var evaluations = Evaluate_CancelTrace(policy); + var exception = (UnexpectedlyTerminatedActionException)evaluations[1].Exception; + + Assert.IsType(exception.Action); + Assert.IsType(exception.InnerException); + Assert.Equal( + "CancelTrace can only be called in TxAction.", exception.InnerException.Message); + } + + [Fact] + public void Evaluate_CancelTrace_EndTxAction_Throw() + { + var policy = new BlockPolicy( + new PolicyActionsRegistry( + beginBlockActions: ImmutableArray.Empty, + endBlockActions: ImmutableArray.Empty, + beginTxActions: ImmutableArray.Empty, + endTxActions: ImmutableArray.Create( + new GasTraceAction() { CancelTrace = true })), + getMaxTransactionsBytes: _ => 50 * 1024); + var evaluations = Evaluate_CancelTrace(policy); + var exception = (UnexpectedlyTerminatedActionException)evaluations[1].Exception; + + Assert.IsType(exception.Action); + Assert.IsType(exception.InnerException); + Assert.Equal( + "CancelTrace can only be called in TxAction.", exception.InnerException.Message); + } + + [Fact] + public void Evaluate_CancelTrace_Action_Throw() + { + var policy = new BlockPolicy( + new PolicyActionsRegistry( + beginBlockActions: ImmutableArray.Empty, + endBlockActions: ImmutableArray.Empty, + beginTxActions: ImmutableArray.Empty, + endTxActions: ImmutableArray.Empty), + getMaxTransactionsBytes: _ => 50 * 1024); + var gold = Currency.Uncapped("FOO", 18, null); + var gas = Currency.Uncapped("GAS", 18, null); + var privateKey = new PrivateKey(); + + var store = new MemoryStore(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + var chain = TestUtils.MakeBlockChain( + policy: policy, + store: store, + stateStore: stateStore, + actionLoader: new SingleActionLoader(typeof(GasTraceAction))); + var action = new GasTraceAction + { + CancelTrace = true, + }; + + var tx = Transaction.Create( + nonce: 0, + privateKey: privateKey, + genesisHash: chain.Genesis.Hash, + actions: new[] { action }.ToPlainValues(), + maxGasPrice: gas * 10, + gasLimit: 10); + + chain.StageTransaction(tx); + var miner = new PrivateKey(); + Block block = chain.ProposeBlock(miner); + chain.Append(block, CreateBlockCommit(block)); + var evaluations = chain.ActionEvaluator.Evaluate( + block, chain.GetNextStateRootHash((BlockHash)block.PreviousHash)); + var exception = (UnexpectedlyTerminatedActionException)evaluations[0].Exception; + + Assert.IsType(exception.Action); + Assert.IsType(exception.InnerException); + Assert.Equal( + "CancelTrace can only be called in TxAction.", exception.InnerException.Message); + } + + private IReadOnlyList Evaluate_CancelTrace(BlockPolicy policy) + { + var gold = Currency.Uncapped("FOO", 18, null); + var gas = Currency.Uncapped("GAS", 18, null); + var privateKey = new PrivateKey(); + + var store = new MemoryStore(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + var chain = TestUtils.MakeBlockChain( + policy: policy, + store: store, + stateStore: stateStore, + actionLoader: new SingleActionLoader(typeof(UseGasAction))); + var action = new UseGasAction + { + GasUsage = 10, + MintValue = gold * 10, + Receiver = privateKey.Address, + Memo = string.Empty, + }; + + var tx = Transaction.Create( + nonce: 0, + privateKey: privateKey, + genesisHash: chain.Genesis.Hash, + actions: new[] { action }.ToPlainValues(), + maxGasPrice: gas * 10, + gasLimit: 10); + + chain.StageTransaction(tx); + var miner = new PrivateKey(); + Block block = chain.ProposeBlock(miner); + chain.Append(block, CreateBlockCommit(block)); + return chain.ActionEvaluator.Evaluate( + block, chain.GetNextStateRootHash((BlockHash)block.PreviousHash)); + } + + private sealed class GasTraceAction : IAction + { + public bool CancelTrace { get; set; } + + public IValue PlainValue => new List( + (Bencodex.Types.Boolean)CancelTrace); + + public void LoadPlainValue(IValue plainValue) + { + var list = (List)plainValue; + CancelTrace = (Bencodex.Types.Boolean)list[0]; + } + + public IWorld Execute(IActionContext context) + { + if (CancelTrace) + { + GasTracer.CancelTrace(); + } + + return context.PreviousState; + } + } + } +} From f2f8525712bc5b78a1ab967a7e28fb92b23cacb8 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 21:34:03 +0900 Subject: [PATCH 3/3] chore: Changes --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1d575cff428..73d6c092e6d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ To be released. - (Libplanet.Action) Added `GasTracer` static class. [[#3912]] - (Libplanet.Action) Added `LastCommit` property to `IActionContext` interface and its implementations. [[#3912]] - + - (Libplanet.Action) Added `CancelTrace` method to `GasTracer`. [[#3974]] ### Backward-incompatible network protocol changes @@ -40,6 +40,7 @@ To be released. ### CLI tools [#3912]: https://github.com/planetarium/libplanet/pull/3912 +[#3974]: https://github.com/planetarium/libplanet/pull/3974 Version 5.3.1