Skip to content

Commit

Permalink
Merge 8828a66 into 252fd28
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia authored Jul 28, 2021
2 parents 252fd28 + 8828a66 commit 9455606
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 22 deletions.
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ To be released.

### Added APIs

- Added `Transaction<T>.CreateUnsigned()` method. [[#1378]]

### Behavioral changes

- `Transaction<T>.Validate()` became to throw `InvalidTxSignatureException`
if the transaction was not signed. [[#1378]]

### Bug fixes

### CLI tools

[#1378]: https://github.com/planetarium/libplanet/pull/1378


Version 0.12.0
--------------

Expand Down
28 changes: 28 additions & 0 deletions Libplanet.Tests/Tx/TransactionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ public void Create()
Assert.Equal(338, tx.BytesLength);
}

[Fact]
public void CreateUnsigned()
{
var tx =
Transaction<PolymorphicAction<BaseAction>>.CreateUnsigned(
0,
_fx.PublicKey1,
null,
_fx.TxWithActions.Actions);
Assert.Empty(tx.Signature);
Assert.Equal(
new[] { _fx.Address1 }.ToImmutableHashSet(),
tx.UpdatedAddresses
);
}

[Fact]
public void CreateWithDefaultUpdatedAddresses()
{
Expand Down Expand Up @@ -547,6 +563,18 @@ public void Validate()
tx.Validate();
}

[Fact]
public void DetectUnsignedTransaction()
{
Transaction<DumbAction> tx = Transaction<DumbAction>.CreateUnsigned(
0,
_fx.PublicKey1,
null,
new DumbAction[0]);

Assert.Throws<InvalidTxSignatureException>(() => tx.Validate());
}

[Fact]
public void DetectBadSignature()
{
Expand Down
138 changes: 116 additions & 22 deletions Libplanet/Tx/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,120 @@ public static Transaction<T> Create(
throw new ArgumentNullException(nameof(privateKey));
}

PublicKey publicKey = privateKey.PublicKey;
Transaction<T> unsignedTransaction = CreateUnsigned(
nonce,
privateKey.PublicKey,
genesisHash,
actions,
updatedAddresses,
timestamp);
byte[] payload = unsignedTransaction.Serialize(false);
byte[] sig = privateKey.Sign(payload);
return new Transaction<T>(
unsignedTransaction.Nonce,
unsignedTransaction.Signer,
unsignedTransaction.PublicKey,
unsignedTransaction.GenesisHash,
unsignedTransaction.UpdatedAddresses,
unsignedTransaction.Timestamp,
unsignedTransaction.Actions,
sig);
}

/// <summary>
/// A fa&#xe7;ade factory to create a new <see cref="Transaction{T}"/>.
/// Unlike the <see cref="Transaction(long, Address, PublicKey, BlockHash?,
/// IImmutableSet{Address}, DateTimeOffset, IEnumerable{T}, byte[])"/>
/// constructor, it automatically fills the following values from:
/// <list type="table">
/// <listheader>
/// <term>Property</term>
/// <description>Parameter the filled value derived from</description>
/// </listheader>
/// <item>
/// <term><see cref="Signer"/></term>
/// <description><paramref name="publicKey"/></description>
/// </item>
/// <item>
/// <term><see cref="PublicKey"/></term>
/// <description><paramref name="publicKey"/></description>
/// </item>
/// <item>
/// <term><see cref="UpdatedAddresses"/></term>
/// <description><paramref name="actions"/> and
/// <paramref name="updatedAddresses"/></description>
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// This factory method tries its best to fill the <see
/// cref="UpdatedAddresses"/> property by actually evaluating
/// the given <paramref name="actions"/> (we call it &#x201c;rehearsal
/// mode&#x201d;), but remember that its result
/// is approximated in some degree, because the result of
/// <paramref name="actions"/> are not deterministic until
/// the <see cref="Transaction{T}"/> belongs to a <see
/// cref="Libplanet.Blocks.Block{T}"/>.
/// <para>If an <see cref="IAction"/> depends on previous states or
/// some randomness to determine what <see cref="Address"/> to update,
/// the automatically filled <see cref="UpdatedAddresses"/> became
/// mismatched from the <see cref="Address"/>es
/// <paramref name="actions"/> actually update after
/// a <see cref="Libplanet.Blocks.Block{T}"/> is mined.
/// Although such case would be rare, a programmer could manually give
/// the <paramref name="updatedAddresses"/> parameter
/// the <see cref="Address"/>es they predict to be updated.</para>
/// <para>If an <see cref="IAction"/> oversimplifies the assumption
/// about the <see cref="Libplanet.Blocks.Block{T}"/> it belongs to,
/// runtime exceptions could be thrown from this factory method.
/// The best solution to that is not to oversimplify things,
/// there is an option to check <see cref="IActionContext"/>'s
/// <see cref="IActionContext.Rehearsal"/> is <c>true</c> and
/// a conditional logic for the case.</para>
/// </remarks>
/// <param name="nonce">The number of previous
/// <see cref="Transaction{T}"/>s committed by the <see cref="Signer"/>
/// of this transaction. This goes to the
/// <see cref="Transaction{T}.Nonce"/> property.</param>
/// <param name="publicKey">A <see cref="PublicKey"/> of the account
/// who creates a new transaction. This key is used to fill
/// the <see cref="Signer"/> and <see cref="PublicKey"/> properties,
/// but this in itself is not included in the transaction.</param>
/// <param name="genesisHash">A <see cref="HashDigest{SHA256}"/> value
/// of the genesis which this <see cref="Transaction{T}"/> is made from.
/// This can be <c>null</c> iff the transaction is contained
/// in the genesis block.
/// </param>
/// <param name="actions">A list of <see cref="IAction"/>s. This
/// can be empty, but cannot be <c>null</c>. This goes to
/// the <see cref="Actions"/> property, and <see cref="IAction"/>s
/// are evaluated before a <see cref="Transaction{T}"/> is created
/// in order to fill the <see cref="UpdatedAddresses"/>. See also
/// <em>Remarks</em> section.</param>
/// <param name="updatedAddresses"><see cref="Address"/>es whose
/// states affected by <paramref name="actions"/>.
/// These <see cref="Address"/>es are also included in
/// the <see cref="UpdatedAddresses"/> property, besides
/// <see cref="Address"/>es projected by evaluating
/// <paramref name="actions"/>. See also <em>Remarks</em> section.
/// </param>
/// <param name="timestamp">The time this <see cref="Transaction{T}"/>
/// is created. This goes to the <see cref="Timestamp"/>
/// property. If <c>null</c> (which is default) is passed this will
/// be the current time.</param>
/// <returns>A created new <see cref="Transaction{T}"/> unsigned.</returns>
/// <exception cref="ArgumentNullException">Thrown when <c>null</c>
/// is passed to <paramref name="actions"/>.
/// </exception>
public static Transaction<T> CreateUnsigned(
long nonce,
PublicKey publicKey,
BlockHash? genesisHash,
IEnumerable<T> actions,
IImmutableSet<Address> updatedAddresses = null,
DateTimeOffset? timestamp = null
)
{
var signer = new Address(publicKey);

if (ReferenceEquals(updatedAddresses, null))
Expand All @@ -425,15 +538,6 @@ public static Transaction<T> Create(
DateTimeOffset ts = timestamp ?? DateTimeOffset.UtcNow;

ImmutableArray<T> actionsArray = actions.ToImmutableArray();
byte[] payload = new Transaction<T>(
nonce,
signer,
publicKey,
genesisHash,
updatedAddresses,
ts,
actionsArray).Serialize(false);

if (!actionsArray.IsEmpty)
{
// FIXME: Although we are assuming all block hashes are SHA256 digest, we should
Expand All @@ -452,27 +556,17 @@ public static Transaction<T> Create(
if (!updatedAddresses.IsSupersetOf(evalUpdatedAddresses))
{
updatedAddresses = updatedAddresses.Union(evalUpdatedAddresses);
payload = new Transaction<T>(
nonce,
signer,
publicKey,
genesisHash,
updatedAddresses,
ts,
actionsArray).Serialize(false);
}
}

byte[] sig = privateKey.Sign(payload);
return new Transaction<T>(
nonce,
signer,
publicKey,
genesisHash,
updatedAddresses,
ts,
actionsArray,
sig);
actionsArray);
}

/// <summary>
Expand Down Expand Up @@ -554,7 +648,7 @@ public Bencodex.Types.Dictionary ToBencodex(bool sign) =>
/// <see cref="Transaction{T}.PublicKey"/>.</exception>
public void Validate()
{
if (!PublicKey.Verify(Serialize(false), Signature))
if (Signature.Length == 0 || !PublicKey.Verify(Serialize(false), Signature))
{
string message =
$"The signature ({ByteUtil.Hex(Signature)}) is failed " +
Expand Down

0 comments on commit 9455606

Please sign in to comment.