Skip to content

Commit

Permalink
app: Port Orchid lottery ticket code to Dart with tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
patniemeyer committed Feb 23, 2024
1 parent 1986053 commit cedd75a
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 1 deletion.
5 changes: 5 additions & 0 deletions gui-orchid/lib/api/orchid_crypto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ class Crypto {
static String uuid() {
return Uuid(options: {'grng': UuidUtil.cryptoRNG}).v4();
}

static String formatSecretFixed(BigInt private) {
return private.toRadixString(16).padLeft(64, '0');
}

}

class EthereumKeyPair {
Expand Down
43 changes: 43 additions & 0 deletions gui-orchid/lib/api/orchid_eth/abi_encode.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:typed_data';

import '../orchid_crypto.dart';
import 'package:orchid/util/strings.dart';

Expand All @@ -16,6 +18,10 @@ class AbiEncode {
return value.toRadixString(16).padLeft(64, '0');
}

static String toHexBytes32(BigInt val) {
return '0x' + int256(val);
}

static String uint128(BigInt value) {
return value.toUnsigned(128).toRadixString(16).padLeft(64, '0');
}
Expand Down Expand Up @@ -69,3 +75,40 @@ class AbiEncodePacked {
return (value & 0xff).toRadixString(16).padLeft(2, '0');
}
}

// Add some extension methods to BigInt
extension BigIntExtension on BigInt {
Uint8List toBytes32() {
return toBytesUint256();
}

Uint8List toBytesUint256() {
final number = this;
// Assert the number is non-negative and fits within 256 bits
assert(number >= BigInt.zero && number < (BigInt.one << 256),
'Number must be non-negative and less than 2^256');
var byteData = number.toRadixString(16).padLeft(64, '0'); // Ensure 32 bytes
var result = Uint8List(32);
for (int i = 0; i < byteData.length; i += 2) {
var byteString = byteData.substring(i, i + 2);
var byteValue = int.parse(byteString, radix: 16);
result[i ~/ 2] = byteValue;
}
return result;
}

Uint8List toBytesUint128() {
final number = this;
// Assert the number is non-negative and fits within 128 bits
assert(number >= BigInt.zero && number < (BigInt.one << 128),
'Number must be non-negative and less than 2^128');
var byteData = number.toRadixString(16).padLeft(32, '0'); // Ensure 16 bytes
var result = Uint8List(16);
for (int i = 0; i < byteData.length; i += 2) {
var byteString = byteData.substring(i, i + 2);
var byteValue = int.parse(byteString, radix: 16);
result[i ~/ 2] = byteValue;
}
return result;
}
}
152 changes: 152 additions & 0 deletions gui-orchid/lib/api/orchid_eth/orchid_ticket.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:orchid/util/hex.dart';
import 'dart:typed_data';
import 'package:web3dart/crypto.dart';
import '../orchid_crypto.dart';
import 'abi_encode.dart';
import 'package:web3dart/credentials.dart' as web3;

