Skip to content

Commit

Permalink
Give TEX addresses their own receiver struct
Browse files Browse the repository at this point in the history
Even though `TransparentP2PKHReceiver` is perfectly serviceable for TEX addresses, reusing the type is inviting bugs where the app or library may misinterpret a TEX address as an ordinary transparent address and neglect the special treatment that TEX addresses require.
  • Loading branch information
AArnott committed May 29, 2024
1 parent 6b1c081 commit 559d6c6
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 18 deletions.
15 changes: 14 additions & 1 deletion src/Nerdbank.Zcash/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,19 @@ Nerdbank.Zcash.SproutReceiver.Span.get -> System.ReadOnlySpan<byte>
Nerdbank.Zcash.SproutReceiver.SproutReceiver() -> void
Nerdbank.Zcash.SproutReceiver.SproutReceiver(System.ReadOnlySpan<byte> apk, System.ReadOnlySpan<byte> pkEnc) -> void
Nerdbank.Zcash.TexAddress
Nerdbank.Zcash.TexAddress.TexAddress(in Nerdbank.Zcash.TransparentP2PKHReceiver receiver, Nerdbank.Zcash.ZcashNetwork network = Nerdbank.Zcash.ZcashNetwork.MainNet) -> void
Nerdbank.Zcash.TexAddress.TexAddress(in Nerdbank.Zcash.TexReceiver receiver, Nerdbank.Zcash.ZcashNetwork network = Nerdbank.Zcash.ZcashNetwork.MainNet) -> void
Nerdbank.Zcash.TexAddress.TexAddress(Nerdbank.Zcash.TransparentP2PKHAddress! transparentAddress) -> void
Nerdbank.Zcash.TexReceiver
Nerdbank.Zcash.TexReceiver.Encode(System.Span<byte> buffer) -> int
Nerdbank.Zcash.TexReceiver.EncodingLength.get -> int
Nerdbank.Zcash.TexReceiver.Equals(Nerdbank.Zcash.TexReceiver other) -> bool
Nerdbank.Zcash.TexReceiver.Pool.get -> Nerdbank.Zcash.Pool
Nerdbank.Zcash.TexReceiver.Span.get -> System.ReadOnlySpan<byte>
Nerdbank.Zcash.TexReceiver.TexReceiver() -> void
Nerdbank.Zcash.TexReceiver.TexReceiver(Nerdbank.Zcash.Transparent.PublicKey! publicKey) -> void
Nerdbank.Zcash.TexReceiver.TexReceiver(Nerdbank.Zcash.Zip32HDWallet.Transparent.ExtendedViewingKey! publicKey) -> void
Nerdbank.Zcash.TexReceiver.TexReceiver(System.ReadOnlySpan<byte> p2pkh) -> void
Nerdbank.Zcash.TexReceiver.ValidatingKeyHash.get -> System.ReadOnlySpan<byte>
Nerdbank.Zcash.Transaction
Nerdbank.Zcash.Transaction.Change.get -> System.Collections.Immutable.ImmutableArray<Nerdbank.Zcash.Transaction.LineItem>
Nerdbank.Zcash.Transaction.ExpiredUnmined.get -> bool
Expand Down Expand Up @@ -891,6 +902,8 @@ static Nerdbank.Zcash.Sapling.DiversifiableIncomingViewingKey.TryDecode(string!
static Nerdbank.Zcash.Sapling.FullViewingKey.TryDecode(string! encoding, out Nerdbank.Cryptocurrencies.DecodeError? decodeError, out string? errorMessage, out Nerdbank.Zcash.Sapling.FullViewingKey? key) -> bool
static Nerdbank.Zcash.Sapling.IncomingViewingKey.TryDecode(System.ReadOnlySpan<char> encoding, out Nerdbank.Cryptocurrencies.DecodeError? decodeError, out string? errorMessage, out Nerdbank.Zcash.Sapling.IncomingViewingKey? key) -> bool
static Nerdbank.Zcash.SaplingReceiver.UnifiedReceiverTypeCode.get -> byte
static Nerdbank.Zcash.TexReceiver.explicit operator Nerdbank.Zcash.TransparentP2PKHReceiver(in Nerdbank.Zcash.TexReceiver receiver) -> Nerdbank.Zcash.TransparentP2PKHReceiver
static Nerdbank.Zcash.TexReceiver.implicit operator Nerdbank.Zcash.TexReceiver(in Nerdbank.Zcash.TransparentP2PKHReceiver receiver) -> Nerdbank.Zcash.TexReceiver
static Nerdbank.Zcash.Transaction.operator !=(Nerdbank.Zcash.Transaction? left, Nerdbank.Zcash.Transaction? right) -> bool
static Nerdbank.Zcash.Transaction.operator ==(Nerdbank.Zcash.Transaction? left, Nerdbank.Zcash.Transaction? right) -> bool
static Nerdbank.Zcash.Transaction.LineItem.operator !=(Nerdbank.Zcash.Transaction.LineItem left, Nerdbank.Zcash.Transaction.LineItem right) -> bool
Expand Down
16 changes: 8 additions & 8 deletions src/Nerdbank.Zcash/TexAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ namespace Nerdbank.Zcash;
/// <remarks>
/// <para>TEX addresses are designed for use by exchanges that require transparently funded accounts for compliance purposes
/// and so that they have an address to return rejected funds.</para>
/// <para>The matching receiver type for this address is <see cref="TransparentP2PKHReceiver"/>.</para>
/// <para>The matching receiver type for this address is <see cref="TexReceiver"/>.</para>
/// <para>This implements <see href="https://zips.z.cash/zip-0320">ZIP-320</see>.</para>
/// </remarks>
public class TexAddress : TransparentAddress
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly TransparentP2PKHReceiver receiver;
private readonly TexReceiver receiver;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly ZcashNetwork network;

/// <inheritdoc cref="TexAddress(string, in TransparentP2PKHReceiver, ZcashNetwork)"/>
public TexAddress(in TransparentP2PKHReceiver receiver, ZcashNetwork network = ZcashNetwork.MainNet)
/// <inheritdoc cref="TexAddress(string, in TexReceiver, ZcashNetwork)"/>
public TexAddress(in TexReceiver receiver, ZcashNetwork network = ZcashNetwork.MainNet)
: base(CreateAddress(receiver, network))
{
this.receiver = receiver;
Expand All @@ -45,7 +45,7 @@ public TexAddress(TransparentP2PKHAddress transparentAddress)
/// <param name="address"><inheritdoc cref="ZcashAddress(string)" path="/param"/></param>
/// <param name="receiver">The encoded receiver.</param>
/// <param name="network">The network to which this address belongs.</param>
internal TexAddress(string address, in TransparentP2PKHReceiver receiver, ZcashNetwork network)
internal TexAddress(string address, in TexReceiver receiver, ZcashNetwork network)
: base(address)
{
this.receiver = receiver;
Expand All @@ -65,7 +65,7 @@ internal TexAddress(string address, in TransparentP2PKHReceiver receiver, ZcashN
internal override int ReceiverEncodingLength => this.receiver.EncodingLength;

/// <inheritdoc/>
public override TPoolReceiver? GetPoolReceiver<TPoolReceiver>() => AsReceiver<TransparentP2PKHReceiver, TPoolReceiver>(this.receiver);
public override TPoolReceiver? GetPoolReceiver<TPoolReceiver>() => AsReceiver<TexReceiver, TPoolReceiver>(this.receiver);

/// <summary>
/// Checks whether a given string looks like a Zcash TEX address.
Expand Down Expand Up @@ -107,7 +107,7 @@ internal static bool TryParse(string address, [NotNullWhen(false)] out DecodeErr
return false;
}

result = new TexAddress(address, new TransparentP2PKHReceiver(data), network.Value);
result = new TexAddress(address, new TexReceiver(data), network.Value);
return true;
}

Expand All @@ -120,7 +120,7 @@ internal static bool TryParse(string address, [NotNullWhen(false)] out DecodeErr
/// <inheritdoc/>
internal override int GetReceiverEncoding(Span<byte> output) => this.receiver.Encode(output);

private static string CreateAddress(in TransparentP2PKHReceiver receiver, ZcashNetwork network)
private static string CreateAddress(in TexReceiver receiver, ZcashNetwork network)
{
string hrp = network switch
{
Expand Down
81 changes: 81 additions & 0 deletions src/Nerdbank.Zcash/TexReceiver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Nerdbank.Zcash;

/// <summary>
/// A receiver that contains the cryptography parameters required to send Zcash to the <see cref="Pool.Transparent"/> pool
/// by way of a Pay to Public Key Hash method.
/// </summary>
/// <remarks>
/// This receiver is used for <see cref="TexAddress"/> to represent receivers that must only be sent transparent funds.
/// It is otherwise equivalent to <see cref="TransparentP2PKHReceiver"/>.
/// </remarks>
public unsafe struct TexReceiver : IPoolReceiver, IEquatable<TexReceiver>
{
private readonly TransparentP2PKHReceiver p2pkhReceiver;

/// <summary>
/// Initializes a new instance of the <see cref="TexReceiver"/> struct.
/// </summary>
/// <param name="p2pkh">The validating key hash.</param>
/// <exception cref="ArgumentException">Thrown when the arguments have an unexpected length.</exception>
public TexReceiver(ReadOnlySpan<byte> p2pkh)
{
this.p2pkhReceiver = new(p2pkh);
}

/// <summary>
/// Initializes a new instance of the <see cref="TexReceiver"/> struct.
/// </summary>
/// <param name="publicKey">The EC public key to create a receiver for.</param>
public TexReceiver(Zip32HDWallet.Transparent.ExtendedViewingKey publicKey)
{
this.p2pkhReceiver = new(publicKey);
}

/// <summary>
/// Initializes a new instance of the <see cref="TexReceiver"/> struct.
/// </summary>
/// <param name="publicKey">The EC public key to create a receiver for.</param>
public TexReceiver(Transparent.PublicKey publicKey)
{
this.p2pkhReceiver = new(publicKey);
}

/// <inheritdoc/>
public readonly Pool Pool => Pool.Transparent;

/// <summary>
/// Gets the encoded representation of the entire receiver.
/// </summary>
[UnscopedRef]
public readonly ReadOnlySpan<byte> Span => this.ValidatingKeyHash;

/// <inheritdoc />
public readonly int EncodingLength => this.Span.Length;

/// <summary>
/// Gets the validating key hash.
/// </summary>
[UnscopedRef]
public readonly ReadOnlySpan<byte> ValidatingKeyHash => this.p2pkhReceiver.ValidatingKeyHash;

/// <summary>
/// Converts a <see cref="TransparentP2PKHReceiver"/> into a <see cref="TexReceiver" />.
/// </summary>
/// <param name="receiver">The receiver to convert from.</param>
public static implicit operator TexReceiver(in TransparentP2PKHReceiver receiver) => new(receiver.ValidatingKeyHash);

/// <summary>
/// Converts a <see cref="TexReceiver"/> into a <see cref="TransparentP2PKHReceiver" />.
/// </summary>
/// <param name="receiver">The receiver to convert from.</param>
public static explicit operator TransparentP2PKHReceiver(in TexReceiver receiver) => new(receiver.ValidatingKeyHash);

/// <inheritdoc/>
public int Encode(Span<byte> buffer) => this.Span.CopyToRetLength(buffer);

/// <inheritdoc/>
public bool Equals(TexReceiver other) => this.ValidatingKeyHash.SequenceEqual(other.ValidatingKeyHash);
}
6 changes: 0 additions & 6 deletions src/Nerdbank.Zcash/TransparentAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ namespace Nerdbank.Zcash;
/// <summary>
/// A transparent Zcash address.
/// </summary>
/// <remarks>
/// Not all transparent addresses carry the same semantics.
/// For example, care should be taken when extracting a <see cref="TransparentP2PKHReceiver"/>
/// to check whether the <see cref="TransparentAddress"/> is an instance of <see cref="TexAddress"/>,
/// which should only receive funds from unshielded sources.
/// </remarks>
public abstract class TransparentAddress : ZcashAddress
{
/// <summary>
Expand Down
4 changes: 3 additions & 1 deletion test/Nerdbank.Zcash.Tests/TexAddressTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public void SameReceiverAsTransparent()
{
var tex = (TexAddress)ZcashAddress.Decode("tex1s2rt77ggv6q989lr49rkgzmh5slsksa9khdgte");
var tAddr = (TransparentP2PKHAddress)ZcashAddress.Decode("t1VmmGiyjVNeCjxDZzg7vZmd99WyzVby9yC");
Assert.Equal(tAddr.GetPoolReceiver<TransparentP2PKHReceiver>(), tex.GetPoolReceiver<TransparentP2PKHReceiver>());
Assert.Equal(
tAddr.GetPoolReceiver<TransparentP2PKHReceiver>()!.Value.ValidatingKeyHash,
tex.GetPoolReceiver<TexReceiver>()!.Value.ValidatingKeyHash);
}
}
47 changes: 47 additions & 0 deletions test/Nerdbank.Zcash.Tests/TexReceiverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

public class TexReceiverTests
{
[Fact]
public void Ctor()
{
byte[] hash = new byte[20];
hash[1] = 2;
TexReceiver receiver = new(hash);
Assert.Equal(hash, receiver.ValidatingKeyHash.ToArray());

// Verify that a copy of the data has been made.
hash[0] = 3;
Assert.Equal(0, receiver.ValidatingKeyHash[0]);
}

[Fact]
public void Ctor_ArgValidation()
{
Assert.Throws<ArgumentException>(() => new TexReceiver(new byte[1]));
}

[Fact]
public void Pool_Transparent() => Assert.Equal(Pool.Transparent, default(TexReceiver).Pool);

[Fact]
public void TexToTransparentConversion()
{
Span<byte> p2pkh = stackalloc byte[20];
Random.Shared.NextBytes(p2pkh);
TexReceiver texReceiver = new(p2pkh);
TransparentP2PKHReceiver transparentReceiver = (TransparentP2PKHReceiver)texReceiver;
Assert.Equal(texReceiver.ValidatingKeyHash, transparentReceiver.ValidatingKeyHash);
}

[Fact]
public void TransparentToTexConversion()
{
Span<byte> p2pkh = stackalloc byte[20];
Random.Shared.NextBytes(p2pkh);
TransparentP2PKHReceiver transparentReceiver = new(p2pkh);
TexReceiver texReceiver = transparentReceiver;
Assert.Equal(transparentReceiver.ValidatingKeyHash, texReceiver.ValidatingKeyHash);
}
}
2 changes: 1 addition & 1 deletion test/Nerdbank.Zcash.Tests/TransparentP2PKHReceiverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public void Ctor_ArgValidation()
}

[Fact]
public void Pool_Orchard() => Assert.Equal(Pool.Transparent, default(TransparentP2PKHReceiver).Pool);
public void Pool_Transparent() => Assert.Equal(Pool.Transparent, default(TransparentP2PKHReceiver).Pool);

[Fact]
public void UnifiedReceiverTypeCode() => Assert.Equal(0x02, TransparentP2PKHReceiver.UnifiedReceiverTypeCode);
Expand Down
2 changes: 1 addition & 1 deletion test/Nerdbank.Zcash.Tests/TransparentP2SHReceiverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public void Ctor_ArgValidation()
}

[Fact]
public void Pool_Orchard() => Assert.Equal(Pool.Transparent, default(TransparentP2SHReceiver).Pool);
public void Pool_Transparent() => Assert.Equal(Pool.Transparent, default(TransparentP2SHReceiver).Pool);

[Fact]
public void UnifiedReceiverTypeCode() => Assert.Equal(0x01, TransparentP2SHReceiver.UnifiedReceiverTypeCode);
Expand Down

0 comments on commit 559d6c6

Please sign in to comment.