Skip to content

Commit

Permalink
Merge pull request #3974 from planetarium/refactor/gas-tracer
Browse files Browse the repository at this point in the history
Refactor GasTracer
  • Loading branch information
s2quake authored Oct 28, 2024
2 parents 625e617 + f2f8525 commit c1a1c5e
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 21 deletions.
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/Libplanet.Action/ActionEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,14 @@ internal IEnumerable<ActionEvaluation> EvaluateTx(
IWorld previousState)
{
GasTracer.Initialize(tx.GasLimit ?? long.MaxValue);
GasTracer.StartTrace();
var evaluations = ImmutableList<ActionEvaluation>.Empty;
if (_policyActionsRegistry.BeginTxActions.Length > 0)
{
GasTracer.IsTxAction = true;
evaluations = evaluations.AddRange(
EvaluatePolicyBeginTxActions(block, tx, previousState));
previousState = evaluations.Last().OutputState;
GasTracer.IsTxAction = false;
}

ImmutableList<IAction> actions =
Expand All @@ -500,7 +501,7 @@ internal IEnumerable<ActionEvaluation> EvaluateTx(
EvaluatePolicyEndTxActions(block, tx, previousState));
}

GasTracer.EndTrace();
GasTracer.Release();

return evaluations;
}
Expand Down
20 changes: 7 additions & 13 deletions src/Libplanet.Action/GasMeter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,15 +43,5 @@ public void UseGas(long gas)

GasUsed = newGasUsed;
}

private void SetGasLimit(long gasLimit)
{
if (gasLimit < 0)
{
throw new GasLimitNegativeException();
}

GasLimit = gasLimit;
}
}
}
30 changes: 25 additions & 5 deletions src/Libplanet.Action/GasTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static class GasTracer

private static readonly AsyncLocal<bool> IsTrace = new AsyncLocal<bool>();

private static readonly AsyncLocal<bool> IsTraceCancelled = new AsyncLocal<bool>();

/// <summary>
/// The amount of gas used so far.
/// </summary>
Expand All @@ -26,6 +28,8 @@ public static class GasTracer
/// </summary>
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.");
Expand All @@ -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;
}
}
}
247 changes: 247 additions & 0 deletions test/Libplanet.Tests/Action/ActionEvaluatorTest.GasTracer.cs
Original file line number Diff line number Diff line change
@@ -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<IAction>.Empty,
endBlockActions: ImmutableArray<IAction>.Empty,
beginTxActions: ImmutableArray.Create<IAction>(
new GasTraceAction() { CancelTrace = cancelTrace }),
endTxActions: ImmutableArray<IAction>.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<IAction>(
new GasTraceAction() { CancelTrace = true }),
endBlockActions: ImmutableArray<IAction>.Empty,
beginTxActions: ImmutableArray<IAction>.Empty,
endTxActions: ImmutableArray<IAction>.Empty),
getMaxTransactionsBytes: _ => 50 * 1024);
var evaluations = Evaluate_CancelTrace(policy);
var exception = (UnexpectedlyTerminatedActionException)evaluations[0].Exception;

Assert.IsType<GasTraceAction>(exception.Action);
Assert.IsType<InvalidOperationException>(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<IAction>.Empty,
endBlockActions: ImmutableArray.Create<IAction>(
new GasTraceAction() { CancelTrace = true }),
beginTxActions: ImmutableArray<IAction>.Empty,
endTxActions: ImmutableArray<IAction>.Empty),
getMaxTransactionsBytes: _ => 50 * 1024);
var evaluations = Evaluate_CancelTrace(policy);
var exception = (UnexpectedlyTerminatedActionException)evaluations[1].Exception;

Assert.IsType<GasTraceAction>(exception.Action);
Assert.IsType<InvalidOperationException>(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<IAction>.Empty,
endBlockActions: ImmutableArray<IAction>.Empty,
beginTxActions: ImmutableArray<IAction>.Empty,
endTxActions: ImmutableArray.Create<IAction>(
new GasTraceAction() { CancelTrace = true })),
getMaxTransactionsBytes: _ => 50 * 1024);
var evaluations = Evaluate_CancelTrace(policy);
var exception = (UnexpectedlyTerminatedActionException)evaluations[1].Exception;

Assert.IsType<GasTraceAction>(exception.Action);
Assert.IsType<InvalidOperationException>(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<IAction>.Empty,
endBlockActions: ImmutableArray<IAction>.Empty,
beginTxActions: ImmutableArray<IAction>.Empty,
endTxActions: ImmutableArray<IAction>.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<GasTraceAction>(exception.Action);
Assert.IsType<InvalidOperationException>(exception.InnerException);
Assert.Equal(
"CancelTrace can only be called in TxAction.", exception.InnerException.Message);
}

private IReadOnlyList<ICommittedActionEvaluation> 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;
}
}
}
}

0 comments on commit c1a1c5e

Please sign in to comment.