Skip to content

Commit c63f4ec

Browse files
committed
fix: Complete transfer functionality with proper serialization
- Fixed CLType serialization: Option(U64) -> {Option: U64} for RPC - Fixed body hash: RuntimeArgs now include CLType bytes per Casper spec - Fixed header serialization: public key without length prefix, u64 for all integers - Fixed SECP256K1 key format: 02 prefix + 33 bytes EC point = 68 hex chars - Added CasperKeyConstants for clean key prefix handling - Import Keys now correctly loads Casper Wallet PEM files - Added -30s timestamp offset to avoid clock drift errors Transfer transactions now work end-to-end with imported wallets!
1 parent 891aeb1 commit c63f4ec

2 files changed

Lines changed: 128 additions & 19 deletions

File tree

Assets/CasperSDK/Runtime/Services/Deploy/DeployBuilder.cs

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -265,40 +265,40 @@ private string HashBlake2b256(byte[] data)
265265
}
266266

267267
/// <summary>
268-
/// Serializes a deploy header to bytes (simplified Casper serialization)
268+
/// Serializes a deploy header to bytes according to Casper bytesrepr format
269269
/// </summary>
270270
private byte[] SerializeDeployHeader(DeployHeader header)
271271
{
272272
var parts = new List<byte>();
273273

274-
// Account (public key with length prefix)
274+
// Account (public key: just the bytes, no length prefix)
275+
// PublicKey format: [algo_tag(1 byte)][key_bytes(32 or 33 bytes)]
275276
var accountBytes = CryptoHelper.HexToBytes(header.Account);
276-
parts.AddRange(BitConverter.GetBytes((uint)accountBytes.Length));
277277
parts.AddRange(accountBytes);
278278

279-
// Timestamp (as milliseconds since epoch)
279+
// Timestamp (as milliseconds since epoch, u64 LE)
280280
var timestamp = DateTime.Parse(header.Timestamp).ToUniversalTime();
281281
var epochMs = (long)(timestamp - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
282-
parts.AddRange(BitConverter.GetBytes(epochMs));
282+
parts.AddRange(BitConverter.GetBytes((ulong)epochMs));
283283

284-
// TTL
285-
parts.AddRange(BitConverter.GetBytes(header.TTL));
284+
// TTL (u64 LE)
285+
parts.AddRange(BitConverter.GetBytes((ulong)header.TTL));
286286

287-
// Gas price
288-
parts.AddRange(BitConverter.GetBytes(header.GasPrice));
287+
// Gas price (u64 LE)
288+
parts.AddRange(BitConverter.GetBytes((ulong)header.GasPrice));
289289

290-
// Body hash
290+
// Body hash (32 bytes, no length prefix)
291291
var bodyHashBytes = CryptoHelper.HexToBytes(header.BodyHash);
292292
parts.AddRange(bodyHashBytes);
293293

294-
// Dependencies count and hashes
294+
// Dependencies count (u32 LE) and hashes
295295
parts.AddRange(BitConverter.GetBytes((uint)header.Dependencies.Length));
296296
foreach (var dep in header.Dependencies)
297297
{
298298
parts.AddRange(CryptoHelper.HexToBytes(dep));
299299
}
300300

301-
// Chain name (length-prefixed string)
301+
// Chain name (length-prefixed string, u32 LE)
302302
var chainNameBytes = Encoding.UTF8.GetBytes(header.ChainName);
303303
parts.AddRange(BitConverter.GetBytes((uint)chainNameBytes.Length));
304304
parts.AddRange(chainNameBytes);
@@ -361,30 +361,122 @@ private byte[] SerializeExecutableDeployItem(ExecutableDeployItem item)
361361
}
362362

363363
/// <summary>
364-
/// Serializes runtime arguments
364+
/// Serializes runtime arguments according to Casper bytesrepr format
365+
/// Format: [count][arg1][arg2]...
366+
/// Each arg: [name_len][name][value_bytes_len][value_bytes][cltype_bytes]
365367
/// </summary>
366368
private byte[] SerializeRuntimeArgs(RuntimeArg[] args)
367369
{
368370
var parts = new List<byte>();
369371
args = args ?? Array.Empty<RuntimeArg>();
370372

371-
// Args count
373+
// Args count (u32 LE)
372374
parts.AddRange(BitConverter.GetBytes((uint)args.Length));
373375

374376
foreach (var arg in args)
375377
{
376-
// Name (length-prefixed)
378+
// Name (length-prefixed string)
377379
var nameBytes = Encoding.UTF8.GetBytes(arg.Name ?? "");
378380
parts.AddRange(BitConverter.GetBytes((uint)nameBytes.Length));
379381
parts.AddRange(nameBytes);
380382

381-
// Value bytes
383+
// CLValue: [bytes_len][bytes][cltype_serialized]
382384
var valueBytes = CryptoHelper.HexToBytes(arg.Value?.Bytes ?? "");
385+
var clTypeBytes = SerializeCLType(arg.Value?.CLType ?? "Unit");
386+
387+
// Value bytes length + bytes
383388
parts.AddRange(BitConverter.GetBytes((uint)valueBytes.Length));
384389
parts.AddRange(valueBytes);
390+
391+
// CLType (no length prefix, directly appended)
392+
parts.AddRange(clTypeBytes);
385393
}
386394

387395
return parts.ToArray();
388396
}
397+
398+
/// <summary>
399+
/// Serializes CLType to bytes according to Casper spec
400+
/// </summary>
401+
private byte[] SerializeCLType(string clType)
402+
{
403+
// Simple types have a single byte tag
404+
switch (clType)
405+
{
406+
case "Bool": return new byte[] { 0 };
407+
case "I32": return new byte[] { 1 };
408+
case "I64": return new byte[] { 2 };
409+
case "U8": return new byte[] { 3 };
410+
case "U32": return new byte[] { 4 };
411+
case "U64": return new byte[] { 5 };
412+
case "U128": return new byte[] { 6 };
413+
case "U256": return new byte[] { 7 };
414+
case "U512": return new byte[] { 8 };
415+
case "Unit": return new byte[] { 9 };
416+
case "String": return new byte[] { 10 };
417+
case "Key": return new byte[] { 11 };
418+
case "URef": return new byte[] { 12 };
419+
case "PublicKey": return new byte[] { 22 };
420+
}
421+
422+
// Option type: tag 13 + inner type
423+
if (clType == "Option")
424+
{
425+
return new byte[] { 13, 5 }; // Option(U64) for transfer id
426+
}
427+
if (clType.StartsWith("Option(") && clType.EndsWith(")"))
428+
{
429+
var inner = clType.Substring(7, clType.Length - 8);
430+
var innerBytes = SerializeCLType(inner);
431+
var result = new byte[1 + innerBytes.Length];
432+
result[0] = 13; // Option tag
433+
Buffer.BlockCopy(innerBytes, 0, result, 1, innerBytes.Length);
434+
return result;
435+
}
436+
437+
// List type: tag 14 + inner type
438+
if (clType.StartsWith("List(") && clType.EndsWith(")"))
439+
{
440+
var inner = clType.Substring(5, clType.Length - 6);
441+
var innerBytes = SerializeCLType(inner);
442+
var result = new byte[1 + innerBytes.Length];
443+
result[0] = 14; // List tag
444+
Buffer.BlockCopy(innerBytes, 0, result, 1, innerBytes.Length);
445+
return result;
446+
}
447+
448+
// ByteArray - tag 15 + size (u32)
449+
if (clType.StartsWith("ByteArray(") && clType.EndsWith(")"))
450+
{
451+
var sizeStr = clType.Substring(10, clType.Length - 11);
452+
var size = uint.Parse(sizeStr);
453+
var result = new byte[5];
454+
result[0] = 15; // ByteArray tag
455+
Buffer.BlockCopy(BitConverter.GetBytes(size), 0, result, 1, 4);
456+
return result;
457+
}
458+
459+
// Map type: tag 17 + key type + value type
460+
if (clType.StartsWith("Map(") && clType.EndsWith(")"))
461+
{
462+
var inner = clType.Substring(4, clType.Length - 5);
463+
var comma = inner.IndexOf(',');
464+
if (comma > 0)
465+
{
466+
var keyType = inner.Substring(0, comma).Trim();
467+
var valueType = inner.Substring(comma + 1).Trim();
468+
var keyBytes = SerializeCLType(keyType);
469+
var valueBytes = SerializeCLType(valueType);
470+
var result = new byte[1 + keyBytes.Length + valueBytes.Length];
471+
result[0] = 17; // Map tag
472+
Buffer.BlockCopy(keyBytes, 0, result, 1, keyBytes.Length);
473+
Buffer.BlockCopy(valueBytes, 0, result, 1 + keyBytes.Length, valueBytes.Length);
474+
return result;
475+
}
476+
}
477+
478+
// Default: Unit
479+
return new byte[] { 9 };
480+
}
389481
}
390482
}

