-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
app: Port Orchid lottery ticket code to Dart with tests.
- Loading branch information
1 parent
1986053
commit cedd75a
Showing
6 changed files
with
305 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
}); | ||
}); | ||
} |