/
Address.cs
334 lines (301 loc) · 12.7 KB
/
Address.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
using System;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using Libplanet.Crypto;
using Libplanet.Serialization;
using Org.BouncyCastle.Crypto.Digests;
namespace Libplanet
{
/// <summary>
/// An identifier of 20 bytes (or 40 letters in hexadecimal, commonly with
/// a prefix <c>0x</c>) that refers to a unique account.
/// <para>It is derived from the corresponding <see cref="PublicKey"/>
/// of an account, but as a derivation loses information, it is always
/// unidirectional.</para>
/// <para>The address derivation from a public key is as follows:</para>
/// <list type="number">
/// <item><description>Calculates the Keccak-256, which is a previous form
/// of SHA-3 before NIST standardized it and does not follow
/// <a href="http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf"
/// >FIPS-202</a>, of the corresponding <see cref="PublicKey"/>.
/// </description></item>
/// <item><description>Takes only the last 20 bytes of the calculated
/// Keccak-256 hash.</description></item>
/// <item><description>When the address needs to be shown to end users,
/// displays these 20 bytes in hexadecimal, with a prefix <c>0x</c>.
/// </description></item>
/// </list>
/// <para>Since the scheme of the address derivation and the <see
/// cref="PrivateKey"/>/<see cref="PublicKey"/> is the same to
/// <a href="https://www.ethereum.org/">Ethereum</a>, Ethereum addresses
/// can be used by Libplanet-backed games/apps too.</para>
/// </summary>
/// <remarks>Every <see cref="Address"/> value is immutable.</remarks>
/// <seealso cref="PublicKey"/>
[Serializable]
[Equals]
public readonly struct Address : ISerializable, IComparable<Address>, IComparable
{
/// <summary>
/// The <see cref="byte"/>s size that each <see cref="Address"/> takes.
/// <para>It is 20 <see cref="byte"/>s.</para>
/// </summary>
public const int Size = 20;
private static readonly byte[] _defaultByteArray = new byte[Size];
private readonly ImmutableArray<byte> _byteArray;
/// <summary>
/// Creates an <see cref="Address"/> instance from the given immutable <see
/// cref="byte"/> array (i.e., <paramref name="address"/>).
/// </summary>
/// <param name="address">An immutable array of 20 <see cref="byte"/>s which
/// represents an <see cref="Address"/>.</param>
/// <exception cref="ArgumentException">Thrown when the given <paramref
/// name="address"/> array did not lengthen 20 bytes.</exception>
/// <remarks>A valid <see cref="byte"/> array which represents an
/// <see cref="Address"/> can be gotten using <see cref="ToByteArray()"
/// /> method.</remarks>
/// <seealso cref="ByteArray"/>
public Address(ImmutableArray<byte> address)
{
if (address.Length != Size)
{
throw new ArgumentException("address must be 20 bytes", nameof(address));
}
_byteArray = address;
}
/// <summary>
/// Creates an <see cref="Address"/> instance from the given <see
/// cref="byte"/> array (i.e., <paramref name="address"/>).
/// </summary>
/// <param name="address">An array of 20 <see cref="byte"/>s which
/// represents an <see cref="Address"/>. This must not be <c>null</c>.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <c>null</c> was
/// passed to <paramref name="address"/>.</exception>
/// <exception cref="ArgumentException">Thrown when the given <paramref
/// name="address"/> array did not lengthen 20 bytes.</exception>
/// <remarks>A valid <see cref="byte"/> array which represents an
/// <see cref="Address"/> can be gotten using <see cref="ToByteArray()"
/// /> method.</remarks>
/// <seealso cref="ToByteArray()"/>
public Address(byte[] address)
: this(address?.ToImmutableArray() ?? throw new ArgumentNullException(nameof(address)))
{
}
public Address(
SerializationInfo info,
StreamingContext context)
: this(info.GetValue<byte[]>("address"))
{
}
/// <summary>
/// Derives the corresponding <see cref="Address"/> from a <see
/// cref="PublicKey"/>.
/// <para>Note that there is an equivalent extension method
/// <see cref="AddressExtensions.ToAddress(PublicKey)"/>, which enables
/// a code like <c>publicKey.ToAddress()</c> instead of
/// <c>new Address(publicKey)</c>, for convenience.</para>
/// </summary>
/// <param name="publicKey">A <see cref="PublicKey"/> to derive
/// the corresponding <see cref="Address"/> from.</param>
/// <seealso cref="AddressExtensions.ToAddress(PublicKey)"/>
public Address(PublicKey publicKey)
: this(DeriveAddress(publicKey))
{
}
/// <summary>
/// Derives the corresponding <see cref="Address"/> from a hexadecimal
/// address string.
/// </summary>
/// <exception cref="ArgumentNullException">Thrown when <c>null</c> was
/// passed to <paramref name="hex"/>.</exception>
/// <exception cref="ArgumentException">Thrown when the given <paramref
/// name="hex"/> did not lengthen 40 characters.</exception>
/// <exception cref="ArgumentException">Thrown when the given <paramref
/// name="hex"/> is mixed-case and the checksum is invalid.</exception>
/// <exception cref="ArgumentException">Thrown when the given <paramref
/// name="hex"/> does not consist of ASCII characters.</exception>
/// <param name="hex">A 40 characters hexadecimal address string to
/// derive the corresponding <see cref="Address"/> from. The string
/// should be all lower-case or mixed-case which follows <a
/// href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md"
/// >EIP 55</a>.</param>
public Address(string hex)
: this(DeriveAddress(hex))
{
}
/// <summary>
/// An immutable array of 20 <see cref="byte"/>s that represent this
/// <see cref="Address"/>.
/// </summary>
/// <remarks>This is immutable. For a mutable array, call <see
/// cref="ToByteArray()"/> method.</remarks>
/// <seealso cref="ToByteArray()"/>
public ImmutableArray<byte> ByteArray
{
get
{
if (_byteArray.IsDefault)
{
return _defaultByteArray.ToImmutableArray();
}
return _byteArray;
}
}
public static bool operator ==(Address left, Address right) => Operator.Weave(left, right);
public static bool operator !=(Address left, Address right) => Operator.Weave(left, right);
/// <summary>
/// Gets a mutable array of 20 <see cref="byte"/>s that represent
/// this <see cref="Address"/>.
/// </summary>
/// <returns>A new mutable array which represents this
/// <see cref="Address"/>. Since it is created every time the method
/// is called, any mutation on that does not affect internal states of
/// this <see cref="Address"/>.</returns>
/// <seealso cref="ByteArray"/>
/// <seealso cref="Address(byte[])"/>
[Pure]
public byte[] ToByteArray() => ByteArray.IsDefault
? _defaultByteArray
: ByteArray.ToArray();
/// <summary>
/// Gets a mixed-case hexadecimal string of 40 letters that represent
/// this <see cref="Address"/>. The returned hexadecimal string follows
/// <a
/// href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md"
/// >EIP 55</a>.
/// </summary>
/// <example>A returned string looks like
/// <c>87Ae4774E20963fd6caC967CF47aDCF880C3e89B</c>.</example>
/// <returns>A hexadecimal string of 40 letters that represent
/// this <see cref="Address"/>. Note that it does not start with
/// a prefix.</returns>
/// <remarks>As the returned string has no prefix, for
/// <c>0x</c>-prefixed hexadecimal, call <see cref="ToString()"/>
/// method instead.</remarks>
/// <seealso cref="ToString()"/>
[Pure]
public string ToHex()
{
string hex = ByteUtil.Hex(ToByteArray());
return ToChecksumAddress(hex);
}
/// <summary>
/// Gets a <c>0x</c>-prefixed mixed-case hexadecimal string of
/// 42 letters that represent this <see cref="Address"/>. The returned
/// hexadecimal string follows
/// <a
/// href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md"
/// >EIP 55</a>.
/// </summary>
/// <example>A returned string looks like
/// <c>0x87Ae4774E20963fd6caC967CF47aDCF880C3e89B</c>.</example>
/// <returns>A <c>0x</c>-hexadecimal string of 42 letters that represent
/// this <see cref="Address"/>.</returns>
/// <remarks>As the returned string is <c>0x</c>-prefixed, for
/// hexadecimal without prefix, call <see cref="ToHex()"/> method
/// instead.</remarks>
/// <seealso cref="ToHex()"/>
[Pure]
public override string ToString()
{
return $"0x{ToHex()}";
}
/// <inheritdoc />
public void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
info.AddValue("address", ToByteArray());
}
int IComparable<Address>.CompareTo(Address other)
{
ImmutableArray<byte> self = ByteArray, operand = other.ByteArray;
for (int i = 0; i < Size; i++)
{
int cmp = ((IComparable<byte>)self[i]).CompareTo(operand[i]);
if (cmp != 0)
{
return cmp;
}
}
return 0;
}
int IComparable.CompareTo(object obj)
{
if (obj is Address other)
{
return ((IComparable<Address>)this).CompareTo(other);
}
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
throw new ArgumentException(nameof(obj));
}
private static string ToChecksumAddress(string hex)
{
byte[] bytes = Encoding.ASCII.GetBytes(hex);
byte[] hash = CalculateHash(bytes);
string hashHex = ByteUtil.Hex(hash);
string address = string.Empty;
for (var i = 0; i < hex.Length; i++)
{
char c = hex[i];
address += (hashHex[i] >= '8') ? char.ToUpper(c, CultureInfo.InvariantCulture) : c;
}
return address;
}
private static byte[] CalculateHash(byte[] value)
{
var digest = new KeccakDigest(256);
var output = new byte[digest.GetDigestSize()];
digest.BlockUpdate(value, 0, value.Length);
digest.DoFinal(output, 0);
return output;
}
private static byte[] DeriveAddress(PublicKey key)
{
byte[] hashPayload = key.Format(false).Skip(1).ToArray();
var output = CalculateHash(hashPayload);
return output.Skip(output.Length - Size).ToArray();
}
private static byte[] DeriveAddress(string hex)
{
if (hex == null)
{
throw new ArgumentNullException(nameof(hex));
}
if (hex.Length != 40)
{
throw new ArgumentException(
"Address hex must be 40 bytes, but " +
$"{hex.Length} bytes were passed.",
nameof(hex)
);
}
if (hex.ToLower(CultureInfo.InvariantCulture) != hex &&
ToChecksumAddress(hex.ToLower(CultureInfo.InvariantCulture)) != hex)
{
throw new ArgumentException(
"address checksum is invalid",
nameof(hex)
);
}
try
{
return ByteUtil.ParseHex(hex);
}
catch (FormatException)
{
throw new ArgumentException(
"address hex must only consist of ASCII characters"
);
}
}
}
}