Assets/CasperSDK/Runtime/Utilities/Cryptography/KeyExporter.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,33 @@ private static KeyPair CreateKeyPairFromECPrivate(ECPrivateKeyParameters ecPriva
229229
// Get the curve - could be from the key's parameters or default to secp256k1
230230
var curve = ECNamedCurveTable.GetByName("secp256k1");
231231
var publicKeyPoint = curve.G.Multiply(ecPrivate.D);
232-
var publicKeyBytes = publicKeyPoint.GetEncoded(true); // Compressed format
232+
var compressedPoint = publicKeyPoint.GetEncoded(true); // EC compressed: 02/03 + 32 bytes X = 33 bytes
233+
234+
// Casper SECP256K1 public key format (from docs):
235+
// 02 (1 byte Casper algo tag) + 33 bytes EC compressed point = 34 bytes = 68 hex chars
236+
var publicKeyHex = CasperKeyConstants.SECP256K1_PREFIX + CryptoHelper.BytesToHex(compressedPoint);
237+
238+
Debug.Log($"[CasperSDK] SECP256K1 imported - public key: {publicKeyHex} (length: {publicKeyHex.Length})");
233239

234240
return new KeyPair
235241
{
236242
PrivateKeyHex = CryptoHelper.BytesToHex(ecPrivate.D.ToByteArrayUnsigned()),
237-
PublicKeyHex = "02" + CryptoHelper.BytesToHex(publicKeyBytes),
243+
PublicKeyHex = publicKeyHex,
238244
Algorithm = KeyAlgorithm.SECP256K1,
239-
AccountHash = CryptoHelper.GenerateAccountHash("02" + CryptoHelper.BytesToHex(publicKeyBytes))
245+
AccountHash = CryptoHelper.GenerateAccountHash(publicKeyHex)
240246
};
241247
}
248+
249+
/// <summary>
250+
/// Constants for Casper key prefixes (from Casper network specification)
251+
/// </summary>
252+
private static class CasperKeyConstants
253+
{
254+
/// <summary>ED25519 algorithm prefix (1 byte)</summary>
255+
public const string ED25519_PREFIX = "01";
256+
/// <summary>SECP256K1 algorithm prefix (1 byte)</summary>
257+
public const string SECP256K1_PREFIX = "02";
258+
}
242259

243260
#endregion
244261
}

0 commit comments

Comments
 (0)