// Orchid Lottery ticket serialization and evaluation.
// This is direct port of the JS version of this class.
// @see ticket_test for validation.
class OrchidTicket {
final uint64 = BigInt.from(2).pow(64) - BigInt.one;
final uint128 = BigInt.from(2).pow(128) - BigInt.one;
final addrtype = (BigInt.from(2).pow(160)) - BigInt.one;

late BigInt packed0, packed1;
late String sig_r, sig_s;

OrchidTicket({
required BigInt data, // uint256
required EthereumAddress lotaddr,
required EthereumAddress token,
required BigInt amount, // uint128
required BigInt ratio, // uint64
required EthereumAddress funder,
required EthereumAddress recipient,
required BigInt commitment, // bytes32
required BigInt privateKey, // uint256
int? millisecondsSinceEpoch,
}) {
this.initTicketData(data, lotaddr, token, amount, ratio, funder, recipient,
commitment, privateKey,
millisecondsSinceEpoch: millisecondsSinceEpoch);
}

OrchidTicket.fromPacked(
this.packed0,
this.packed1,
this.sig_r,
this.sig_s,
);

OrchidTicket.fromSerialized(String str) {
final ticket = [];
for (var i = 0; i < str.length; i += 64) {
ticket.add(str.substring(i, i + 64));
}
this.packed0 = BigInt.parse(ticket[0], radix: 16);
this.packed1 = BigInt.parse(ticket[1], radix: 16);
this.sig_r = ticket[2].startsWith("0x") ? ticket[2] : '0x' + ticket[2];
this.sig_s = ticket[3].startsWith("0x") ? ticket[3] : '0x' + ticket[3];
}

void initTicketData(
BigInt data, // uint256
EthereumAddress lotaddr,
EthereumAddress token,
BigInt amount,
BigInt ratio,
EthereumAddress funder,
EthereumAddress recipient,
BigInt commitment,
BigInt privateKey, {
int? millisecondsSinceEpoch,
}) {
DateTime nowUtc;
if (millisecondsSinceEpoch != null) {
// print('millisecondsSinceEpoch: $millisecondsSinceEpoch');
nowUtc = DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch,
isUtc: true);
} else {
nowUtc = DateTime.now().toUtc();
}
final dateForNonce =
'${nowUtc.toIso8601String().replaceFirst('T', ' ').replaceFirst('Z', '')}000';
Uint8List hash = keccak256(Uint8List.fromList(utf8.encode(dateForNonce)));
final hashhex = bytesToHex(hash);
final hashint = BigInt.parse(hashhex, radix: 16);
final l2nonce = hashint & uint64;
BigInt expire = BigInt.from(2).pow(31) - BigInt.from(1);
final issued = BigInt.from(nowUtc.millisecondsSinceEpoch / 1000);
BigInt packed0 = (issued << 192) | (l2nonce << 128) | amount;
BigInt packed1 = (expire << 224) |
(ratio << 160) |
BigInt.parse(funder.toString().substring(2), radix: 16);

final encoded = '' +
AbiEncodePacked.bytes1(0x19) +
AbiEncodePacked.bytes1(0x00) +
AbiEncodePacked.address(lotaddr) +
'64'.padLeft(64, '0') +
AbiEncodePacked.address(token) +
AbiEncodePacked.address(recipient) +
bytesToHex(keccak256(commitment.toBytes32())) +
AbiEncodePacked.uint256(packed0) +
AbiEncodePacked.uint256(packed1) +
AbiEncodePacked.uint256(data);

final payload = Uint8List.fromList(hex.decode(encoded));
final credentials =
web3.EthPrivateKey.fromHex(Crypto.formatSecretFixed(privateKey));
MsgSignature sig = sign(keccak256(payload), credentials.privateKey);

packed1 = (packed1 << 1) | BigInt.from((sig.v - 27) & 1);
this.packed0 = packed0;
this.packed1 = packed1;
this.sig_r = AbiEncode.toHexBytes32(sig.r);
this.sig_s = AbiEncode.toHexBytes32(sig.s);
}

String serializeTicket() {
return AbiEncode.uint256(this.packed0) +
AbiEncode.uint256(this.packed1) +
Hex.remove0x(sig_r) +
Hex.remove0x(sig_s);
}

BigInt nonce() {
return (packed0 >> 128) & uint64;
}

bool isWinner(String reveal) {
final ratio = uint64 & (packed1 >> 161);
final revealBytes = Hex.parseBigInt(reveal).toBytesUint256();
final nonceBytes = nonce().toBytesUint128();
final message = Uint8List.fromList([...revealBytes, ...nonceBytes]);
final Uint8List digest = keccak256(message);
final hash = BigInt.parse(bytesToHex(digest), radix: 16);
final comp = uint64 & hash;
return ratio >= comp;
}

void printTicket() {
final amount = packed0 & uint128;
final funder = addrtype & (packed1 >> 1);
final ratio = uint64 & (packed1 >> 161);

print('Ticket data:');
// print(' Data: ${parseInt(this.data, 16)}');
// print(' Reveal: ${this.commitment}');
print(' Packed0: ${this.packed0}');
print(' Packed1: ${this.packed1}');
print('Packed data:');
print(' Amount: $amount');
print(' Nonce: ${nonce()}');
print(' Funder: $funder');
print(' Ratio: $ratio');
print('r: ${this.sig_r}\ns: ${this.sig_s}');
}
}
3 changes: 2 additions & 1 deletion gui-orchid/lib/api/orchid_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ class StoredEthereumKey {

/// Format the secret as a 64 character hex string, zero padded, without prefix.
String formatSecretFixed() {
return private.toRadixString(16).padLeft(64, '0');
return Crypto.formatSecretFixed(private);
}


String toExportString() {
return 'account={ secret: "${formatSecretFixed()}" }';
}
Expand Down
1 change: 1 addition & 0 deletions gui-orchid/lib/util/hex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Hex {
}
}

