Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor GasTracer #3974

Merged
merged 3 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
}
}
Loading