// Or use hex.decode() from: 'package:convert/convert.dart';
static List<int> decodeBytes(String hexStr) {
hexStr = remove0x(hexStr);
if (hexStr.isEmpty) {
Expand Down
102 changes: 102 additions & 0 deletions gui-orchid/test/ticket_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:orchid/api/orchid_crypto.dart';
import 'package:orchid/api/orchid_eth/orchid_ticket.dart';

void main() {
group('ticket tests', () {
/*
JS version output:
Ticket data:
Data: NaN
Reveal: undefined
Packed0: 0
Packed1: 74418616462743697996404837125958524653305997647975479859375788807275769590323
r: 0x0000000065d6bdd66f8fcd69afdfec200000000000000000016345785d8a0000 s: 0xffffffffccccccccccccd000e04d6ec797cfa9ce4093d4cfd1264c8654a1df09
Packed data:
Amount: 0
Nonce: 0
Funder: 840389638478969449152772927472898064667089090841
Ratio: 10077190556129977236
*/
test('Test serialization', () async {
print("Test serialization round trip");
final ser1 =
'0000000000000000000000000000000000000000000000000000000000000000'
'a48771bb17b2bed6d018c7292668bfda9baa2ccc048b99752fdb684789d97233'
'0000000065d6bdd66f8fcd69afdfec200000000000000000016345785d8a0000'
'ffffffffccccccccccccd000e04d6ec797cfa9ce4093d4cfd1264c8654a1df09';
// Two extra fields in this serialized version?
// The JS lib ignores these as does our impl.
// '7b50455687184c0a1f5ff0be3b4b802a3b2a15205a94d33655b1a7529967c9c9'
// '75f7ee1ed8af30f42902eee69e91c6ca7a936942db9878f34442b187203c2cbb';
final ticket = OrchidTicket.fromSerialized(ser1);
// ticket.printTicket();
expect(ticket.packed0, BigInt.zero);
expect(ticket.packed1.toString(),
'74418616462743697996404837125958524653305997647975479859375788807275769590323');
final ser2 = ticket.serializeTicket();
expect(ser2, ser1);
});

/*
JS output:
Ticket data:
Packed0: 10725299090569305319344573190133412787358038370907851216529272602624
Packed1: 115792089210356248756420345214244490354657239511130867306431705402380410144357
Packed data:
Amount: 2000000000000000000
Nonce: 10444928296939929927
Funder: 111798794203442759563723844757346937785445376818
Ratio: 9223372036854775808
r: 0xd73c001751ebd66407ef8cb61ffc2a77757d2da4e7ea853af744d1123e68fb4a
s: 0x03cc65f3e60f1b007609f69f464a1ec2ae10b39805116434f8c647d63c1e7bb6
*/
test('Test construction', () async {
print("Test construction");
final funder =
EthereumAddress.from('0x13953B378987A76c65F7041BE8CE983381d5E332');
final signer_key = BigInt.parse(
'0x1cf5423866f216ecc2ed50c79447249604d274099e1f8e106dde3a5a6eaea365');
final recipient =
EthereumAddress.from('0x405BC10E04e3f487E9925ad5815E4406D78B769e');
final amountf = 1.0;
final amount = BigInt.from(2000000000000000000) * BigInt.from(amountf);
final data = BigInt.zero;
final lotaddr =
EthereumAddress.from('0x6dB8381b2B41b74E17F5D4eB82E8d5b04ddA0a82');
final token = EthereumAddress.zero;
final ratio = BigInt.parse('9223372036854775808');
final commit = BigInt.parse('0x100');
final ticket = OrchidTicket(
data: data,
lotaddr: lotaddr,
token: token,
amount: amount,
ratio: ratio,
funder: funder,
recipient: recipient,
commitment: commit,
privateKey: signer_key,
millisecondsSinceEpoch: 1708638722494,
);
// ticket.printTicket();
expect(ticket.packed0.toString(),
'10725299090569305319344573190133412787358038370907851216529272602624');
expect(ticket.packed1.toString(),
'115792089210356248756420345214244490354657239511130867306431705402380410144357');
expect(ticket.sig_r,
'0xd73c001751ebd66407ef8cb61ffc2a77757d2da4e7ea853af744d1123e68fb4a');
expect(ticket.sig_s,
'0x03cc65f3e60f1b007609f69f464a1ec2ae10b39805116434f8c647d63c1e7bb6');

print("Test winner");
// test winner (values from the JS test)
expect(ticket.isWinner('0x00'), true);
expect(ticket.isWinner('0x01'), true);
expect(ticket.isWinner('0x05'), false);
expect(ticket.isWinner('0x07'), false);
expect(ticket.isWinner('0x0D'), true);

});
});
}

0 comments on commit cedd75a

Please sign in to comment.